From 0e605d9a9df7d3fb5099a9857f7ebfc1bc8c5e59 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Thu, 6 Mar 2025 22:56:18 +0000 Subject: [PATCH] fix(PortProxy): Improved code formatting and readability in PortProxy class by adjusting spacing and comments. --- changelog.md | 6 + ts/00_commitinfo_data.ts | 2 +- ts/classes.portproxy.ts | 770 +++++++++++++++++++++++++-------------- 3 files changed, 494 insertions(+), 284 deletions(-) diff --git a/changelog.md b/changelog.md index b368b5a..10ece37 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,11 @@ # Changelog +## 2025-03-06 - 3.28.1 - fix(PortProxy) +Improved code formatting and readability in PortProxy class by adjusting spacing and comments. + +- Adjusted comment and spacing for better code readability. +- No functional changes made in the PortProxy class. + ## 2025-03-06 - 3.28.0 - feat(router) Add detailed routing tests and refactor ProxyRouter for improved path matching diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index f666c53..95c96b3 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '3.28.0', + version: '3.28.1', description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.' } diff --git a/ts/classes.portproxy.ts b/ts/classes.portproxy.ts index 01f19db..5b99472 100644 --- a/ts/classes.portproxy.ts +++ b/ts/classes.portproxy.ts @@ -2,10 +2,10 @@ import * as plugins from './plugins.js'; /** Domain configuration with per-domain allowed port ranges */ export interface IDomainConfig { - domains: string[]; // Glob patterns for domain(s) - allowedIPs: string[]; // Glob patterns for allowed IPs - blockedIPs?: string[]; // Glob patterns for blocked IPs - targetIPs?: string[]; // If multiple targetIPs are given, use round robin. + domains: string[]; // Glob patterns for domain(s) + allowedIPs: string[]; // Glob patterns for allowed IPs + blockedIPs?: string[]; // Glob patterns for blocked IPs + targetIPs?: string[]; // If multiple targetIPs are given, use round robin. portRanges?: Array<{ from: number; to: number }>; // Optional port ranges // Allow domain-specific timeout override connectionTimeout?: number; // Connection timeout override (ms) @@ -21,33 +21,33 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions { defaultAllowedIPs?: string[]; defaultBlockedIPs?: string[]; preserveSourceIP?: boolean; - + // Timeout settings - initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s) - socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h) - inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s) - maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h) - inactivityTimeout?: number; // Inactivity timeout (ms), default: 3600000 (1h) - - gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown + initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s) + socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h) + inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s) + maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h) + inactivityTimeout?: number; // Inactivity timeout (ms), default: 3600000 (1h) + + gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges - forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP - + forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP + // Socket optimization settings - noDelay?: boolean; // Disable Nagle's algorithm (default: true) - keepAlive?: boolean; // Enable TCP keepalive (default: true) - keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms) - maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup - + noDelay?: boolean; // Disable Nagle's algorithm (default: true) + keepAlive?: boolean; // Enable TCP keepalive (default: true) + keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms) + maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup + // Enhanced features - disableInactivityCheck?: boolean; // Disable inactivity checking entirely - enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes - enableDetailedLogging?: boolean; // Enable detailed connection logging - enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging + disableInactivityCheck?: boolean; // Disable inactivity checking entirely + enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes + enableDetailedLogging?: boolean; // Enable detailed connection logging + enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd - + // Rate limiting and security - maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP + maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP } @@ -55,28 +55,28 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions { * Enhanced connection record */ interface IConnectionRecord { - id: string; // Unique connection identifier + id: string; // Unique connection identifier incoming: plugins.net.Socket; outgoing: plugins.net.Socket | null; incomingStartTime: number; outgoingStartTime?: number; outgoingClosedTime?: number; - lockedDomain?: string; // Used to lock this connection to the initial SNI - connectionClosed: boolean; // Flag to prevent multiple cleanup attempts - cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity - lastActivity: number; // Last activity timestamp for inactivity detection - pendingData: Buffer[]; // Buffer to hold data during connection setup - pendingDataSize: number; // Track total size of pending data - + lockedDomain?: string; // Used to lock this connection to the initial SNI + connectionClosed: boolean; // Flag to prevent multiple cleanup attempts + cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity + lastActivity: number; // Last activity timestamp for inactivity detection + pendingData: Buffer[]; // Buffer to hold data during connection setup + pendingDataSize: number; // Track total size of pending data + // Enhanced tracking fields - bytesReceived: number; // Total bytes received - bytesSent: number; // Total bytes sent - remoteIP: string; // Remote IP (cached for logging after socket close) - localPort: number; // Local port (cached for logging) - isTLS: boolean; // Whether this connection is a TLS connection - tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete - hasReceivedInitialData: boolean; // Whether initial data has been received - domainConfig?: IDomainConfig; // Associated domain config for this connection + bytesReceived: number; // Total bytes received + bytesSent: number; // Total bytes sent + remoteIP: string; // Remote IP (cached for logging after socket close) + localPort: number; // Local port (cached for logging) + isTLS: boolean; // Whether this connection is a TLS connection + tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete + hasReceivedInitialData: boolean; // Whether initial data has been received + domainConfig?: IDomainConfig; // Associated domain config for this connection } /** @@ -90,7 +90,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un try { // Check if buffer is too small for TLS if (buffer.length < 5) { - if (enableLogging) console.log("Buffer too small for TLS header"); + if (enableLogging) console.log('Buffer too small for TLS header'); return undefined; } @@ -105,11 +105,14 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un const majorVersion = buffer.readUInt8(1); const minorVersion = buffer.readUInt8(2); if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`); - + // Check record length const recordLength = buffer.readUInt16BE(3); if (buffer.length < 5 + recordLength) { - if (enableLogging) console.log(`Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`); + if (enableLogging) + console.log( + `Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}` + ); return undefined; } @@ -121,12 +124,12 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un } offset += 4; // Skip handshake header (type + length) - + // Client version const clientMajorVersion = buffer.readUInt8(offset); const clientMinorVersion = buffer.readUInt8(offset + 1); if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`); - + offset += 2 + 32; // Skip client version and random // Session ID @@ -136,7 +139,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un // Cipher suites if (offset + 2 > buffer.length) { - if (enableLogging) console.log("Buffer too small for cipher suites length"); + if (enableLogging) console.log('Buffer too small for cipher suites length'); return undefined; } const cipherSuitesLength = buffer.readUInt16BE(offset); @@ -145,7 +148,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un // Compression methods if (offset + 1 > buffer.length) { - if (enableLogging) console.log("Buffer too small for compression methods length"); + if (enableLogging) console.log('Buffer too small for compression methods length'); return undefined; } const compressionMethodsLength = buffer.readUInt8(offset); @@ -154,7 +157,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un // Extensions if (offset + 2 > buffer.length) { - if (enableLogging) console.log("Buffer too small for extensions length"); + if (enableLogging) console.log('Buffer too small for extensions length'); return undefined; } const extensionsLength = buffer.readUInt16BE(offset); @@ -163,7 +166,10 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un const extensionsEnd = offset + extensionsLength; if (extensionsEnd > buffer.length) { - if (enableLogging) console.log(`Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`); + if (enableLogging) + console.log( + `Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}` + ); return undefined; } @@ -171,45 +177,56 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un while (offset + 4 <= extensionsEnd) { const extensionType = buffer.readUInt16BE(offset); const extensionLength = buffer.readUInt16BE(offset + 2); - - if (enableLogging) console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`); - + + if (enableLogging) + console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`); + offset += 4; - - if (extensionType === 0x0000) { // SNI extension + + if (extensionType === 0x0000) { + // SNI extension if (offset + 2 > buffer.length) { - if (enableLogging) console.log("Buffer too small for SNI list length"); + if (enableLogging) console.log('Buffer too small for SNI list length'); return undefined; } - + const sniListLength = buffer.readUInt16BE(offset); if (enableLogging) console.log(`SNI List Length: ${sniListLength}`); offset += 2; const sniListEnd = offset + sniListLength; - + if (sniListEnd > buffer.length) { - if (enableLogging) console.log(`Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`); + if (enableLogging) + console.log( + `Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}` + ); return undefined; } - + while (offset + 3 < sniListEnd) { const nameType = buffer.readUInt8(offset++); const nameLen = buffer.readUInt16BE(offset); offset += 2; - + if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`); - - if (nameType === 0) { // host_name + + if (nameType === 0) { + // host_name if (offset + nameLen > buffer.length) { - if (enableLogging) console.log(`Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${buffer.length}`); + if (enableLogging) + console.log( + `Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${ + buffer.length + }` + ); return undefined; } - + const serverName = buffer.toString('utf8', offset, offset + nameLen); if (enableLogging) console.log(`Extracted SNI: ${serverName}`); return serverName; } - + offset += nameLen; } break; @@ -217,8 +234,8 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un offset += extensionLength; } } - - if (enableLogging) console.log("No SNI extension found"); + + if (enableLogging) console.log('No SNI extension found'); return undefined; } catch (err) { console.log(`Error extracting SNI: ${err}`); @@ -228,13 +245,13 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un // Helper: 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); + return ranges.some((range) => port >= range.from && port <= range.to); }; // Helper: Check if a given IP matches any of the glob patterns const isAllowed = (ip: string, patterns: string[]): boolean => { if (!ip || !patterns || patterns.length === 0) return false; - + const normalizeIP = (ip: string): string[] => { if (!ip) return []; if (ip.startsWith('::ffff:')) { @@ -246,13 +263,13 @@ const isAllowed = (ip: string, patterns: string[]): boolean => { } return [ip]; }; - + const normalizedIPVariants = normalizeIP(ip); if (normalizedIPVariants.length === 0) return false; - + const expandedPatterns = patterns.flatMap(normalizeIP); - return normalizedIPVariants.some(ipVariant => - expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern)) + return normalizedIPVariants.some((ipVariant) => + expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern)) ); }; @@ -297,7 +314,7 @@ export class PortProxy { incoming: {}, outgoing: {}, }; - + // Connection tracking by IP for rate limiting private connectionsByIP: Map> = new Map(); private connectionRateByIP: Map = new Map(); @@ -307,29 +324,29 @@ export class PortProxy { this.settings = { ...settingsArg, targetIP: settingsArg.targetIP || 'localhost', - + // Timeout settings with our enhanced defaults - initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial data - socketTimeout: settingsArg.socketTimeout || 3600000, // 1 hour socket timeout + initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake + socketTimeout: settingsArg.socketTimeout || 2592000000, // 30 days socket timeout inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval - maxConnectionLifetime: settingsArg.maxConnectionLifetime || 3600000, // 1 hour default lifetime - inactivityTimeout: settingsArg.inactivityTimeout || 3600000, // 1 hour inactivity timeout - + maxConnectionLifetime: settingsArg.maxConnectionLifetime || 2592000000, // 30 days max lifetime + inactivityTimeout: settingsArg.inactivityTimeout || 14400000, // 4 hours inactivity timeout + gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds - + // Socket optimization settings noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true, keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true, keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes - + // Feature flags disableInactivityCheck: settingsArg.disableInactivityCheck || false, enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false, enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || true, - + // Rate limiting defaults maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute @@ -349,17 +366,17 @@ export class PortProxy { private checkConnectionRate(ip: string): boolean { const now = Date.now(); const minute = 60 * 1000; - + if (!this.connectionRateByIP.has(ip)) { this.connectionRateByIP.set(ip, [now]); return true; } - + // Get timestamps and filter out entries older than 1 minute - const timestamps = this.connectionRateByIP.get(ip)!.filter(time => now - time < minute); + const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute); timestamps.push(now); this.connectionRateByIP.set(ip, timestamps); - + // Check if rate exceeds limit return timestamps.length <= this.settings.connectionRateLimitPerMinute!; } @@ -402,14 +419,14 @@ export class PortProxy { if (record.domainConfig?.connectionTimeout) { return record.domainConfig.connectionTimeout; } - + // Use default timeout, potentially randomized const baseTimeout = this.settings.maxConnectionLifetime!; - + if (this.settings.enableRandomizedTimeouts) { return randomizeTimeout(baseTimeout); } - + return baseTimeout; } @@ -422,20 +439,20 @@ export class PortProxy { private cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void { if (!record.connectionClosed) { record.connectionClosed = true; - + // Track connection termination this.removeConnectionByIP(record.remoteIP, record.id); - + if (record.cleanupTimer) { clearTimeout(record.cleanupTimer); record.cleanupTimer = undefined; } - + // Detailed logging data const duration = Date.now() - record.incomingStartTime; const bytesReceived = record.bytesReceived; const bytesSent = record.bytesSent; - + try { if (!record.incoming.destroyed) { // Try graceful shutdown first, then force destroy after a short timeout @@ -449,7 +466,7 @@ export class PortProxy { console.log(`[${record.id}] Error destroying incoming socket: ${err}`); } }, 1000); - + // Ensure the timeout doesn't block Node from exiting if (incomingTimeout.unref) { incomingTimeout.unref(); @@ -465,7 +482,7 @@ export class PortProxy { console.log(`[${record.id}] Error destroying incoming socket: ${destroyErr}`); } } - + try { if (record.outgoing && !record.outgoing.destroyed) { // Try graceful shutdown first, then force destroy after a short timeout @@ -479,7 +496,7 @@ export class PortProxy { console.log(`[${record.id}] Error destroying outgoing socket: ${err}`); } }, 1000); - + // Ensure the timeout doesn't block Node from exiting if (outgoingTimeout.unref) { outgoingTimeout.unref(); @@ -495,21 +512,27 @@ export class PortProxy { console.log(`[${record.id}] Error destroying outgoing socket: ${destroyErr}`); } } - + // Clear pendingData to avoid memory leaks record.pendingData = []; record.pendingDataSize = 0; - + // Remove the record from the tracking map this.connectionRecords.delete(record.id); - + // Log connection details if (this.settings.enableDetailedLogging) { - console.log(`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` + - ` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` + - `TLS: ${record.isTLS ? 'Yes' : 'No'}`); + console.log( + `[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` + + ` Duration: ${plugins.prettyMs( + duration + )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` + + `TLS: ${record.isTLS ? 'Yes' : 'No'}` + ); } else { - console.log(`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`); + console.log( + `[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}` + ); } } } @@ -543,7 +566,7 @@ export class PortProxy { console.log("Cannot start PortProxy while it's shutting down"); return; } - + // Define a unified connection handler for all listening ports. const connectionHandler = (socket: plugins.net.Socket) => { if (this.isShuttingDown) { @@ -554,29 +577,35 @@ export class PortProxy { const remoteIP = socket.remoteAddress || ''; const localPort = socket.localPort || 0; // The port on which this connection was accepted. - + // Check rate limits - if (this.settings.maxConnectionsPerIP && - this.getConnectionCountByIP(remoteIP) >= this.settings.maxConnectionsPerIP) { - console.log(`Connection rejected from ${remoteIP}: Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`); + if ( + this.settings.maxConnectionsPerIP && + this.getConnectionCountByIP(remoteIP) >= this.settings.maxConnectionsPerIP + ) { + console.log( + `Connection rejected from ${remoteIP}: Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded` + ); socket.end(); socket.destroy(); return; } - + if (this.settings.connectionRateLimitPerMinute && !this.checkConnectionRate(remoteIP)) { - console.log(`Connection rejected from ${remoteIP}: Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`); + console.log( + `Connection rejected from ${remoteIP}: Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded` + ); socket.end(); socket.destroy(); return; } - + // Apply socket optimizations socket.setNoDelay(this.settings.noDelay); if (this.settings.keepAlive) { socket.setKeepAlive(true, this.settings.keepAliveInitialDelay); } - + // Apply enhanced TCP options if available if (this.settings.enableKeepAliveProbes) { try { @@ -591,7 +620,7 @@ export class PortProxy { // Ignore errors - these are optional enhancements } } - + // Create a unique connection ID and record const connectionId = generateConnectionId(); const connectionRecord: IConnectionRecord = { @@ -603,7 +632,7 @@ export class PortProxy { connectionClosed: false, pendingData: [], pendingDataSize: 0, - + // Initialize enhanced tracking fields bytesReceived: 0, bytesSent: 0, @@ -611,17 +640,21 @@ export class PortProxy { localPort: localPort, isTLS: false, tlsHandshakeComplete: false, - hasReceivedInitialData: false + hasReceivedInitialData: false, }; - + // Track connection by IP this.trackConnectionByIP(remoteIP, connectionId); this.connectionRecords.set(connectionId, connectionRecord); - + if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`); + console.log( + `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}` + ); } else { - console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`); + console.log( + `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}` + ); } let initialDataReceived = false; @@ -632,7 +665,7 @@ export class PortProxy { const cleanupOnce = () => { this.cleanupConnection(connectionRecord); }; - + // Define initiateCleanupOnce for compatibility const initiateCleanupOnce = (reason: string = 'normal') => { if (this.settings.enableDetailedLogging) { @@ -661,7 +694,9 @@ export class PortProxy { if (this.settings.sniEnabled) { initialTimeout = setTimeout(() => { if (!initialDataReceived) { - console.log(`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`); + console.log( + `[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}` + ); if (incomingTerminationReason === null) { incomingTerminationReason = 'initial_timeout'; this.incrementTerminationStat('incoming', 'initial_timeout'); @@ -670,7 +705,7 @@ export class PortProxy { cleanupOnce(); } }, this.settings.initialDataTimeout!); - + // Make sure timeout doesn't keep the process alive if (initialTimeout.unref) { initialTimeout.unref(); @@ -688,13 +723,15 @@ export class PortProxy { socket.on('data', (chunk: Buffer) => { connectionRecord.bytesReceived += chunk.length; this.updateActivity(connectionRecord); - + // Check for TLS handshake if this is the first chunk if (!connectionRecord.isTLS && isTlsHandshake(chunk)) { connectionRecord.isTLS = true; - + if (this.settings.enableTlsDebugLogging) { - console.log(`[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`); + console.log( + `[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes` + ); // Try to extract SNI and log detailed debug info extractSNI(chunk, true); } @@ -704,21 +741,39 @@ export class PortProxy { const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => { const code = (err as any).code; let reason = 'error'; - + const now = Date.now(); const connectionDuration = now - connectionRecord.incomingStartTime; const lastActivityAge = now - connectionRecord.lastActivity; - + if (code === 'ECONNRESET') { reason = 'econnreset'; - console.log(`[${connectionId}] ECONNRESET on ${side} side from ${remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`); + console.log( + `[${connectionId}] ECONNRESET on ${side} side from ${remoteIP}: ${ + err.message + }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs( + lastActivityAge + )} ago` + ); } else if (code === 'ETIMEDOUT') { reason = 'etimedout'; - console.log(`[${connectionId}] ETIMEDOUT on ${side} side from ${remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`); + console.log( + `[${connectionId}] ETIMEDOUT on ${side} side from ${remoteIP}: ${ + err.message + }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs( + lastActivityAge + )} ago` + ); } else { - console.log(`[${connectionId}] Error on ${side} side from ${remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`); + console.log( + `[${connectionId}] Error on ${side} side from ${remoteIP}: ${ + err.message + }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs( + lastActivityAge + )} ago` + ); } - + if (side === 'incoming' && incomingTerminationReason === null) { incomingTerminationReason = reason; this.incrementTerminationStat('incoming', reason); @@ -726,7 +781,7 @@ export class PortProxy { outgoingTerminationReason = reason; this.incrementTerminationStat('outgoing', reason); } - + initiateCleanupOnce(reason); }; @@ -734,7 +789,7 @@ export class PortProxy { if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Connection closed on ${side} side from ${remoteIP}`); } - + if (side === 'incoming' && incomingTerminationReason === null) { incomingTerminationReason = 'normal'; this.incrementTerminationStat('incoming', 'normal'); @@ -744,7 +799,7 @@ export class PortProxy { // Record the time when outgoing socket closed. connectionRecord.outgoingClosedTime = Date.now(); } - + initiateCleanupOnce('closed_' + side); }; @@ -755,54 +810,80 @@ export class PortProxy { * @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing). * @param overridePort - If provided, use this port for the outgoing connection. */ - const setupConnection = (serverName: string, initialChunk?: Buffer, forcedDomain?: IDomainConfig, overridePort?: number) => { + const setupConnection = ( + serverName: string, + initialChunk?: Buffer, + forcedDomain?: IDomainConfig, + overridePort?: number + ) => { // Clear the initial timeout since we've received data if (initialTimeout) { clearTimeout(initialTimeout); initialTimeout = null; } - + // Mark that we've received initial data initialDataReceived = true; connectionRecord.hasReceivedInitialData = true; - + // Check if this looks like a TLS handshake if (initialChunk && isTlsHandshake(initialChunk)) { connectionRecord.isTLS = true; - + if (this.settings.enableTlsDebugLogging) { - console.log(`[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`); + console.log( + `[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes` + ); } } - + // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup. const domainConfig = forcedDomain ? forcedDomain - : (serverName ? this.settings.domainConfigs.find(config => - config.domains.some(d => plugins.minimatch(serverName, d)) - ) : undefined); + : serverName + ? this.settings.domainConfigs.find((config) => + config.domains.some((d) => plugins.minimatch(serverName, d)) + ) + : undefined; // Save domain config in connection record connectionRecord.domainConfig = domainConfig; - + // IP validation is skipped if allowedIPs is empty if (domainConfig) { const effectiveAllowedIPs: string[] = [ ...domainConfig.allowedIPs, - ...(this.settings.defaultAllowedIPs || []) + ...(this.settings.defaultAllowedIPs || []), ]; const effectiveBlockedIPs: string[] = [ ...(domainConfig.blockedIPs || []), - ...(this.settings.defaultBlockedIPs || []) + ...(this.settings.defaultBlockedIPs || []), ]; - + // Skip IP validation if allowedIPs is empty - if (domainConfig.allowedIPs.length > 0 && !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) { - return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`); + if ( + domainConfig.allowedIPs.length > 0 && + !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs) + ) { + return rejectIncomingConnection( + 'rejected', + `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join( + ', ' + )}` + ); } } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) { - if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) { - return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`); + if ( + !isGlobIPAllowed( + remoteIP, + this.settings.defaultAllowedIPs, + this.settings.defaultBlockedIPs || [] + ) + ) { + return rejectIncomingConnection( + 'rejected', + `Connection rejected: IP ${remoteIP} not allowed by default allowed list` + ); } } @@ -822,25 +903,29 @@ export class PortProxy { const tempDataHandler = (chunk: Buffer) => { // Track bytes received connectionRecord.bytesReceived += chunk.length; - + // Check for TLS handshake if (!connectionRecord.isTLS && isTlsHandshake(chunk)) { connectionRecord.isTLS = true; - + if (this.settings.enableTlsDebugLogging) { - console.log(`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`); + console.log( + `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes` + ); } } - + // Check if adding this chunk would exceed the buffer limit const newSize = connectionRecord.pendingDataSize + chunk.length; - + if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) { - console.log(`[${connectionId}] Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`); + console.log( + `[${connectionId}] Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes` + ); socket.end(); // Gracefully close the socket return initiateCleanupOnce('buffer_limit_exceeded'); } - + // Buffer the chunk and update the size counter connectionRecord.pendingData.push(Buffer.from(chunk)); connectionRecord.pendingDataSize = newSize; @@ -861,13 +946,13 @@ export class PortProxy { const targetSocket = plugins.net.connect(connectionOptions); connectionRecord.outgoing = targetSocket; connectionRecord.outgoingStartTime = Date.now(); - + // Apply socket optimizations targetSocket.setNoDelay(this.settings.noDelay); if (this.settings.keepAlive) { targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay); } - + // Apply enhanced TCP options if available if (this.settings.enableKeepAliveProbes) { try { @@ -881,57 +966,73 @@ export class PortProxy { // Ignore errors - these are optional enhancements } } - + // Setup specific error handler for connection phase targetSocket.once('error', (err) => { // This handler runs only once during the initial connection phase const code = (err as any).code; - console.log(`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`); - + console.log( + `[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})` + ); + // Resume the incoming socket to prevent it from hanging socket.resume(); - + if (code === 'ECONNREFUSED') { - console.log(`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`); + console.log( + `[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection` + ); } else if (code === 'ETIMEDOUT') { - console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`); + console.log( + `[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out` + ); } else if (code === 'ECONNRESET') { - console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`); + console.log( + `[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset` + ); } else if (code === 'EHOSTUNREACH') { console.log(`[${connectionId}] Host ${targetHost} is unreachable`); } - + // Clear any existing error handler after connection phase targetSocket.removeAllListeners('error'); - + // Re-add the normal error handler for established connections targetSocket.on('error', handleError('outgoing')); - + if (outgoingTerminationReason === null) { outgoingTerminationReason = 'connection_failed'; this.incrementTerminationStat('outgoing', 'connection_failed'); } - + // Clean up the connection initiateCleanupOnce(`connection_failed_${code}`); }); - + // Setup close handler targetSocket.on('close', handleClose('outgoing')); socket.on('close', handleClose('incoming')); - + // Handle timeouts socket.on('timeout', () => { - console.log(`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`); + console.log( + `[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs( + this.settings.socketTimeout || 3600000 + )}` + ); if (incomingTerminationReason === null) { incomingTerminationReason = 'timeout'; this.incrementTerminationStat('incoming', 'timeout'); } initiateCleanupOnce('timeout_incoming'); }); - + targetSocket.on('timeout', () => { - console.log(`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`); + console.log( + `[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs( + this.settings.socketTimeout || 3600000 + )}` + ); if (outgoingTerminationReason === null) { outgoingTerminationReason = 'timeout'; this.incrementTerminationStat('outgoing', 'timeout'); @@ -942,48 +1043,62 @@ export class PortProxy { // Set appropriate timeouts using the configured value socket.setTimeout(this.settings.socketTimeout || 3600000); targetSocket.setTimeout(this.settings.socketTimeout || 3600000); - + // Track outgoing data for bytes counting targetSocket.on('data', (chunk: Buffer) => { connectionRecord.bytesSent += chunk.length; this.updateActivity(connectionRecord); }); - + // Wait for the outgoing connection to be ready before setting up piping targetSocket.once('connect', () => { // Clear the initial connection error handler targetSocket.removeAllListeners('error'); - + // Add the normal error handler for established connections targetSocket.on('error', handleError('outgoing')); - + // Remove temporary data handler socket.removeListener('data', tempDataHandler); - + // Flush all pending data to target if (connectionRecord.pendingData.length > 0) { const combinedData = Buffer.concat(connectionRecord.pendingData); targetSocket.write(combinedData, (err) => { if (err) { - console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`); + console.log( + `[${connectionId}] Error writing pending data to target: ${err.message}` + ); return initiateCleanupOnce('write_error'); } - + // Now set up piping for future data and resume the socket socket.pipe(targetSocket); targetSocket.pipe(socket); socket.resume(); // Resume the socket after piping is established - + if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` + - `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` + - ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}` + `${ + serverName + ? ` (SNI: ${serverName})` + : forcedDomain + ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` + : '' + }` + + ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}` ); } else { console.log( `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` + - `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` + `${ + serverName + ? ` (SNI: ${serverName})` + : forcedDomain + ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` + : '' + }` ); } }); @@ -992,25 +1107,37 @@ export class PortProxy { socket.pipe(targetSocket); targetSocket.pipe(socket); socket.resume(); // Resume the socket after piping is established - + if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` + - `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` + - ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}` + `${ + serverName + ? ` (SNI: ${serverName})` + : forcedDomain + ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` + : '' + }` + + ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}` ); } else { console.log( `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` + - `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` + `${ + serverName + ? ` (SNI: ${serverName})` + : forcedDomain + ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` + : '' + }` ); } } - + // Clear the buffer now that we've processed it connectionRecord.pendingData = []; connectionRecord.pendingDataSize = 0; - + // Add the renegotiation listener for SNI validation if (serverName) { socket.on('data', (renegChunk: Buffer) => { @@ -1019,41 +1146,53 @@ export class PortProxy { // Try to extract SNI from potential renegotiation const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging); if (newSNI && newSNI !== connectionRecord.lockedDomain) { - console.log(`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`); + console.log( + `[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.` + ); initiateCleanupOnce('sni_mismatch'); } else if (newSNI && this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`); + console.log( + `[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.` + ); } } catch (err) { - console.log(`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`); + console.log( + `[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.` + ); } } }); } - + // Set connection timeout if (connectionRecord.cleanupTimer) { clearTimeout(connectionRecord.cleanupTimer); } - + // Set timeout based on domain config or default const connectionTimeout = this.getConnectionTimeout(connectionRecord); connectionRecord.cleanupTimer = setTimeout(() => { - console.log(`[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`); + console.log( + `[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs( + connectionTimeout + )}), forcing cleanup.` + ); initiateCleanupOnce('connection_timeout'); }, connectionTimeout); - + // Make sure timeout doesn't keep the process alive if (connectionRecord.cleanupTimer.unref) { connectionRecord.cleanupTimer.unref(); } - + // Mark TLS handshake as complete for TLS connections if (connectionRecord.isTLS) { connectionRecord.tlsHandshakeComplete = true; - + if (this.settings.enableTlsDebugLogging) { - console.log(`[${connectionId}] TLS handshake complete for connection from ${remoteIP}`); + console.log( + `[${connectionId}] TLS handshake complete for connection from ${remoteIP}` + ); } } }); @@ -1061,45 +1200,72 @@ export class PortProxy { // --- PORT RANGE-BASED HANDLING --- // Only apply port-based rules if the incoming port is within one of the global port ranges. - if (this.settings.globalPortRanges && isPortInRanges(localPort, this.settings.globalPortRanges)) { + if ( + this.settings.globalPortRanges && + isPortInRanges(localPort, this.settings.globalPortRanges) + ) { if (this.settings.forwardAllGlobalRanges) { - if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) { - console.log(`[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`); + if ( + this.settings.defaultAllowedIPs && + this.settings.defaultAllowedIPs.length > 0 && + !isAllowed(remoteIP, this.settings.defaultAllowedIPs) + ) { + console.log( + `[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.` + ); socket.end(); return; } if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`); + console.log( + `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.` + ); } - setupConnection('', undefined, { - domains: ['global'], - allowedIPs: this.settings.defaultAllowedIPs || [], - blockedIPs: this.settings.defaultBlockedIPs || [], - targetIPs: [this.settings.targetIP!], - portRanges: [] - }, localPort); + setupConnection( + '', + undefined, + { + domains: ['global'], + allowedIPs: this.settings.defaultAllowedIPs || [], + blockedIPs: this.settings.defaultBlockedIPs || [], + targetIPs: [this.settings.targetIP!], + portRanges: [], + }, + localPort + ); return; } else { // Attempt to find a matching forced domain config based on the local port. const forcedDomain = this.settings.domainConfigs.find( - domain => domain.portRanges && domain.portRanges.length > 0 && isPortInRanges(localPort, domain.portRanges) + (domain) => + domain.portRanges && + domain.portRanges.length > 0 && + isPortInRanges(localPort, domain.portRanges) ); if (forcedDomain) { const effectiveAllowedIPs: string[] = [ ...forcedDomain.allowedIPs, - ...(this.settings.defaultAllowedIPs || []) + ...(this.settings.defaultAllowedIPs || []), ]; const effectiveBlockedIPs: string[] = [ ...(forcedDomain.blockedIPs || []), - ...(this.settings.defaultBlockedIPs || []) + ...(this.settings.defaultBlockedIPs || []), ]; if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) { - console.log(`[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`); + console.log( + `[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join( + ', ' + )} on port ${localPort}.` + ); socket.end(); return; } if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`); + console.log( + `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join( + ', ' + )}.` + ); } setupConnection('', undefined, forcedDomain, localPort); return; @@ -1117,39 +1283,52 @@ export class PortProxy { clearTimeout(initialTimeout); initialTimeout = null; } - + initialDataReceived = true; - + // Try to extract SNI let serverName = ''; - + if (isTlsHandshake(chunk)) { connectionRecord.isTLS = true; - + if (this.settings.enableTlsDebugLogging) { - console.log(`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`); + console.log( + `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes` + ); } - + serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || ''; } - + // Lock the connection to the negotiated SNI. connectionRecord.lockedDomain = serverName; - + if (this.settings.enableDetailedLogging) { - console.log(`[${connectionId}] Received connection from ${remoteIP} with SNI: ${serverName || '(empty)'}`); + console.log( + `[${connectionId}] Received connection from ${remoteIP} with SNI: ${ + serverName || '(empty)' + }` + ); } - + setupConnection(serverName, chunk); }); } else { initialDataReceived = true; connectionRecord.hasReceivedInitialData = true; - - if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) { - return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`); + + if ( + this.settings.defaultAllowedIPs && + this.settings.defaultAllowedIPs.length > 0 && + !isAllowed(remoteIP, this.settings.defaultAllowedIPs) + ) { + return rejectIncomingConnection( + 'rejected', + `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection` + ); } - + setupConnection(''); } }; @@ -1172,13 +1351,15 @@ export class PortProxy { // Create a server for each port. for (const port of listeningPorts) { - const server = plugins.net - .createServer(connectionHandler) - .on('error', (err: Error) => { - console.log(`Server Error on port ${port}: ${err.message}`); - }); + const server = plugins.net.createServer(connectionHandler).on('error', (err: Error) => { + console.log(`Server Error on port ${port}: ${err.message}`); + }); server.listen(port, () => { - console.log(`PortProxy -> OK: Now listening on port ${port}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`); + console.log( + `PortProxy -> OK: Now listening on port ${port}${ + this.settings.sniEnabled ? ' (SNI passthrough enabled)' : '' + }` + ); }); this.netServers.push(server); } @@ -1187,7 +1368,7 @@ export class PortProxy { this.connectionLogger = setInterval(() => { // Immediately return if shutting down if (this.isShuttingDown) return; - + const now = Date.now(); let maxIncoming = 0; let maxOutgoing = 0; @@ -1195,14 +1376,14 @@ export class PortProxy { let nonTlsConnections = 0; let completedTlsHandshakes = 0; let pendingTlsHandshakes = 0; - + // Create a copy of the keys to avoid modification during iteration const connectionIds = [...this.connectionRecords.keys()]; - + for (const id of connectionIds) { const record = this.connectionRecords.get(id); if (!record) continue; - + // Track connection stats if (record.isTLS) { tlsConnections++; @@ -1214,50 +1395,73 @@ export class PortProxy { } else { nonTlsConnections++; } - + maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); if (record.outgoingStartTime) { maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime); } - + // Parity check: if outgoing socket closed and incoming remains active - if (record.outgoingClosedTime && - !record.incoming.destroyed && - !record.connectionClosed && - (now - record.outgoingClosedTime > 120000)) { + if ( + record.outgoingClosedTime && + !record.incoming.destroyed && + !record.connectionClosed && + now - record.outgoingClosedTime > 120000 + ) { const remoteIP = record.remoteIP; - console.log(`[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing closed.`); + console.log( + `[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs( + now - record.outgoingClosedTime + )} after outgoing closed.` + ); this.cleanupConnection(record, 'parity_check'); } - + // Check for stalled connections waiting for initial data - if (!record.hasReceivedInitialData && - (now - record.incomingStartTime > this.settings.initialDataTimeout! / 2)) { - console.log(`[${id}] Warning: Connection from ${record.remoteIP} has not received initial data after ${plugins.prettyMs(now - record.incomingStartTime)}`); + if ( + !record.hasReceivedInitialData && + now - record.incomingStartTime > this.settings.initialDataTimeout! / 2 + ) { + console.log( + `[${id}] Warning: Connection from ${ + record.remoteIP + } has not received initial data after ${plugins.prettyMs( + now - record.incomingStartTime + )}` + ); } - + // Skip inactivity check if disabled if (!this.settings.disableInactivityCheck) { // Inactivity check with configurable timeout const inactivityThreshold = this.settings.inactivityTimeout!; - + const inactivityTime = now - record.lastActivity; if (inactivityTime > inactivityThreshold && !record.connectionClosed) { - console.log(`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} for ${plugins.prettyMs(inactivityTime)}.`); + console.log( + `[${id}] Inactivity check: No activity on connection from ${ + record.remoteIP + } for ${plugins.prettyMs(inactivityTime)}.` + ); this.cleanupConnection(record, 'inactivity'); } } } - + // Log detailed stats periodically console.log( `Active connections: ${this.connectionRecords.size}. ` + - `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), Non-TLS=${nonTlsConnections}. ` + - `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` + - `Termination stats: ${JSON.stringify({IN: this.terminationStats.incoming, OUT: this.terminationStats.outgoing})}` + `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), Non-TLS=${nonTlsConnections}. ` + + `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs( + maxOutgoing + )}. ` + + `Termination stats: ${JSON.stringify({ + IN: this.terminationStats.incoming, + OUT: this.terminationStats.outgoing, + })}` ); }, this.settings.inactivityCheckInterval || 60000); - + // Make sure the interval doesn't keep the process alive if (this.connectionLogger.unref) { this.connectionLogger.unref(); @@ -1268,12 +1472,12 @@ export class PortProxy { * Gracefully shut down the proxy */ public async stop() { - console.log("PortProxy shutting down..."); + console.log('PortProxy shutting down...'); this.isShuttingDown = true; - + // Stop accepting new connections const closeServerPromises: Promise[] = this.netServers.map( - server => + (server) => new Promise((resolve) => { if (!server.listening) { resolve(); @@ -1287,7 +1491,7 @@ export class PortProxy { }); }) ); - + // Stop the connection logger if (this.connectionLogger) { clearInterval(this.connectionLogger); @@ -1296,12 +1500,12 @@ export class PortProxy { // Wait for servers to close await Promise.all(closeServerPromises); - console.log("All servers closed. Cleaning up active connections..."); - + console.log('All servers closed. Cleaning up active connections...'); + // Force destroy all active connections immediately const connectionIds = [...this.connectionRecords.keys()]; console.log(`Cleaning up ${connectionIds.length} active connections...`); - + // First pass: End all connections gracefully for (const id of connectionIds) { const record = this.connectionRecords.get(id); @@ -1312,12 +1516,12 @@ export class PortProxy { clearTimeout(record.cleanupTimer); record.cleanupTimer = undefined; } - + // End sockets gracefully if (record.incoming && !record.incoming.destroyed) { record.incoming.end(); } - + if (record.outgoing && !record.outgoing.destroyed) { record.outgoing.end(); } @@ -1326,10 +1530,10 @@ export class PortProxy { } } } - + // Short delay to allow graceful ends to process - await new Promise(resolve => setTimeout(resolve, 100)); - + await new Promise((resolve) => setTimeout(resolve, 100)); + // Second pass: Force destroy everything for (const id of connectionIds) { const record = this.connectionRecords.get(id); @@ -1342,7 +1546,7 @@ export class PortProxy { record.incoming.destroy(); } } - + if (record.outgoing) { record.outgoing.removeAllListeners(); if (!record.outgoing.destroyed) { @@ -1354,20 +1558,20 @@ export class PortProxy { } } } - + // Clear all tracking maps this.connectionRecords.clear(); this.domainTargetIndices.clear(); this.connectionsByIP.clear(); this.connectionRateByIP.clear(); this.netServers = []; - + // Reset termination stats this.terminationStats = { incoming: {}, - outgoing: {} + outgoing: {}, }; - - console.log("PortProxy shutdown complete."); + + console.log('PortProxy shutdown complete.'); } -} \ No newline at end of file +}