import * as plugins from '../plugins.js'; import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js'; import type { ISecurityCompiledPolicy } from '../../ts_interfaces/data/security-policy.js'; import type { DcRouter } from '../classes.dcrouter.js'; type TInboundProxyProtocolPolicy = NonNullable; type TIngressOrigin = NonNullable< NonNullable['ingress'] >['allowedOrigins'][number]; /** * Pure route-policy transformations applied before routes reach SmartProxy: * route-level trusted ingress authorization, listener-level inbound PROXY * protocol policy, and merging of compiled security policies. */ export class RoutePolicyAugmenter { constructor(private dcRouterRef: DcRouter) {} /** * Compile each dcrouter ingress policy into SmartProxy route security, then * derive one compatible PROXY protocol mode for every shared listener. * * The listener mode only controls parsing. Route security remains the * authorization boundary, so a direct request cannot fall through to a * wildcard route that only permits RemoteIngress or SmartVPN. */ public applyInboundProxyProtocolPolicies( routes: plugins.smartproxy.IRouteConfig[], ): plugins.smartproxy.IRouteConfig[] { const routesWithIngressSecurity = routes.map((route) => { const allowedOrigins = this.getAllowedIngressOrigins(route); if (allowedOrigins.length === 0) { throw new Error(`Route '${route.name}' has no enabled ingress path`); } return { ...route, security: { ...route.security, ingress: { allowedOrigins, }, }, }; }); const originsByListener = new Map>(); for (const route of routesWithIngressSecurity) { const origins = route.security!.ingress!.allowedOrigins; for (const listenerKey of this.getInboundProxyListenerKeys(route)) { const listenerOrigins = originsByListener.get(listenerKey) || new Set(); for (const origin of origins) { listenerOrigins.add(origin); } originsByListener.set(listenerKey, listenerOrigins); } } return routesWithIngressSecurity.map((route) => { const listenerOrigins = new Set(); for (const listenerKey of this.getInboundProxyListenerKeys(route)) { for (const origin of originsByListener.get(listenerKey) || []) { listenerOrigins.add(origin); } } const inboundProxyProtocol = this.getListenerProxyProtocolPolicy(listenerOrigins); return { ...route, match: { ...route.match, inboundProxyProtocol, }, }; }); } /** Union blocked IPs/CIDRs across configured and compiled security policies. */ public mergeSecurityPolicies( ...policies: Array | undefined> ): ISecurityCompiledPolicy | undefined { const blockedIps = new Set(); const blockedCidrs = new Set(); for (const policy of policies) { for (const ip of policy?.blockedIps || []) { if (ip) blockedIps.add(ip); } for (const cidr of policy?.blockedCidrs || []) { if (cidr) blockedCidrs.add(cidr); } } if (blockedIps.size === 0 && blockedCidrs.size === 0) { return undefined; } return { blockedIps: [...blockedIps].sort(), blockedCidrs: [...blockedCidrs].sort(), }; } private getAllowedIngressOrigins( route: plugins.smartproxy.IRouteConfig, ): TIngressOrigin[] { const dcRoute = route as IDcRouterRouteConfig; const origins = new Set(); if (dcRoute.ingress) { if (dcRoute.ingress.directHub) origins.add('direct'); if (dcRoute.ingress.smartVpn) origins.add('smartVpn'); } else if (dcRoute.vpnOnly) { origins.add('smartVpn'); } else { // Compatibility for runtime routes and producers predating the explicit // contract. Persisted routes are backfilled by the versioned migration. origins.add('direct'); origins.add('smartVpn'); } if (dcRoute.remoteIngress?.enabled) { origins.add('trustedProxy'); } return [...origins]; } private getListenerProxyProtocolPolicy( origins: Set, ): TInboundProxyProtocolPolicy { const allowsDirect = origins.has('direct'); const allowsProxied = origins.has('trustedProxy') || origins.has('smartVpn'); if (allowsDirect && allowsProxied) return { mode: 'optional' }; if (allowsProxied) return { mode: 'required' }; return { mode: 'reject' }; } private getInboundProxyListenerKeys(route: plugins.smartproxy.IRouteConfig): string[] { const ports = plugins.smartproxy.expandPortRange(route.match.ports as any) as number[]; const transports = route.match.transport === 'udp' ? ['udp'] : route.match.transport === 'all' ? ['tcp', 'udp'] : ['tcp']; const keys: string[] = []; for (const port of ports) { for (const transport of transports) { keys.push(`${transport}:${port}`); } } return keys; } }