feat(smart-proxy): calculate when SNI is required for TLS routing and allow session tickets for single-target passthrough routes; add tests, docs, and npm metadata updates

This commit is contained in:
2026-01-30 10:44:28 +00:00
parent ea3b8290d2
commit c2dd7494d6
6 changed files with 482 additions and 21 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '22.3.0',
version: '22.4.0',
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
}

View File

@@ -69,6 +69,58 @@ export class RouteConnectionHandler {
};
}
/**
* Determines if SNI is required for routing decisions on this port.
*
* SNI is REQUIRED when:
* - Multiple routes exist on this port (need SNI to pick correct route)
* - Route has dynamic target function (needs ctx.domain)
* - Route has specific domain restriction (strict validation)
*
* SNI is NOT required when:
* - TLS termination mode (HttpProxy handles session resumption)
* - Single route with static target and no domain restriction (or wildcard)
*/
private calculateSniRequirement(port: number): boolean {
const routesOnPort = this.smartProxy.routeManager.getRoutesForPort(port);
// No routes = no SNI requirement (will fail routing anyway)
if (routesOnPort.length === 0) return false;
// Check if any route terminates TLS - if so, SNI not required
// (HttpProxy handles session resumption internally)
const hasTermination = routesOnPort.some(route =>
route.action.tls?.mode === 'terminate' ||
route.action.tls?.mode === 'terminate-and-reencrypt'
);
if (hasTermination) return false;
// Multiple routes = need SNI to pick the correct route
if (routesOnPort.length > 1) return true;
// Single route - check if it needs SNI for validation or routing
const route = routesOnPort[0];
// Dynamic host selection requires SNI (function receives ctx.domain)
const hasDynamicTarget = route.action.targets?.some(t => typeof t.host === 'function');
if (hasDynamicTarget) return true;
// Specific domain restriction requires SNI for strict validation
const hasSpecificDomain = route.match.domains && !this.isWildcardOnly(route.match.domains);
if (hasSpecificDomain) return true;
// Single route, static target(s), no domain restriction = SNI not required
return false;
}
/**
* Check if domains config is wildcard-only (matches everything)
*/
private isWildcardOnly(domains: string | string[]): boolean {
const domainList = Array.isArray(domains) ? domains : [domains];
return domainList.length === 1 && domainList[0] === '*';
}
/**
* Handle a new incoming connection
*/
@@ -201,19 +253,10 @@ export class RouteConnectionHandler {
route.action.tls.mode === 'passthrough');
});
// Auto-calculate session ticket handling based on route configuration
// If any route on this port terminates TLS, allow session tickets (HttpProxy handles resumption)
// Otherwise, block session tickets (need SNI for passthrough routing)
const hasTlsTermination = allRoutes.some(route => {
const matchesPort = this.smartProxy.routeManager.getRoutesForPort(localPort).includes(route);
return matchesPort &&
route.action.type === 'forward' &&
route.action.tls &&
(route.action.tls.mode === 'terminate' ||
route.action.tls.mode === 'terminate-and-reencrypt');
});
const allowSessionTicket = hasTlsTermination;
// Smart SNI requirement calculation
// Determines if we need SNI for routing decisions on this port
const needsSniForRouting = this.calculateSniRequirement(localPort);
const allowSessionTicket = !needsSniForRouting;
// If no routes require TLS handling and it's not port 443, route immediately
if (!needsTlsHandling && localPort !== 443) {