feat(classes.portproxy): Add support for port range-based routing with enhanced IP and port validation.
This commit is contained in:
		| @@ -1,5 +1,12 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-02-27 - 3.15.0 - feat(classes.portproxy) | ||||
| Add support for port range-based routing with enhanced IP and port validation. | ||||
|  | ||||
| - Introduced globalPortRanges in IPortProxySettings for routing based on port ranges. | ||||
| - Improved connection handling with port range and domain configuration validations. | ||||
| - Updated connection logging to include the local port information. | ||||
|  | ||||
| ## 2025-02-26 - 3.14.2 - fix(PortProxy) | ||||
| Fix cleanup timer reset for PortProxy | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '3.14.2', | ||||
|   version: '3.15.0', | ||||
|   description: 'A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.' | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,14 @@ | ||||
| import * as plugins from './plugins.js'; | ||||
|  | ||||
| /** Domain configuration with per‐domain allowed port ranges */ | ||||
| export interface IDomainConfig { | ||||
|   domain: string;         // Glob pattern for domain | ||||
|   allowedIPs: string[];   // Glob patterns for allowed IPs | ||||
|   targetIP?: string;      // Optional target IP for this domain | ||||
|   portRanges: Array<{ from: number; to: number }>; // Domain-specific allowed port ranges | ||||
| } | ||||
|  | ||||
| /** Port proxy settings including global allowed port ranges */ | ||||
| export interface IPortProxySettings extends plugins.tls.TlsOptions { | ||||
|   fromPort: number; | ||||
|   toPort: number; | ||||
| @@ -14,7 +17,8 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions { | ||||
|   sniEnabled?: boolean; | ||||
|   defaultAllowedIPs?: string[]; | ||||
|   preserveSourceIP?: boolean; | ||||
|   maxConnectionLifetime?: number; // New option (in milliseconds) to force cleanup of long-lived connections | ||||
|   maxConnectionLifetime?: number; // (ms) force cleanup of long-lived connections | ||||
|   globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -144,12 +148,18 @@ export class PortProxy { | ||||
|       ); | ||||
|     }; | ||||
|  | ||||
|     // Find a matching domain config based on the SNI. | ||||
|     // Check if a port falls within any of the given port ranges. | ||||
|     const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => { | ||||
|       return ranges.some(range => port >= range.from && port <= range.to); | ||||
|     }; | ||||
|  | ||||
|     // Find a matching domain config based on SNI (fallback when port ranges aren’t used) | ||||
|     const findMatchingDomain = (serverName: string): IDomainConfig | undefined => | ||||
|       this.settings.domains.find(config => plugins.minimatch(serverName, config.domain)); | ||||
|  | ||||
|     this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => { | ||||
|       const remoteIP = socket.remoteAddress || ''; | ||||
|       const localPort = socket.localPort; // The port on which this connection was accepted. | ||||
|       const connectionRecord: IConnectionRecord = { | ||||
|         incoming: socket, | ||||
|         outgoing: null, | ||||
| @@ -157,7 +167,7 @@ export class PortProxy { | ||||
|         connectionClosed: false, | ||||
|       }; | ||||
|       this.connectionRecords.add(connectionRecord); | ||||
|       console.log(`New connection from ${remoteIP}. Active connections: ${this.connectionRecords.size}`); | ||||
|       console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`); | ||||
|  | ||||
|       let initialDataReceived = false; | ||||
|       let incomingTerminationReason: string | null = null; | ||||
| @@ -225,24 +235,27 @@ export class PortProxy { | ||||
|         cleanupOnce(); | ||||
|       }; | ||||
|  | ||||
|       const setupConnection = (serverName: string, initialChunk?: Buffer) => { | ||||
|       /** | ||||
|        * Sets up the connection to the target host. | ||||
|        * @param serverName - The SNI hostname (unused when forcedDomain is provided). | ||||
|        * @param initialChunk - Optional initial data chunk. | ||||
|        * @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing). | ||||
|        */ | ||||
|       const setupConnection = (serverName: string, initialChunk?: Buffer, forcedDomain?: IDomainConfig) => { | ||||
|         // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup. | ||||
|         const domainConfig = forcedDomain ? forcedDomain : (serverName ? findMatchingDomain(serverName) : undefined); | ||||
|         const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs); | ||||
|  | ||||
|         if (!defaultAllowed && serverName) { | ||||
|           const domainConfig = findMatchingDomain(serverName); | ||||
|         if (!defaultAllowed && serverName && !forcedDomain) { | ||||
|           if (!domainConfig) { | ||||
|             return rejectIncomingConnection('rejected', `Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`); | ||||
|           } | ||||
|           if (!isAllowed(remoteIP, domainConfig.allowedIPs)) { | ||||
|             return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`); | ||||
|           } | ||||
|         } else if (!defaultAllowed && !serverName) { | ||||
|           return rejectIncomingConnection('rejected', `Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`); | ||||
|         } else if (defaultAllowed && !serverName) { | ||||
|           console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`); | ||||
|         } | ||||
|  | ||||
|         const domainConfig = serverName ? findMatchingDomain(serverName) : undefined; | ||||
|         const targetHost = domainConfig?.targetIP || this.settings.toHost!; | ||||
|         const connectionOptions: plugins.net.NetConnectOpts = { | ||||
|           host: targetHost, | ||||
| @@ -258,7 +271,7 @@ export class PortProxy { | ||||
|  | ||||
|         console.log( | ||||
|           `Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}` + | ||||
|           `${serverName ? ` (SNI: ${serverName})` : ''}` | ||||
|           `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domain})` : ''}` | ||||
|         ); | ||||
|  | ||||
|         if (initialChunk) { | ||||
| @@ -292,7 +305,7 @@ export class PortProxy { | ||||
|         socket.on('end', handleClose('incoming')); | ||||
|         targetSocket.on('end', handleClose('outgoing')); | ||||
|  | ||||
|         // If maxConnectionLifetime is set, initialize a cleanup timer that will be reset on data flow. | ||||
|         // Initialize a cleanup timer for max connection lifetime. | ||||
|         if (this.settings.maxConnectionLifetime) { | ||||
|           let incomingActive = false; | ||||
|           let outgoingActive = false; | ||||
| @@ -308,10 +321,8 @@ export class PortProxy { | ||||
|             } | ||||
|           }; | ||||
|  | ||||
|           // Start the cleanup timer. | ||||
|           resetCleanupTimer(); | ||||
|  | ||||
|           // Listen for data events on both sides and reset the timer when both are active. | ||||
|           socket.on('data', () => { | ||||
|             incomingActive = true; | ||||
|             if (incomingActive && outgoingActive) { | ||||
| @@ -331,6 +342,37 @@ export class PortProxy { | ||||
|         } | ||||
|       }; | ||||
|  | ||||
|       // --- PORT RANGE-BASED HANDLING --- | ||||
|       // If global port ranges are defined, enforce port-based routing and ignore SNI. | ||||
|       if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) { | ||||
|         if (!isPortInRanges(localPort, this.settings.globalPortRanges)) { | ||||
|           console.log(`Connection from ${remoteIP} rejected: port ${localPort} is not in global allowed ranges.`); | ||||
|           socket.destroy(); | ||||
|           return; | ||||
|         } | ||||
|         // Find a matching domain config based on the incoming local port. | ||||
|         const forcedDomain = this.settings.domains.find( | ||||
|           domain => domain.portRanges && domain.portRanges.length > 0 && isPortInRanges(localPort, domain.portRanges) | ||||
|         ); | ||||
|         if (!forcedDomain) { | ||||
|           console.log(`Connection from ${remoteIP} rejected: port ${localPort} not configured in any domain's portRanges.`); | ||||
|           socket.destroy(); | ||||
|           return; | ||||
|         } | ||||
|         // Check allowed IPs for the forced domain. | ||||
|         const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs); | ||||
|         if (!defaultAllowed && !isAllowed(remoteIP, forcedDomain.allowedIPs)) { | ||||
|           console.log(`Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domain} on port ${localPort}.`); | ||||
|           socket.end(); | ||||
|           return; | ||||
|         } | ||||
|         console.log(`Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domain}.`); | ||||
|         // Proceed immediately using the forced domain; ignore SNI. | ||||
|         setupConnection('', undefined, forcedDomain); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // --- FALLBACK: SNI-BASED HANDLING (if no global port ranges are defined) --- | ||||
|       if (this.settings.sniEnabled) { | ||||
|         socket.setTimeout(5000, () => { | ||||
|           console.log(`Initial data timeout for ${remoteIP}`); | ||||
| @@ -363,7 +405,7 @@ export class PortProxy { | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|     // Every 10 seconds log active connection count and longest running durations. | ||||
|     // Log active connection count and longest running durations every 10 seconds. | ||||
|     this.connectionLogger = setInterval(() => { | ||||
|       const now = Date.now(); | ||||
|       let maxIncoming = 0; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user