feat(PortProxy): Add active connection tracking and logging in PortProxy
This commit is contained in:
		| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '3.7.3', | ||||
|   version: '3.8.0', | ||||
|   description: 'a proxy for handling high workloads of proxying' | ||||
| } | ||||
|   | ||||
| @@ -115,6 +115,9 @@ function extractSNI(buffer: Buffer): string | undefined { | ||||
| export class PortProxy { | ||||
|   netServer: plugins.net.Server; | ||||
|   settings: IProxySettings; | ||||
|   // Track active incoming connections | ||||
|   private activeConnections: Set<plugins.net.Socket> = new Set(); | ||||
|   private connectionLogger: NodeJS.Timeout | null = null; | ||||
|  | ||||
|   constructor(settings: IProxySettings) { | ||||
|     this.settings = { | ||||
| @@ -161,81 +164,73 @@ export class PortProxy { | ||||
|       return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain)); | ||||
|     }; | ||||
|  | ||||
|     // Always create a plain net server for TLS passthrough. | ||||
|     // Create a plain net server for TLS passthrough. | ||||
|     this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => { | ||||
|       const remoteIP = socket.remoteAddress || ''; | ||||
|        | ||||
|       // If SNI is enabled, we peek at the first chunk to extract the SNI. | ||||
|       if (this.settings.sniEnabled) { | ||||
|         socket.once('data', (chunk: Buffer) => { | ||||
|           // Try to extract the server name from the ClientHello. | ||||
|           const serverName = extractSNI(chunk) || ''; | ||||
|           console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`); | ||||
|       // Track the new incoming connection. | ||||
|       this.activeConnections.add(socket); | ||||
|       console.log(`New connection from ${remoteIP}. Active connections: ${this.activeConnections.size}`); | ||||
|  | ||||
|           // Check if the IP is allowed by default. | ||||
|           const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs); | ||||
|           if (!isDefaultAllowed && serverName) { | ||||
|             const domainConfig = findMatchingDomain(serverName); | ||||
|             if (!domainConfig) { | ||||
|               console.log(`Connection rejected: No matching domain config for ${serverName} from IP ${remoteIP}`); | ||||
|               socket.end(); | ||||
|               return; | ||||
|             } | ||||
|             if (!isAllowed(remoteIP, domainConfig.allowedIPs)) { | ||||
|               console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`); | ||||
|               socket.end(); | ||||
|               return; | ||||
|             } | ||||
|           } else if (!isDefaultAllowed && !serverName) { | ||||
|             console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`); | ||||
|       // Flag to ensure cleanup happens only once. | ||||
|       let connectionClosed = false; | ||||
|       const cleanupOnce = () => { | ||||
|         if (!connectionClosed) { | ||||
|           connectionClosed = true; | ||||
|           cleanUpSockets(socket, to); | ||||
|           if (this.activeConnections.has(socket)) { | ||||
|             this.activeConnections.delete(socket); | ||||
|             console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.activeConnections.size}`); | ||||
|           } | ||||
|         } | ||||
|       }; | ||||
|  | ||||
|       let to: plugins.net.Socket; | ||||
|  | ||||
|       const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => { | ||||
|         const code = (err as any).code; | ||||
|         if (code === 'ECONNRESET') { | ||||
|           console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`); | ||||
|         } else { | ||||
|           console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`); | ||||
|         } | ||||
|         cleanupOnce(); | ||||
|       }; | ||||
|  | ||||
|       const handleClose = (side: 'incoming' | 'outgoing') => () => { | ||||
|         console.log(`Connection closed on ${side} side from ${remoteIP}`); | ||||
|         cleanupOnce(); | ||||
|       }; | ||||
|  | ||||
|       // Setup connection, optionally accepting the initial data chunk. | ||||
|       const setupConnection = (serverName: string, initialChunk?: Buffer) => { | ||||
|         // Check if the IP is allowed by default. | ||||
|         const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs); | ||||
|         if (!isDefaultAllowed && serverName) { | ||||
|           const domainConfig = findMatchingDomain(serverName); | ||||
|           if (!domainConfig) { | ||||
|             console.log(`Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`); | ||||
|             socket.end(); | ||||
|             return; | ||||
|           } else { | ||||
|             console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`); | ||||
|           } | ||||
|  | ||||
|           // Determine target host. | ||||
|           const domainConfig = serverName ? findMatchingDomain(serverName) : undefined; | ||||
|           const targetHost = domainConfig?.targetIP || this.settings.toHost!; | ||||
|  | ||||
|           // Create connection options. | ||||
|           const connectionOptions: plugins.net.NetConnectOpts = { | ||||
|             host: targetHost, | ||||
|             port: this.settings.toPort, | ||||
|           }; | ||||
|           if (this.settings.preserveSourceIP) { | ||||
|             connectionOptions.localAddress = remoteIP.replace('::ffff:', ''); | ||||
|           if (!isAllowed(remoteIP, domainConfig.allowedIPs)) { | ||||
|             console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`); | ||||
|             socket.end(); | ||||
|             return; | ||||
|           } | ||||
|  | ||||
|           const to = plugins.net.connect(connectionOptions); | ||||
|           console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`); | ||||
|  | ||||
|           // Unshift the data chunk back so that the TLS handshake can complete at the backend. | ||||
|           socket.unshift(chunk); | ||||
|           socket.setTimeout(120000); | ||||
|           socket.pipe(to); | ||||
|           to.pipe(socket); | ||||
|  | ||||
|           const errorHandler = () => { | ||||
|             cleanUpSockets(socket, to); | ||||
|           }; | ||||
|           socket.on('error', errorHandler); | ||||
|           to.on('error', errorHandler); | ||||
|           socket.on('close', errorHandler); | ||||
|           to.on('close', errorHandler); | ||||
|           socket.on('timeout', errorHandler); | ||||
|           to.on('timeout', errorHandler); | ||||
|           socket.on('end', errorHandler); | ||||
|           to.on('end', errorHandler); | ||||
|         }); | ||||
|       } else { | ||||
|         // If SNI is not enabled, use defaultAllowedIPs check. | ||||
|         if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) { | ||||
|           console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`); | ||||
|         } else if (!isDefaultAllowed && !serverName) { | ||||
|           console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`); | ||||
|           socket.end(); | ||||
|           return; | ||||
|         } else { | ||||
|           console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`); | ||||
|         } | ||||
|         const targetHost = this.settings.toHost!; | ||||
|  | ||||
|         // Determine target host. | ||||
|         const domainConfig = serverName ? findMatchingDomain(serverName) : undefined; | ||||
|         const targetHost = domainConfig?.targetIP || this.settings.toHost!; | ||||
|  | ||||
|         // Create connection options. | ||||
|         const connectionOptions: plugins.net.NetConnectOpts = { | ||||
|           host: targetHost, | ||||
|           port: this.settings.toPort, | ||||
| @@ -243,22 +238,46 @@ export class PortProxy { | ||||
|         if (this.settings.preserveSourceIP) { | ||||
|           connectionOptions.localAddress = remoteIP.replace('::ffff:', ''); | ||||
|         } | ||||
|         const to = plugins.net.connect(connectionOptions); | ||||
|         console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}`); | ||||
|  | ||||
|         // Establish outgoing connection. | ||||
|         to = plugins.net.connect(connectionOptions); | ||||
|         console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`); | ||||
|          | ||||
|         // Push back the initial chunk if provided. | ||||
|         if (initialChunk) { | ||||
|           socket.unshift(initialChunk); | ||||
|         } | ||||
|         socket.setTimeout(120000); | ||||
|         socket.pipe(to); | ||||
|         to.pipe(socket); | ||||
|         const errorHandler = () => { | ||||
|           cleanUpSockets(socket, to); | ||||
|         }; | ||||
|         socket.on('error', errorHandler); | ||||
|         to.on('error', errorHandler); | ||||
|         socket.on('close', errorHandler); | ||||
|         to.on('close', errorHandler); | ||||
|         socket.on('timeout', errorHandler); | ||||
|         to.on('timeout', errorHandler); | ||||
|         socket.on('end', errorHandler); | ||||
|         to.on('end', errorHandler); | ||||
|  | ||||
|         // Attach error and close handlers for both sockets. | ||||
|         socket.on('error', handleError('incoming')); | ||||
|         to.on('error', handleError('outgoing')); | ||||
|         socket.on('close', handleClose('incoming')); | ||||
|         to.on('close', handleClose('outgoing')); | ||||
|         socket.on('timeout', handleError('incoming')); | ||||
|         to.on('timeout', handleError('outgoing')); | ||||
|         socket.on('end', handleClose('incoming')); | ||||
|         to.on('end', handleClose('outgoing')); | ||||
|       }; | ||||
|  | ||||
|       // For SNI-enabled connections, peek at the first chunk. | ||||
|       if (this.settings.sniEnabled) { | ||||
|         socket.once('data', (chunk: Buffer) => { | ||||
|           // Try to extract the server name from the ClientHello. | ||||
|           const serverName = extractSNI(chunk) || ''; | ||||
|           console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`); | ||||
|           setupConnection(serverName, chunk); | ||||
|         }); | ||||
|       } else { | ||||
|         // For non-SNI connections, simply check defaultAllowedIPs. | ||||
|         if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) { | ||||
|           console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`); | ||||
|           socket.end(); | ||||
|           return; | ||||
|         } | ||||
|         setupConnection(''); | ||||
|       } | ||||
|     }) | ||||
|     .on('error', (err: Error) => { | ||||
| @@ -267,6 +286,11 @@ export class PortProxy { | ||||
|     .listen(this.settings.fromPort, () => { | ||||
|       console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`); | ||||
|     }); | ||||
|  | ||||
|     // Log active connection count every 10 seconds. | ||||
|     this.connectionLogger = setInterval(() => { | ||||
|       console.log(`(Interval Log) Active connections: ${this.activeConnections.size}`); | ||||
|     }, 10000); | ||||
|   } | ||||
|  | ||||
|   public async stop() { | ||||
| @@ -274,6 +298,10 @@ export class PortProxy { | ||||
|     this.netServer.close(() => { | ||||
|       done.resolve(); | ||||
|     }); | ||||
|     if (this.connectionLogger) { | ||||
|       clearInterval(this.connectionLogger); | ||||
|       this.connectionLogger = null; | ||||
|     } | ||||
|     await done.promise; | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user