2025-02-21 23:11:13 +00:00
import * as plugins from './plugins.js' ;
2025-02-21 15:14:02 +00:00
2025-02-27 21:25:03 +00:00
/** Domain configuration with per-domain allowed port ranges */
2025-02-21 20:17:35 +00:00
export interface IDomainConfig {
2025-02-27 21:19:34 +00:00
domains : string [ ] ; // Glob patterns for domain(s)
allowedIPs : string [ ] ; // Glob patterns for allowed IPs
2025-03-01 13:17:05 +00:00
blockedIPs? : string [ ] ; // Glob patterns for blocked IPs
2025-02-27 21:19:34 +00:00
targetIPs? : string [ ] ; // If multiple targetIPs are given, use round robin.
portRanges? : Array < { from : number ; to : number } > ; // Optional port ranges
2025-03-05 17:46:25 +00:00
// Protocol-specific timeout overrides
httpTimeout? : number ; // HTTP connection timeout override (ms)
wsTimeout? : number ; // WebSocket connection timeout override (ms)
2025-02-21 15:14:02 +00:00
}
2025-03-05 17:46:25 +00:00
/** Connection protocol types for timeout management */
export type ProtocolType = 'http' | 'websocket' | 'https' | 'tls' | 'unknown' ;
2025-02-27 12:25:48 +00:00
/** Port proxy settings including global allowed port ranges */
2025-02-24 23:27:48 +00:00
export interface IPortProxySettings extends plugins . tls . TlsOptions {
2025-02-21 17:01:02 +00:00
fromPort : number ;
toPort : number ;
2025-02-27 12:41:20 +00:00
targetIP? : string ; // Global target host to proxy to, defaults to 'localhost'
2025-02-27 21:19:34 +00:00
domainConfigs : IDomainConfig [ ] ;
2025-02-21 15:14:02 +00:00
sniEnabled? : boolean ;
2025-02-23 17:30:41 +00:00
defaultAllowedIPs? : string [ ] ;
2025-03-01 13:17:05 +00:00
defaultBlockedIPs? : string [ ] ;
2025-02-23 17:30:41 +00:00
preserveSourceIP? : boolean ;
2025-03-05 17:46:25 +00:00
// Updated timeout settings with better defaults
initialDataTimeout? : number ; // Timeout for initial data/SNI (ms), default: 15000 (15s)
socketTimeout? : number ; // Socket inactivity timeout (ms), default: 300000 (5m)
inactivityCheckInterval? : number ; // How often to check for inactive connections (ms), default: 30000 (30s)
// Protocol-specific timeouts
maxConnectionLifetime? : number ; // Default max connection lifetime (ms), default: 3600000 (1h)
httpConnectionTimeout? : number ; // HTTP specific timeout (ms), default: 1800000 (30m)
wsConnectionTimeout? : number ; // WebSocket specific timeout (ms), default: 14400000 (4h)
httpKeepAliveTimeout? : number ; // HTTP keep-alive header timeout (ms), default: 1200000 (20m)
gracefulShutdownTimeout? : number ; // (ms) maximum time to wait for connections to close during shutdown
2025-02-27 12:25:48 +00:00
globalPortRanges : Array < { from : number ; to : number } > ; // Global allowed port ranges
2025-03-05 17:46:25 +00:00
forwardAllGlobalRanges? : boolean ; // When true, forwards all connections on global port ranges to the global targetIP
2025-03-05 17:06:51 +00:00
// Socket optimization settings
2025-03-05 17:46:25 +00:00
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
// Enable enhanced features
disableInactivityCheck? : boolean ; // Disable inactivity checking entirely
enableKeepAliveProbes? : boolean ; // Enable TCP keep-alive probes
enableProtocolDetection? : boolean ; // Enable HTTP/WebSocket protocol detection
enableDetailedLogging? : boolean ; // Enable detailed connection logging
// Rate limiting and security
maxConnectionsPerIP? : number ; // Maximum simultaneous connections from a single IP
connectionRateLimitPerMinute? : number ; // Max new connections per minute from a single IP
}
/ * *
* Enhanced connection record with protocol - specific handling
* /
interface IConnectionRecord {
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
// Enhanced tracking fields
protocolType : ProtocolType ; // Connection protocol type
isPooledConnection : boolean ; // Whether this is likely a browser pooled connection
lastHttpRequest? : number ; // Timestamp of last HTTP request (for keep-alive tracking)
httpKeepAliveTimeout? : number ; // HTTP keep-alive timeout from headers
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)
httpRequests : number ; // Count of HTTP requests on this connection
2025-02-21 20:17:35 +00:00
}
/ * *
2025-02-23 17:30:41 +00:00
* Extracts the SNI ( Server Name Indication ) from a TLS ClientHello packet .
* @param buffer - Buffer containing the TLS ClientHello .
* @returns The server name if found , otherwise undefined .
2025-02-21 20:17:35 +00:00
* /
function extractSNI ( buffer : Buffer ) : string | undefined {
let offset = 0 ;
2025-02-23 17:30:41 +00:00
if ( buffer . length < 5 ) return undefined ;
2025-02-21 20:17:35 +00:00
const recordType = buffer . readUInt8 ( 0 ) ;
2025-02-23 17:30:41 +00:00
if ( recordType !== 22 ) return undefined ; // 22 = handshake
2025-02-21 20:17:35 +00:00
const recordLength = buffer . readUInt16BE ( 3 ) ;
2025-02-23 17:30:41 +00:00
if ( buffer . length < 5 + recordLength ) return undefined ;
2025-02-21 20:17:35 +00:00
offset = 5 ;
const handshakeType = buffer . readUInt8 ( offset ) ;
2025-02-23 17:30:41 +00:00
if ( handshakeType !== 1 ) return undefined ; // 1 = ClientHello
2025-02-21 20:17:35 +00:00
2025-02-23 17:30:41 +00:00
offset += 4 ; // Skip handshake header (type + length)
offset += 2 + 32 ; // Skip client version and random
2025-02-21 20:17:35 +00:00
const sessionIDLength = buffer . readUInt8 ( offset ) ;
2025-02-23 17:30:41 +00:00
offset += 1 + sessionIDLength ; // Skip session ID
2025-02-21 20:17:35 +00:00
const cipherSuitesLength = buffer . readUInt16BE ( offset ) ;
2025-02-23 17:30:41 +00:00
offset += 2 + cipherSuitesLength ; // Skip cipher suites
2025-02-21 20:17:35 +00:00
const compressionMethodsLength = buffer . readUInt8 ( offset ) ;
2025-02-23 17:30:41 +00:00
offset += 1 + compressionMethodsLength ; // Skip compression methods
2025-02-21 20:17:35 +00:00
2025-02-23 17:30:41 +00:00
if ( offset + 2 > buffer . length ) return undefined ;
2025-02-21 20:17:35 +00:00
const extensionsLength = buffer . readUInt16BE ( offset ) ;
offset += 2 ;
const extensionsEnd = offset + extensionsLength ;
while ( offset + 4 <= extensionsEnd ) {
const extensionType = buffer . readUInt16BE ( offset ) ;
const extensionLength = buffer . readUInt16BE ( offset + 2 ) ;
offset += 4 ;
2025-02-23 17:30:41 +00:00
if ( extensionType === 0x0000 ) { // SNI extension
if ( offset + 2 > buffer . length ) return undefined ;
2025-02-21 20:17:35 +00:00
const sniListLength = buffer . readUInt16BE ( offset ) ;
offset += 2 ;
const sniListEnd = offset + sniListLength ;
while ( offset + 3 < sniListEnd ) {
2025-02-23 17:30:41 +00:00
const nameType = buffer . readUInt8 ( offset ++ ) ;
2025-02-21 20:17:35 +00:00
const nameLen = buffer . readUInt16BE ( offset ) ;
offset += 2 ;
if ( nameType === 0 ) { // host_name
2025-02-23 17:30:41 +00:00
if ( offset + nameLen > buffer . length ) return undefined ;
return buffer . toString ( 'utf8' , offset , offset + nameLen ) ;
2025-02-21 20:17:35 +00:00
}
offset += nameLen ;
}
break ;
} else {
offset += extensionLength ;
}
}
return undefined ;
2025-02-21 15:14:02 +00:00
}
2019-08-22 15:09:48 +02:00
2025-03-03 02:14:21 +00:00
// Helper: Check if a port falls within any of the given port ranges
2025-03-01 13:17:05 +00:00
const isPortInRanges = ( port : number , ranges : Array < { from : number ; to : number } > ) : boolean = > {
return ranges . some ( range = > port >= range . from && port <= range . to ) ;
} ;
2025-03-03 02:14:21 +00:00
// Helper: Check if a given IP matches any of the glob patterns
2025-03-01 13:17:05 +00:00
const isAllowed = ( ip : string , patterns : string [ ] ) : boolean = > {
const normalizeIP = ( ip : string ) : string [ ] = > {
if ( ip . startsWith ( '::ffff:' ) ) {
const ipv4 = ip . slice ( 7 ) ;
return [ ip , ipv4 ] ;
}
if ( /^\d{1,3}(\.\d{1,3}){3}$/ . test ( ip ) ) {
return [ ip , ` ::ffff: ${ ip } ` ] ;
}
return [ ip ] ;
} ;
const normalizedIPVariants = normalizeIP ( ip ) ;
const expandedPatterns = patterns . flatMap ( normalizeIP ) ;
return normalizedIPVariants . some ( ipVariant = >
expandedPatterns . some ( pattern = > plugins . minimatch ( ipVariant , pattern ) )
) ;
} ;
2025-03-03 02:14:21 +00:00
// Helper: Check if an IP is allowed considering allowed and blocked glob patterns
2025-03-01 13:17:05 +00:00
const isGlobIPAllowed = ( ip : string , allowed : string [ ] , blocked : string [ ] = [ ] ) : boolean = > {
if ( blocked . length > 0 && isAllowed ( ip , blocked ) ) return false ;
return isAllowed ( ip , allowed ) ;
} ;
2025-03-03 02:14:21 +00:00
// Helper: Generate a unique connection ID
2025-03-03 01:42:16 +00:00
const generateConnectionId = ( ) : string = > {
return Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) + Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ;
} ;
2025-03-05 17:46:25 +00:00
// Protocol detection helpers
const isHttpRequest = ( buffer : Buffer ) : boolean = > {
if ( buffer . length < 4 ) return false ;
const start = buffer . toString ( 'ascii' , 0 , 4 ) . toUpperCase ( ) ;
return (
start . startsWith ( 'GET ' ) ||
start . startsWith ( 'POST' ) ||
start . startsWith ( 'PUT ' ) ||
start . startsWith ( 'HEAD' ) ||
start . startsWith ( 'DELE' ) ||
start . startsWith ( 'PATC' ) ||
start . startsWith ( 'OPTI' )
) ;
} ;
const isWebSocketUpgrade = ( buffer : Buffer ) : boolean = > {
if ( buffer . length < 20 ) return false ;
const data = buffer . toString ( 'ascii' , 0 , Math . min ( buffer . length , 200 ) ) ;
return (
data . includes ( 'Upgrade: websocket' ) ||
data . includes ( 'Upgrade: WebSocket' )
) ;
} ;
const isTlsHandshake = ( buffer : Buffer ) : boolean = > {
return buffer . length > 0 && buffer [ 0 ] === 22 ; // ContentType.handshake
} ;
2022-07-29 00:49:46 +02:00
export class PortProxy {
2025-02-27 13:04:01 +00:00
private netServers : plugins.net.Server [ ] = [ ] ;
2025-02-24 23:27:48 +00:00
settings : IPortProxySettings ;
2025-03-03 01:42:16 +00:00
private connectionRecords : Map < string , IConnectionRecord > = new Map ( ) ;
2025-02-21 23:05:17 +00:00
private connectionLogger : NodeJS.Timeout | null = null ;
2025-03-03 01:42:16 +00:00
private isShuttingDown : boolean = false ;
2022-07-29 00:49:46 +02:00
2025-03-03 02:14:21 +00:00
// Map to track round robin indices for each domain config
2025-02-27 21:19:34 +00:00
private domainTargetIndices : Map < IDomainConfig , number > = new Map ( ) ;
2025-03-05 17:46:25 +00:00
// Enhanced stats tracking
2025-02-22 05:46:30 +00:00
private terminationStats : {
incoming : Record < string , number > ;
outgoing : Record < string , number > ;
} = {
incoming : { } ,
outgoing : { } ,
} ;
2025-03-05 17:46:25 +00:00
// Connection tracking by IP for rate limiting
private connectionsByIP : Map < string , Set < string > > = new Map ( ) ;
private connectionRateByIP : Map < string , number [ ] > = new Map ( ) ;
2025-02-22 05:46:30 +00:00
2025-02-26 10:29:21 +00:00
constructor ( settingsArg : IPortProxySettings ) {
2025-03-05 17:46:25 +00:00
// Set reasonable defaults for all settings
2025-02-21 17:01:02 +00:00
this . settings = {
2025-02-26 10:29:21 +00:00
. . . settingsArg ,
2025-02-27 12:41:20 +00:00
targetIP : settingsArg.targetIP || 'localhost' ,
2025-03-05 17:46:25 +00:00
// Timeout settings with browser-friendly defaults
2025-03-05 18:24:28 +00:00
initialDataTimeout : settingsArg.initialDataTimeout || 30000 , // 30 seconds
2025-03-05 17:46:25 +00:00
socketTimeout : settingsArg.socketTimeout || 300000 , // 5 minutes
inactivityCheckInterval : settingsArg.inactivityCheckInterval || 30000 , // 30 seconds
// Protocol-specific timeouts
maxConnectionLifetime : settingsArg.maxConnectionLifetime || 3600000 , // 1 hour default
httpConnectionTimeout : settingsArg.httpConnectionTimeout || 1800000 , // 30 minutes
wsConnectionTimeout : settingsArg.wsConnectionTimeout || 14400000 , // 4 hours
httpKeepAliveTimeout : settingsArg.httpKeepAliveTimeout || 1200000 , // 20 minutes
gracefulShutdownTimeout : settingsArg.gracefulShutdownTimeout || 30000 , // 30 seconds
// Socket optimization settings
2025-03-05 17:06:51 +00:00
noDelay : settingsArg.noDelay !== undefined ? settingsArg.noDelay : true ,
keepAlive : settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true ,
2025-03-05 18:24:28 +00:00
keepAliveInitialDelay : settingsArg.keepAliveInitialDelay || 30000 , // 30 seconds
2025-03-05 17:06:51 +00:00
maxPendingDataSize : settingsArg.maxPendingDataSize || 1024 * 1024 , // 1MB
2025-03-05 17:46:25 +00:00
// Feature flags
disableInactivityCheck : settingsArg.disableInactivityCheck || false ,
enableKeepAliveProbes : settingsArg.enableKeepAliveProbes || false ,
enableProtocolDetection : settingsArg.enableProtocolDetection !== undefined ? settingsArg.enableProtocolDetection : true ,
enableDetailedLogging : settingsArg.enableDetailedLogging || false ,
// Rate limiting defaults
maxConnectionsPerIP : settingsArg.maxConnectionsPerIP || 100 , // 100 connections per IP
connectionRateLimitPerMinute : settingsArg.connectionRateLimitPerMinute || 300 , // 300 per minute
2025-02-21 17:01:02 +00:00
} ;
2022-07-29 00:49:46 +02:00
}
2025-03-05 17:46:25 +00:00
/ * *
* Get connections count by IP
* /
private getConnectionCountByIP ( ip : string ) : number {
return this . connectionsByIP . get ( ip ) ? . size || 0 ;
}
/ * *
* Check and update connection rate for an IP
* /
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 ) ;
timestamps . push ( now ) ;
this . connectionRateByIP . set ( ip , timestamps ) ;
// Check if rate exceeds limit
return timestamps . length <= this . settings . connectionRateLimitPerMinute ! ;
}
/ * *
* Track connection by IP
* /
private trackConnectionByIP ( ip : string , connectionId : string ) : void {
if ( ! this . connectionsByIP . has ( ip ) ) {
this . connectionsByIP . set ( ip , new Set ( ) ) ;
}
this . connectionsByIP . get ( ip ) ! . add ( connectionId ) ;
}
/ * *
* Remove connection tracking for an IP
* /
private removeConnectionByIP ( ip : string , connectionId : string ) : void {
if ( this . connectionsByIP . has ( ip ) ) {
const connections = this . connectionsByIP . get ( ip ) ! ;
connections . delete ( connectionId ) ;
if ( connections . size === 0 ) {
this . connectionsByIP . delete ( ip ) ;
}
}
}
/ * *
* Track connection termination statistic
* /
2025-02-22 05:46:30 +00:00
private incrementTerminationStat ( side : 'incoming' | 'outgoing' , reason : string ) : void {
2025-02-23 17:30:41 +00:00
this . terminationStats [ side ] [ reason ] = ( this . terminationStats [ side ] [ reason ] || 0 ) + 1 ;
2025-02-22 05:46:30 +00:00
}
2025-03-05 17:46:25 +00:00
/ * *
* Get protocol - specific timeout based on connection type
* /
private getProtocolTimeout ( record : IConnectionRecord , domainConfig? : IDomainConfig ) : number {
// If the protocol has a domain-specific timeout, use that
if ( domainConfig ) {
if ( record . protocolType === 'http' && domainConfig . httpTimeout ) {
return domainConfig . httpTimeout ;
}
if ( record . protocolType === 'websocket' && domainConfig . wsTimeout ) {
return domainConfig . wsTimeout ;
}
}
// Use HTTP keep-alive timeout from headers if available
if ( record . httpKeepAliveTimeout ) {
return record . httpKeepAliveTimeout ;
}
// Otherwise use default protocol-specific timeout
switch ( record . protocolType ) {
case 'http' :
return this . settings . httpConnectionTimeout ! ;
case 'websocket' :
return this . settings . wsConnectionTimeout ! ;
case 'https' :
case 'tls' :
return this . settings . httpConnectionTimeout ! ; // Use HTTP timeout for HTTPS by default
default :
return this . settings . maxConnectionLifetime ! ;
}
}
/ * *
* Detect protocol and update connection record
* /
private detectProtocol ( data : Buffer , record : IConnectionRecord ) : void {
if ( ! this . settings . enableProtocolDetection || record . protocolType !== 'unknown' ) {
return ;
}
try {
// Detect TLS/HTTPS
if ( isTlsHandshake ( data ) ) {
record . protocolType = 'tls' ;
if ( this . settings . enableDetailedLogging ) {
console . log ( ` [ ${ record . id } ] Protocol detected: TLS ` ) ;
}
return ;
}
// Detect HTTP including WebSocket upgrades
if ( isHttpRequest ( data ) ) {
record . httpRequests ++ ;
record . lastHttpRequest = Date . now ( ) ;
// Check for WebSocket upgrade
if ( isWebSocketUpgrade ( data ) ) {
record . protocolType = 'websocket' ;
if ( this . settings . enableDetailedLogging ) {
console . log ( ` [ ${ record . id } ] Protocol detected: WebSocket Upgrade ` ) ;
}
} else {
record . protocolType = 'http' ;
// Parse HTTP keep-alive headers
this . parseHttpHeaders ( data , record ) ;
if ( this . settings . enableDetailedLogging ) {
console . log ( ` [ ${ record . id } ] Protocol detected: HTTP ${ record . isPooledConnection ? ' (KeepAlive)' : '' } ` ) ;
}
}
}
} catch ( err ) {
console . log ( ` [ ${ record . id } ] Error detecting protocol: ${ err } ` ) ;
}
}
/ * *
* Parse HTTP headers for keep - alive and other connection info
* /
private parseHttpHeaders ( data : Buffer , record : IConnectionRecord ) : void {
try {
const headerStr = data . toString ( 'utf8' , 0 , Math . min ( data . length , 1024 ) ) ;
// Check for HTTP keep-alive
const connectionHeader = headerStr . match ( /\r\nConnection:\s*([^\r\n]+)/i ) ;
if ( connectionHeader && connectionHeader [ 1 ] . toLowerCase ( ) . includes ( 'keep-alive' ) ) {
record . isPooledConnection = true ;
// Check for Keep-Alive timeout value
const keepAliveHeader = headerStr . match ( /\r\nKeep-Alive:\s*([^\r\n]+)/i ) ;
if ( keepAliveHeader ) {
const timeoutMatch = keepAliveHeader [ 1 ] . match ( /timeout=(\d+)/i ) ;
if ( timeoutMatch && timeoutMatch [ 1 ] ) {
const timeoutSec = parseInt ( timeoutMatch [ 1 ] , 10 ) ;
if ( ! isNaN ( timeoutSec ) && timeoutSec > 0 ) {
// Convert seconds to milliseconds and add some buffer
record . httpKeepAliveTimeout = ( timeoutSec * 1000 ) + 5000 ;
if ( this . settings . enableDetailedLogging ) {
console . log ( ` [ ${ record . id } ] HTTP Keep-Alive timeout set to ${ timeoutSec } seconds ` ) ;
}
}
}
}
}
} catch ( err ) {
console . log ( ` [ ${ record . id } ] Error parsing HTTP headers: ${ err } ` ) ;
}
}
2025-03-01 17:19:27 +00:00
/ * *
2025-03-03 02:14:21 +00:00
* Cleans up a connection record .
2025-03-03 02:03:24 +00:00
* Destroys both incoming and outgoing sockets , clears timers , and removes the record .
2025-03-03 02:14:21 +00:00
* @param record - The connection record to clean up
* @param reason - Optional reason for cleanup ( for logging )
2025-03-01 17:19:27 +00:00
* /
2025-03-03 02:03:24 +00:00
private cleanupConnection ( record : IConnectionRecord , reason : string = 'normal' ) : void {
if ( ! record . connectionClosed ) {
record . connectionClosed = true ;
2025-03-03 02:14:21 +00:00
2025-03-05 17:46:25 +00:00
// Track connection termination
this . removeConnectionByIP ( record . remoteIP , record . id ) ;
2025-03-03 02:03:24 +00:00
if ( record . cleanupTimer ) {
clearTimeout ( record . cleanupTimer ) ;
2025-03-03 02:14:21 +00:00
record . cleanupTimer = undefined ;
2025-03-01 17:19:27 +00:00
}
2025-03-03 02:14:21 +00:00
2025-03-05 17:46:25 +00:00
// Detailed logging data
const duration = Date . now ( ) - record . incomingStartTime ;
const bytesReceived = record . bytesReceived ;
const bytesSent = record . bytesSent ;
const httpRequests = record . httpRequests ;
2025-03-03 02:14:21 +00:00
try {
if ( ! record . incoming . destroyed ) {
// Try graceful shutdown first, then force destroy after a short timeout
record . incoming . end ( ) ;
2025-03-05 17:06:51 +00:00
const incomingTimeout = setTimeout ( ( ) = > {
try {
if ( record && ! record . incoming . destroyed ) {
record . incoming . destroy ( ) ;
}
} catch ( err ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ record . id } ] Error destroying incoming socket: ${ err } ` ) ;
2025-03-03 02:14:21 +00:00
}
} , 1000 ) ;
2025-03-05 17:06:51 +00:00
// Ensure the timeout doesn't block Node from exiting
if ( incomingTimeout . unref ) {
incomingTimeout . unref ( ) ;
}
2025-03-03 02:14:21 +00:00
}
} catch ( err ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ record . id } ] Error closing incoming socket: ${ err } ` ) ;
2025-03-05 17:06:51 +00:00
try {
if ( ! record . incoming . destroyed ) {
record . incoming . destroy ( ) ;
}
} catch ( destroyErr ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ record . id } ] Error destroying incoming socket: ${ destroyErr } ` ) ;
2025-03-03 02:14:21 +00:00
}
2025-03-01 17:19:27 +00:00
}
2025-03-03 02:14:21 +00:00
try {
if ( record . outgoing && ! record . outgoing . destroyed ) {
// Try graceful shutdown first, then force destroy after a short timeout
record . outgoing . end ( ) ;
2025-03-05 17:06:51 +00:00
const outgoingTimeout = setTimeout ( ( ) = > {
try {
if ( record && record . outgoing && ! record . outgoing . destroyed ) {
record . outgoing . destroy ( ) ;
}
} catch ( err ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ record . id } ] Error destroying outgoing socket: ${ err } ` ) ;
2025-03-03 02:14:21 +00:00
}
} , 1000 ) ;
2025-03-05 17:06:51 +00:00
// Ensure the timeout doesn't block Node from exiting
if ( outgoingTimeout . unref ) {
outgoingTimeout . unref ( ) ;
}
2025-03-03 02:14:21 +00:00
}
} catch ( err ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ record . id } ] Error closing outgoing socket: ${ err } ` ) ;
2025-03-05 17:06:51 +00:00
try {
if ( record . outgoing && ! record . outgoing . destroyed ) {
record . outgoing . destroy ( ) ;
}
} catch ( destroyErr ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ record . id } ] Error destroying outgoing socket: ${ destroyErr } ` ) ;
2025-03-03 02:14:21 +00:00
}
2025-03-01 17:19:27 +00:00
}
2025-03-03 02:14:21 +00:00
2025-03-05 17:06:51 +00:00
// Clear pendingData to avoid memory leaks
record . pendingData = [ ] ;
record . pendingDataSize = 0 ;
2025-03-03 02:14:21 +00:00
// Remove the record from the tracking map
2025-03-03 01:42:16 +00:00
this . connectionRecords . delete ( record . id ) ;
2025-03-03 02:14:21 +00:00
2025-03-05 17:46:25 +00:00
// 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 } , ` +
` HTTP Requests: ${ httpRequests } , Protocol: ${ record . protocolType } , Pooled: ${ record . isPooledConnection } ` ) ;
} else {
console . log ( ` [ ${ record . id } ] Connection from ${ record . remoteIP } terminated ( ${ reason } ). Active connections: ${ this . connectionRecords . size } ` ) ;
}
2025-03-03 02:03:24 +00:00
}
2025-03-01 17:19:27 +00:00
}
2025-03-05 17:46:25 +00:00
/ * *
* Update connection activity timestamp
* /
2025-03-03 02:14:21 +00:00
private updateActivity ( record : IConnectionRecord ) : void {
record . lastActivity = Date . now ( ) ;
}
2025-03-05 17:46:25 +00:00
/ * *
* Get target IP with round - robin support
* /
2025-02-27 21:19:34 +00:00
private getTargetIP ( domainConfig : IDomainConfig ) : string {
if ( domainConfig . targetIPs && domainConfig . targetIPs . length > 0 ) {
const currentIndex = this . domainTargetIndices . get ( domainConfig ) || 0 ;
const ip = domainConfig . targetIPs [ currentIndex % domainConfig . targetIPs . length ] ;
this . domainTargetIndices . set ( domainConfig , currentIndex + 1 ) ;
return ip ;
}
return this . settings . targetIP ! ;
}
2025-03-05 17:46:25 +00:00
/ * *
* Main method to start the proxy
* /
2022-07-29 01:52:34 +02:00
public async start() {
2025-03-05 17:06:51 +00:00
// Don't start if already shutting down
if ( this . isShuttingDown ) {
console . log ( "Cannot start PortProxy while it's shutting down" ) ;
return ;
}
2025-03-05 17:46:25 +00:00
2025-02-27 13:04:01 +00:00
// Define a unified connection handler for all listening ports.
const connectionHandler = ( socket : plugins.net.Socket ) = > {
2025-03-03 01:42:16 +00:00
if ( this . isShuttingDown ) {
socket . end ( ) ;
socket . destroy ( ) ;
return ;
}
2025-02-21 20:17:35 +00:00
const remoteIP = socket . remoteAddress || '' ;
2025-03-05 17:46:25 +00:00
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 ` ) ;
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 ` ) ;
socket . end ( ) ;
socket . destroy ( ) ;
return ;
}
2025-03-03 01:42:16 +00:00
2025-03-05 17:06:51 +00:00
// Apply socket optimizations
socket . setNoDelay ( this . settings . noDelay ) ;
2025-03-05 17:46:25 +00:00
if ( this . settings . keepAlive ) {
socket . setKeepAlive ( true , this . settings . keepAliveInitialDelay ) ;
}
2025-03-05 17:06:51 +00:00
2025-03-05 17:46:25 +00:00
// Create a unique connection ID and record
2025-03-03 01:42:16 +00:00
const connectionId = generateConnectionId ( ) ;
2025-02-23 17:38:22 +00:00
const connectionRecord : IConnectionRecord = {
2025-03-03 01:42:16 +00:00
id : connectionId ,
2025-02-23 17:38:22 +00:00
incoming : socket ,
outgoing : null ,
incomingStartTime : Date.now ( ) ,
2025-03-03 01:42:16 +00:00
lastActivity : Date.now ( ) ,
2025-03-05 14:33:09 +00:00
connectionClosed : false ,
2025-03-05 17:46:25 +00:00
pendingData : [ ] ,
pendingDataSize : 0 ,
// Initialize enhanced tracking fields
protocolType : 'unknown' ,
isPooledConnection : false ,
bytesReceived : 0 ,
bytesSent : 0 ,
remoteIP : remoteIP ,
localPort : localPort ,
httpRequests : 0
2025-02-23 17:38:22 +00:00
} ;
2025-03-05 17:46:25 +00:00
// Track connection by IP
this . trackConnectionByIP ( remoteIP , connectionId ) ;
2025-03-03 01:42:16 +00:00
this . connectionRecords . set ( connectionId , connectionRecord ) ;
2025-03-03 02:14:21 +00:00
2025-03-05 17:46:25 +00:00
if ( this . settings . enableDetailedLogging ) {
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 } ` ) ;
}
2025-02-21 23:18:17 +00:00
2025-02-21 23:11:13 +00:00
let initialDataReceived = false ;
2025-02-23 17:30:41 +00:00
let incomingTerminationReason : string | null = null ;
let outgoingTerminationReason : string | null = null ;
2025-02-21 23:11:13 +00:00
2025-03-03 02:14:21 +00:00
// Local function for cleanupOnce
const cleanupOnce = ( ) = > {
this . cleanupConnection ( connectionRecord ) ;
} ;
2025-03-05 17:46:25 +00:00
// Define initiateCleanupOnce for compatibility
2025-03-03 01:42:16 +00:00
const initiateCleanupOnce = ( reason : string = 'normal' ) = > {
2025-03-05 17:46:25 +00:00
if ( this . settings . enableDetailedLogging ) {
console . log ( ` [ ${ connectionId } ] Connection cleanup initiated for ${ remoteIP } ( ${ reason } ) ` ) ;
}
if ( incomingTerminationReason === null ) {
incomingTerminationReason = reason ;
this . incrementTerminationStat ( 'incoming' , reason ) ;
}
2025-03-03 02:14:21 +00:00
cleanupOnce ( ) ;
2025-02-21 23:05:17 +00:00
} ;
2025-03-03 02:14:21 +00:00
// Helper to reject an incoming connection
2025-02-23 17:30:41 +00:00
const rejectIncomingConnection = ( reason : string , logMessage : string ) = > {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] ${ logMessage } ` ) ;
2025-02-23 17:30:41 +00:00
socket . end ( ) ;
if ( incomingTerminationReason === null ) {
incomingTerminationReason = reason ;
this . incrementTerminationStat ( 'incoming' , reason ) ;
}
2025-03-03 02:03:24 +00:00
cleanupOnce ( ) ;
2025-02-23 17:30:41 +00:00
} ;
2025-03-03 02:14:21 +00:00
// Set an initial timeout for SNI data if needed
2025-03-03 01:57:52 +00:00
let initialTimeout : NodeJS.Timeout | null = null ;
if ( this . settings . sniEnabled ) {
initialTimeout = setTimeout ( ( ) = > {
if ( ! initialDataReceived ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Initial data timeout ( ${ this . settings . initialDataTimeout } ms) for connection from ${ remoteIP } on port ${ localPort } ` ) ;
2025-03-05 17:06:51 +00:00
if ( incomingTerminationReason === null ) {
incomingTerminationReason = 'initial_timeout' ;
this . incrementTerminationStat ( 'incoming' , 'initial_timeout' ) ;
}
2025-03-03 01:57:52 +00:00
socket . end ( ) ;
2025-03-03 02:14:21 +00:00
cleanupOnce ( ) ;
2025-03-03 01:57:52 +00:00
}
2025-03-05 17:46:25 +00:00
} , this . settings . initialDataTimeout ) ;
2025-03-03 01:57:52 +00:00
} else {
2025-03-03 01:50:30 +00:00
initialDataReceived = true ;
}
2025-03-03 01:42:16 +00:00
2025-02-23 17:30:41 +00:00
socket . on ( 'error' , ( err : Error ) = > {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Incoming socket error from ${ remoteIP } : ${ err . message } ` ) ;
} ) ;
// Track data for bytes counting
socket . on ( 'data' , ( chunk : Buffer ) = > {
connectionRecord . bytesReceived += chunk . length ;
this . updateActivity ( connectionRecord ) ;
// Detect protocol on first data chunk
if ( connectionRecord . protocolType === 'unknown' ) {
this . detectProtocol ( chunk , connectionRecord ) ;
// Update timeout based on protocol
if ( connectionRecord . cleanupTimer ) {
clearTimeout ( connectionRecord . cleanupTimer ) ;
// Set new timeout based on protocol
const protocolTimeout = this . getProtocolTimeout ( connectionRecord ) ;
connectionRecord . cleanupTimer = setTimeout ( ( ) = > {
console . log ( ` [ ${ connectionId } ] ${ connectionRecord . protocolType } connection timeout after ${ plugins . prettyMs ( protocolTimeout ) } ` ) ;
initiateCleanupOnce ( ` ${ connectionRecord . protocolType } _timeout ` ) ;
} , protocolTimeout ) ;
}
} else if ( connectionRecord . protocolType === 'http' && isHttpRequest ( chunk ) ) {
// Additional HTTP request on the same connection
connectionRecord . httpRequests ++ ;
connectionRecord . lastHttpRequest = Date . now ( ) ;
// Parse HTTP headers again for keep-alive changes
this . parseHttpHeaders ( chunk , connectionRecord ) ;
// Update timeout based on new HTTP headers
if ( connectionRecord . cleanupTimer ) {
clearTimeout ( connectionRecord . cleanupTimer ) ;
// Set new timeout based on updated HTTP info
const protocolTimeout = this . getProtocolTimeout ( connectionRecord ) ;
connectionRecord . cleanupTimer = setTimeout ( ( ) = > {
console . log ( ` [ ${ connectionId } ] HTTP connection timeout after ${ plugins . prettyMs ( protocolTimeout ) } ` ) ;
initiateCleanupOnce ( 'http_timeout' ) ;
} , protocolTimeout ) ;
}
}
2025-02-23 17:30:41 +00:00
} ) ;
2025-02-21 23:05:17 +00:00
const handleError = ( side : 'incoming' | 'outgoing' ) = > ( err : Error ) = > {
const code = ( err as any ) . code ;
2025-02-22 05:46:30 +00:00
let reason = 'error' ;
2025-03-05 17:46:25 +00:00
const now = Date . now ( ) ;
const connectionDuration = now - connectionRecord . incomingStartTime ;
const lastActivityAge = now - connectionRecord . lastActivity ;
2025-02-21 23:05:17 +00:00
if ( code === 'ECONNRESET' ) {
2025-02-22 05:46:30 +00:00
reason = 'econnreset' ;
2025-03-05 17:46:25 +00:00
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 ` ) ;
2025-02-21 23:05:17 +00:00
} else {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Error on ${ side } side from ${ remoteIP } : ${ err . message } . Duration: ${ plugins . prettyMs ( connectionDuration ) } , Last activity: ${ plugins . prettyMs ( lastActivityAge ) } ago ` ) ;
2025-02-21 23:05:17 +00:00
}
2025-03-05 17:46:25 +00:00
2025-02-23 17:30:41 +00:00
if ( side === 'incoming' && incomingTerminationReason === null ) {
incomingTerminationReason = reason ;
2025-02-22 05:46:30 +00:00
this . incrementTerminationStat ( 'incoming' , reason ) ;
2025-02-23 17:30:41 +00:00
} else if ( side === 'outgoing' && outgoingTerminationReason === null ) {
outgoingTerminationReason = reason ;
2025-02-22 05:46:30 +00:00
this . incrementTerminationStat ( 'outgoing' , reason ) ;
}
2025-03-05 17:46:25 +00:00
2025-03-03 02:14:21 +00:00
initiateCleanupOnce ( reason ) ;
2025-02-21 23:05:17 +00:00
} ;
const handleClose = ( side : 'incoming' | 'outgoing' ) = > ( ) = > {
2025-03-05 17:46:25 +00:00
if ( this . settings . enableDetailedLogging ) {
console . log ( ` [ ${ connectionId } ] Connection closed on ${ side } side from ${ remoteIP } ` ) ;
}
2025-02-23 17:30:41 +00:00
if ( side === 'incoming' && incomingTerminationReason === null ) {
incomingTerminationReason = 'normal' ;
2025-02-22 05:46:30 +00:00
this . incrementTerminationStat ( 'incoming' , 'normal' ) ;
2025-02-23 17:30:41 +00:00
} else if ( side === 'outgoing' && outgoingTerminationReason === null ) {
outgoingTerminationReason = 'normal' ;
2025-02-22 05:46:30 +00:00
this . incrementTerminationStat ( 'outgoing' , 'normal' ) ;
2025-03-01 17:19:27 +00:00
// Record the time when outgoing socket closed.
connectionRecord . outgoingClosedTime = Date . now ( ) ;
2025-02-22 05:46:30 +00:00
}
2025-03-05 17:46:25 +00:00
2025-03-03 02:14:21 +00:00
initiateCleanupOnce ( 'closed_' + side ) ;
2025-02-21 23:05:17 +00:00
} ;
2025-02-21 20:17:35 +00:00
2025-02-27 12:25:48 +00:00
/ * *
* 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 ) .
2025-03-03 01:42:16 +00:00
* @param overridePort - If provided , use this port for the outgoing connection .
2025-02-27 12:25:48 +00:00
* /
2025-02-27 14:23:44 +00:00
const setupConnection = ( serverName : string , initialChunk? : Buffer , forcedDomain? : IDomainConfig , overridePort? : number ) = > {
2025-03-03 01:42:16 +00:00
// Clear the initial timeout since we've received data
if ( initialTimeout ) {
clearTimeout ( initialTimeout ) ;
2025-03-03 02:14:21 +00:00
initialTimeout = null ;
2025-03-03 01:42:16 +00:00
}
2025-03-05 17:46:25 +00:00
// Detect protocol if initial chunk is available
if ( initialChunk && this . settings . enableProtocolDetection ) {
this . detectProtocol ( initialChunk , connectionRecord ) ;
}
2025-02-27 12:25:48 +00:00
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
2025-02-27 15:32:06 +00:00
const domainConfig = forcedDomain
? forcedDomain
2025-02-27 21:19:34 +00:00
: ( serverName ? this . settings . domainConfigs . find ( config = >
config . domains . some ( d = > plugins . minimatch ( serverName , d ) )
) : undefined ) ;
2025-02-23 17:30:41 +00:00
2025-03-03 02:14:21 +00:00
// IP validation is skipped if allowedIPs is empty
2025-02-27 15:32:06 +00:00
if ( domainConfig ) {
2025-03-03 01:57:52 +00:00
const effectiveAllowedIPs : string [ ] = [
. . . domainConfig . allowedIPs ,
. . . ( this . settings . defaultAllowedIPs || [ ] )
] ;
const effectiveBlockedIPs : string [ ] = [
. . . ( domainConfig . blockedIPs || [ ] ) ,
. . . ( this . settings . defaultBlockedIPs || [ ] )
] ;
2025-03-03 02:14:21 +00:00
// Skip IP validation if allowedIPs is empty
2025-03-03 01:57:52 +00:00
if ( domainConfig . allowedIPs . length > 0 && ! isGlobIPAllowed ( remoteIP , effectiveAllowedIPs , effectiveBlockedIPs ) ) {
return rejectIncomingConnection ( 'rejected' , ` Connection rejected: IP ${ remoteIP } not allowed for domain ${ domainConfig . domains . join ( ', ' ) } ` ) ;
2025-02-27 15:32:06 +00:00
}
2025-03-03 01:42:16 +00:00
} else if ( this . settings . defaultAllowedIPs && this . settings . defaultAllowedIPs . length > 0 ) {
2025-03-01 13:17:05 +00:00
if ( ! isGlobIPAllowed ( remoteIP , this . settings . defaultAllowedIPs , this . settings . defaultBlockedIPs || [ ] ) ) {
2025-02-27 15:32:06 +00:00
return rejectIncomingConnection ( 'rejected' , ` Connection rejected: IP ${ remoteIP } not allowed by default allowed list ` ) ;
2025-02-22 05:46:30 +00:00
}
2025-03-03 02:14:21 +00:00
}
2025-03-01 13:17:05 +00:00
2025-02-27 21:19:34 +00:00
const targetHost = domainConfig ? this . getTargetIP ( domainConfig ) : this . settings . targetIP ! ;
2025-02-21 20:17:35 +00:00
const connectionOptions : plugins.net.NetConnectOpts = {
host : targetHost ,
2025-02-27 14:23:44 +00:00
port : overridePort !== undefined ? overridePort : this.settings.toPort ,
2025-02-21 20:17:35 +00:00
} ;
if ( this . settings . preserveSourceIP ) {
connectionOptions . localAddress = remoteIP . replace ( '::ffff:' , '' ) ;
2025-02-21 18:47:18 +00:00
}
2025-02-21 23:05:17 +00:00
2025-03-05 17:06:51 +00:00
// Pause the incoming socket to prevent buffer overflows
socket . pause ( ) ;
2025-03-05 14:33:09 +00:00
// Temporary handler to collect data during connection setup
const tempDataHandler = ( chunk : Buffer ) = > {
2025-03-05 17:46:25 +00:00
// Track bytes received
connectionRecord . bytesReceived += chunk . length ;
// Detect protocol even during connection setup
if ( this . settings . enableProtocolDetection && connectionRecord . protocolType === 'unknown' ) {
this . detectProtocol ( chunk , connectionRecord ) ;
}
2025-03-05 17:06:51 +00:00
// Check if adding this chunk would exceed the buffer limit
const newSize = connectionRecord . pendingDataSize + chunk . length ;
if ( this . settings . maxPendingDataSize && newSize > this . settings . maxPendingDataSize ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Buffer limit exceeded for connection from ${ remoteIP } : ${ newSize } bytes > ${ this . settings . maxPendingDataSize } bytes ` ) ;
2025-03-05 17:06:51 +00:00
socket . end ( ) ; // Gracefully close the socket
return initiateCleanupOnce ( 'buffer_limit_exceeded' ) ;
}
// Buffer the chunk and update the size counter
2025-03-05 14:33:09 +00:00
connectionRecord . pendingData . push ( Buffer . from ( chunk ) ) ;
2025-03-05 17:06:51 +00:00
connectionRecord . pendingDataSize = newSize ;
2025-03-05 14:33:09 +00:00
this . updateActivity ( connectionRecord ) ;
} ;
// Add the temp handler to capture all incoming data during connection setup
socket . on ( 'data' , tempDataHandler ) ;
// Add initial chunk to pending data if present
if ( initialChunk ) {
2025-03-05 17:46:25 +00:00
connectionRecord . bytesReceived += initialChunk . length ;
2025-03-05 14:33:09 +00:00
connectionRecord . pendingData . push ( Buffer . from ( initialChunk ) ) ;
2025-03-05 17:06:51 +00:00
connectionRecord . pendingDataSize = initialChunk . length ;
2025-03-05 14:33:09 +00:00
}
// Create the target socket but don't set up piping immediately
2025-02-23 17:38:22 +00:00
const targetSocket = plugins . net . connect ( connectionOptions ) ;
connectionRecord . outgoing = targetSocket ;
2025-03-03 02:14:21 +00:00
connectionRecord . outgoingStartTime = Date . now ( ) ;
2025-03-03 01:42:16 +00:00
2025-03-05 17:06:51 +00:00
// Apply socket optimizations
targetSocket . setNoDelay ( this . settings . noDelay ) ;
2025-03-05 17:46:25 +00:00
if ( this . settings . keepAlive ) {
targetSocket . setKeepAlive ( true , this . settings . keepAliveInitialDelay ) ;
}
2025-03-05 17:06:51 +00:00
// 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 ;
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Connection setup error to ${ targetHost } : ${ connectionOptions . port } : ${ err . message } ( ${ code } ) ` ) ;
2025-03-05 17:06:51 +00:00
// Resume the incoming socket to prevent it from hanging
socket . resume ( ) ;
if ( code === 'ECONNREFUSED' ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Target ${ targetHost } : ${ connectionOptions . port } refused connection ` ) ;
2025-03-05 17:06:51 +00:00
} else if ( code === 'ETIMEDOUT' ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Connection to ${ targetHost } : ${ connectionOptions . port } timed out ` ) ;
2025-03-05 17:06:51 +00:00
} else if ( code === 'ECONNRESET' ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Connection to ${ targetHost } : ${ connectionOptions . port } was reset ` ) ;
2025-03-05 17:06:51 +00:00
} else if ( code === 'EHOSTUNREACH' ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Host ${ targetHost } is unreachable ` ) ;
2025-03-05 17:06:51 +00:00
}
// 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
2025-02-23 17:30:41 +00:00
targetSocket . on ( 'close' , handleClose ( 'outgoing' ) ) ;
2025-03-05 17:06:51 +00:00
socket . on ( 'close' , handleClose ( 'incoming' ) ) ;
2025-03-05 14:33:09 +00:00
// Handle timeouts
2025-02-21 23:33:15 +00:00
socket . on ( 'timeout' , ( ) = > {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Timeout on incoming side from ${ remoteIP } after ${ plugins . prettyMs ( this . settings . socketTimeout || 300000 ) } ` ) ;
2025-02-23 17:30:41 +00:00
if ( incomingTerminationReason === null ) {
incomingTerminationReason = 'timeout' ;
2025-02-22 05:46:30 +00:00
this . incrementTerminationStat ( 'incoming' , 'timeout' ) ;
}
2025-03-03 02:14:21 +00:00
initiateCleanupOnce ( 'timeout_incoming' ) ;
2025-02-21 23:33:15 +00:00
} ) ;
2025-03-05 17:46:25 +00:00
2025-02-23 17:30:41 +00:00
targetSocket . on ( 'timeout' , ( ) = > {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Timeout on outgoing side from ${ remoteIP } after ${ plugins . prettyMs ( this . settings . socketTimeout || 300000 ) } ` ) ;
2025-02-23 17:30:41 +00:00
if ( outgoingTerminationReason === null ) {
outgoingTerminationReason = 'timeout' ;
2025-02-22 05:46:30 +00:00
this . incrementTerminationStat ( 'outgoing' , 'timeout' ) ;
}
2025-03-03 02:14:21 +00:00
initiateCleanupOnce ( 'timeout_outgoing' ) ;
2025-02-21 23:33:15 +00:00
} ) ;
2025-02-26 10:29:21 +00:00
2025-03-05 17:46:25 +00:00
// Set appropriate timeouts using the configured value
socket . setTimeout ( this . settings . socketTimeout || 300000 ) ;
targetSocket . setTimeout ( this . settings . socketTimeout || 300000 ) ;
// Track outgoing data for bytes counting
targetSocket . on ( 'data' , ( chunk : Buffer ) = > {
connectionRecord . bytesSent += chunk . length ;
this . updateActivity ( connectionRecord ) ;
} ) ;
2025-03-03 02:14:21 +00:00
2025-03-05 14:33:09 +00:00
// Wait for the outgoing connection to be ready before setting up piping
targetSocket . once ( 'connect' , ( ) = > {
2025-03-05 17:06:51 +00:00
// Clear the initial connection error handler
targetSocket . removeAllListeners ( 'error' ) ;
// Add the normal error handler for established connections
targetSocket . on ( 'error' , handleError ( 'outgoing' ) ) ;
2025-03-05 14:33:09 +00:00
// 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 ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Error writing pending data to target: ${ err . message } ` ) ;
2025-03-05 14:33:09 +00:00
return initiateCleanupOnce ( 'write_error' ) ;
}
2025-03-05 17:06:51 +00:00
// Now set up piping for future data and resume the socket
2025-03-05 14:33:09 +00:00
socket . pipe ( targetSocket ) ;
targetSocket . pipe ( socket ) ;
2025-03-05 17:06:51 +00:00
socket . resume ( ) ; // Resume the socket after piping is established
2025-03-05 14:33:09 +00:00
2025-03-05 17:46:25 +00:00
if ( this . settings . enableDetailedLogging ) {
console . log (
` [ ${ connectionId } ] Connection established: ${ remoteIP } -> ${ targetHost } : ${ connectionOptions . port } ` +
` ${ serverName ? ` (SNI: ${ serverName } ) ` : forcedDomain ? ` (Port-based for domain: ${ forcedDomain . domains . join ( ', ' ) } ) ` : '' } ` +
` Protocol: ${ connectionRecord . protocolType } `
) ;
} else {
console . log (
` Connection established: ${ remoteIP } -> ${ targetHost } : ${ connectionOptions . port } ` +
` ${ serverName ? ` (SNI: ${ serverName } ) ` : forcedDomain ? ` (Port-based for domain: ${ forcedDomain . domains . join ( ', ' ) } ) ` : '' } `
) ;
}
2025-03-05 14:33:09 +00:00
} ) ;
} else {
// No pending data, so just set up piping
socket . pipe ( targetSocket ) ;
targetSocket . pipe ( socket ) ;
2025-03-05 17:06:51 +00:00
socket . resume ( ) ; // Resume the socket after piping is established
2025-03-05 14:33:09 +00:00
2025-03-05 17:46:25 +00:00
if ( this . settings . enableDetailedLogging ) {
console . log (
` [ ${ connectionId } ] Connection established: ${ remoteIP } -> ${ targetHost } : ${ connectionOptions . port } ` +
` ${ serverName ? ` (SNI: ${ serverName } ) ` : forcedDomain ? ` (Port-based for domain: ${ forcedDomain . domains . join ( ', ' ) } ) ` : '' } ` +
` Protocol: ${ connectionRecord . protocolType } `
) ;
} else {
console . log (
` Connection established: ${ remoteIP } -> ${ targetHost } : ${ connectionOptions . port } ` +
` ${ serverName ? ` (SNI: ${ serverName } ) ` : forcedDomain ? ` (Port-based for domain: ${ forcedDomain . domains . join ( ', ' ) } ) ` : '' } `
) ;
}
2025-03-05 14:33:09 +00:00
}
// Clear the buffer now that we've processed it
connectionRecord . pendingData = [ ] ;
2025-03-05 17:06:51 +00:00
connectionRecord . pendingDataSize = 0 ;
2025-03-05 14:33:09 +00:00
2025-03-05 17:46:25 +00:00
// Add the renegotiation listener for SNI validation
2025-03-05 14:33:09 +00:00
if ( serverName ) {
socket . on ( 'data' , ( renegChunk : Buffer ) = > {
if ( renegChunk . length > 0 && renegChunk . readUInt8 ( 0 ) === 22 ) {
try {
// Try to extract SNI from potential renegotiation
const newSNI = extractSNI ( renegChunk ) ;
if ( newSNI && newSNI !== connectionRecord . lockedDomain ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Rehandshake detected with different SNI: ${ newSNI } vs locked ${ connectionRecord . lockedDomain } . Terminating connection. ` ) ;
2025-03-05 14:33:09 +00:00
initiateCleanupOnce ( 'sni_mismatch' ) ;
2025-03-05 17:46:25 +00:00
} else if ( newSNI && this . settings . enableDetailedLogging ) {
console . log ( ` [ ${ connectionId } ] Rehandshake detected with same SNI: ${ newSNI } . Allowing. ` ) ;
2025-03-05 14:33:09 +00:00
}
} catch ( err ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Error processing potential renegotiation: ${ err } . Allowing connection to continue. ` ) ;
2025-03-05 14:33:09 +00:00
}
}
} ) ;
}
2025-03-05 17:46:25 +00:00
// Set protocol-specific timeout based on detected protocol
if ( connectionRecord . cleanupTimer ) {
clearTimeout ( connectionRecord . cleanupTimer ) ;
}
// Set timeout based on protocol
const protocolTimeout = this . getProtocolTimeout ( connectionRecord , domainConfig ) ;
2025-03-03 01:42:16 +00:00
connectionRecord . cleanupTimer = setTimeout ( ( ) = > {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] ${ connectionRecord . protocolType } connection exceeded max lifetime ( ${ plugins . prettyMs ( protocolTimeout ) } ), forcing cleanup. ` ) ;
initiateCleanupOnce ( ` ${ connectionRecord . protocolType } _max_lifetime ` ) ;
} , protocolTimeout ) ;
} ) ;
2025-02-21 23:05:17 +00:00
} ;
2025-02-27 12:25:48 +00:00
// --- PORT RANGE-BASED HANDLING ---
2025-02-27 15:41:03 +00:00
// 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 ) ) {
2025-02-27 12:41:20 +00:00
if ( this . settings . forwardAllGlobalRanges ) {
if ( this . settings . defaultAllowedIPs && ! isAllowed ( remoteIP , this . settings . defaultAllowedIPs ) ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Connection from ${ remoteIP } rejected: IP ${ remoteIP } not allowed in global default allowed list. ` ) ;
2025-02-27 12:41:20 +00:00
socket . end ( ) ;
return ;
}
2025-03-05 17:46:25 +00:00
if ( this . settings . enableDetailedLogging ) {
console . log ( ` [ ${ connectionId } ] Port-based connection from ${ remoteIP } on port ${ localPort } forwarded to global target IP ${ this . settings . targetIP } . ` ) ;
}
2025-02-27 12:41:20 +00:00
setupConnection ( '' , undefined , {
2025-02-27 21:19:34 +00:00
domains : [ 'global' ] ,
2025-02-27 12:41:20 +00:00
allowedIPs : this.settings.defaultAllowedIPs || [ ] ,
2025-03-01 13:17:05 +00:00
blockedIPs : this.settings.defaultBlockedIPs || [ ] ,
2025-02-27 21:19:34 +00:00
targetIPs : [ this . settings . targetIP ! ] ,
2025-02-27 12:41:20 +00:00
portRanges : [ ]
2025-02-27 14:23:44 +00:00
} , localPort ) ;
2025-02-27 12:25:48 +00:00
return ;
2025-02-27 12:41:20 +00:00
} else {
2025-02-27 12:54:14 +00:00
// Attempt to find a matching forced domain config based on the local port.
2025-02-27 21:19:34 +00:00
const forcedDomain = this . settings . domainConfigs . find (
2025-02-27 12:41:20 +00:00
domain = > domain . portRanges && domain . portRanges . length > 0 && isPortInRanges ( localPort , domain . portRanges )
) ;
2025-02-27 12:54:14 +00:00
if ( forcedDomain ) {
2025-03-01 17:32:31 +00:00
const effectiveAllowedIPs : string [ ] = [
. . . forcedDomain . allowedIPs ,
. . . ( this . settings . defaultAllowedIPs || [ ] )
] ;
const effectiveBlockedIPs : string [ ] = [
. . . ( forcedDomain . blockedIPs || [ ] ) ,
. . . ( this . settings . defaultBlockedIPs || [ ] )
] ;
if ( ! isGlobIPAllowed ( remoteIP , effectiveAllowedIPs , effectiveBlockedIPs ) ) {
2025-03-05 17:46:25 +00:00
console . log ( ` [ ${ connectionId } ] Connection from ${ remoteIP } rejected: IP not allowed for domain ${ forcedDomain . domains . join ( ', ' ) } on port ${ localPort } . ` ) ;
2025-02-27 12:54:14 +00:00
socket . end ( ) ;
return ;
}
2025-03-05 17:46:25 +00:00
if ( this . settings . enableDetailedLogging ) {
console . log ( ` [ ${ connectionId } ] Port-based connection from ${ remoteIP } on port ${ localPort } matched domain ${ forcedDomain . domains . join ( ', ' ) } . ` ) ;
}
2025-02-27 14:23:44 +00:00
setupConnection ( '' , undefined , forcedDomain , localPort ) ;
2025-02-27 12:41:20 +00:00
return ;
}
2025-02-27 13:04:01 +00:00
// Fall through to SNI/default handling if no forced domain config is found.
2025-02-27 12:25:48 +00:00
}
}
2025-02-27 12:54:14 +00:00
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
2025-02-21 23:05:17 +00:00
if ( this . settings . sniEnabled ) {
2025-03-03 01:42:16 +00:00
initialDataReceived = false ;
2025-03-03 01:57:52 +00:00
2025-02-21 23:05:17 +00:00
socket . once ( 'data' , ( chunk : Buffer ) = > {
2025-03-03 01:50:30 +00:00
if ( initialTimeout ) {
clearTimeout ( initialTimeout ) ;
initialTimeout = null ;
}
2025-02-21 23:11:13 +00:00
initialDataReceived = true ;
2025-03-03 01:57:52 +00:00
const serverName = extractSNI ( chunk ) || '' ;
2025-02-27 20:59:29 +00:00
// Lock the connection to the negotiated SNI.
connectionRecord . lockedDomain = serverName ;
2025-03-05 17:46:25 +00:00
if ( this . settings . enableDetailedLogging ) {
console . log ( ` [ ${ connectionId } ] Received connection from ${ remoteIP } with SNI: ${ serverName || '(empty)' } ` ) ;
}
2025-03-03 01:42:16 +00:00
2025-02-21 23:05:17 +00:00
setupConnection ( serverName , chunk ) ;
} ) ;
} else {
2025-02-21 23:11:13 +00:00
initialDataReceived = true ;
2025-03-03 01:57:52 +00:00
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 ` ) ;
2025-02-21 23:05:17 +00:00
}
setupConnection ( '' ) ;
2025-02-21 19:44:59 +00:00
}
2025-02-27 13:04:01 +00:00
} ;
// --- SETUP LISTENERS ---
// Determine which ports to listen on.
const listeningPorts = new Set < number > ( ) ;
if ( this . settings . globalPortRanges && this . settings . globalPortRanges . length > 0 ) {
// Listen on every port defined by the global ranges.
for ( const range of this . settings . globalPortRanges ) {
for ( let port = range . from ; port <= range . to ; port ++ ) {
listeningPorts . add ( port ) ;
}
}
2025-02-27 21:25:03 +00:00
// Also ensure the default fromPort is listened to if it isn't already in the ranges.
2025-02-27 13:04:01 +00:00
listeningPorts . add ( this . settings . fromPort ) ;
} else {
listeningPorts . add ( this . settings . fromPort ) ;
}
// 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 } ` ) ;
} ) ;
server . listen ( port , ( ) = > {
console . log ( ` PortProxy -> OK: Now listening on port ${ port } ${ this . settings . sniEnabled ? ' (SNI passthrough enabled)' : '' } ` ) ;
2025-02-23 17:30:41 +00:00
} ) ;
2025-02-27 13:04:01 +00:00
this . netServers . push ( server ) ;
}
2025-02-21 23:05:17 +00:00
2025-03-05 17:46:25 +00:00
// Log active connection count, longest running durations, and run parity checks periodically
2025-02-21 23:05:17 +00:00
this . connectionLogger = setInterval ( ( ) = > {
2025-03-05 17:06:51 +00:00
// Immediately return if shutting down
if ( this . isShuttingDown ) return ;
2025-03-03 01:42:16 +00:00
2025-02-21 23:18:17 +00:00
const now = Date . now ( ) ;
let maxIncoming = 0 ;
let maxOutgoing = 0 ;
2025-03-05 17:46:25 +00:00
let httpConnections = 0 ;
let wsConnections = 0 ;
let tlsConnections = 0 ;
let unknownConnections = 0 ;
let pooledConnections = 0 ;
2025-03-03 01:42:16 +00:00
// 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 ;
2025-03-05 17:46:25 +00:00
// Track connection stats by protocol
switch ( record . protocolType ) {
case 'http' : httpConnections ++ ; break ;
case 'websocket' : wsConnections ++ ; break ;
case 'tls' :
case 'https' : tlsConnections ++ ; break ;
default : unknownConnections ++ ; break ;
}
if ( record . isPooledConnection ) {
pooledConnections ++ ;
}
2025-02-23 17:38:22 +00:00
maxIncoming = Math . max ( maxIncoming , now - record . incomingStartTime ) ;
if ( record . outgoingStartTime ) {
maxOutgoing = Math . max ( maxOutgoing , now - record . outgoingStartTime ) ;
}
2025-03-03 01:42:16 +00:00
2025-03-03 02:14:21 +00:00
// Parity check: if outgoing socket closed and incoming remains active
2025-03-03 01:42:16 +00:00
if ( record . outgoingClosedTime &&
! record . incoming . destroyed &&
! record . connectionClosed &&
2025-03-05 18:24:28 +00:00
( now - record . outgoingClosedTime > 120000 ) ) {
2025-03-05 17:46:25 +00:00
const remoteIP = record . remoteIP ;
console . log ( ` [ ${ id } ] Parity check: Incoming socket for ${ remoteIP } still active ${ plugins . prettyMs ( now - record . outgoingClosedTime ) } after outgoing closed. ` ) ;
2025-03-03 02:14:21 +00:00
this . cleanupConnection ( record , 'parity_check' ) ;
2025-03-03 01:42:16 +00:00
}
2025-03-05 17:46:25 +00:00
// Skip inactivity check if disabled
if ( ! this . settings . disableInactivityCheck ) {
// Inactivity check - use protocol-specific values
2025-03-05 18:07:39 +00:00
let inactivityThreshold = Math . floor ( Math . random ( ) * ( 1800000 - 1200000 + 1 ) ) + 1200000 ; // random between 20 and 30 minutes
2025-03-05 17:46:25 +00:00
// Set protocol-specific inactivity thresholds
if ( record . protocolType === 'http' && record . isPooledConnection ) {
inactivityThreshold = this . settings . httpKeepAliveTimeout || 1200000 ; // 20 minutes for pooled HTTP
} else if ( record . protocolType === 'websocket' ) {
inactivityThreshold = this . settings . wsConnectionTimeout || 14400000 ; // 4 hours for WebSocket
} else if ( record . protocolType === 'http' ) {
inactivityThreshold = this . settings . httpConnectionTimeout || 1800000 ; // 30 minutes for HTTP
}
const inactivityTime = now - record . lastActivity ;
if ( inactivityTime > inactivityThreshold && ! record . connectionClosed ) {
console . log ( ` [ ${ id } ] Inactivity check: No activity on ${ record . protocolType } connection from ${ record . remoteIP } for ${ plugins . prettyMs ( inactivityTime ) } . ` ) ;
this . cleanupConnection ( record , 'inactivity' ) ;
}
2025-03-01 17:19:27 +00:00
}
2025-02-21 23:18:17 +00:00
}
2025-03-03 01:42:16 +00:00
2025-03-05 17:46:25 +00:00
// Log detailed stats periodically
2025-02-23 17:30:41 +00:00
console . log (
2025-03-05 17:46:25 +00:00
` Active connections: ${ this . connectionRecords . size } . ` +
` Types: HTTP= ${ httpConnections } , WS= ${ wsConnections } , TLS= ${ tlsConnections } , Unknown= ${ unknownConnections } , Pooled= ${ pooledConnections } . ` +
` Longest running: IN= ${ plugins . prettyMs ( maxIncoming ) } , OUT= ${ plugins . prettyMs ( maxOutgoing ) } . ` +
` Termination stats: ${ JSON . stringify ( { IN : this.terminationStats.incoming , OUT : this.terminationStats.outgoing } )} `
2025-02-23 17:30:41 +00:00
) ;
2025-03-05 17:46:25 +00:00
} , this . settings . inactivityCheckInterval || 30000 ) ;
// Make sure the interval doesn't keep the process alive
if ( this . connectionLogger . unref ) {
this . connectionLogger . unref ( ) ;
}
2022-07-29 00:49:46 +02:00
}
2025-03-05 17:46:25 +00:00
/ * *
* Gracefully shut down the proxy
* /
2022-07-29 00:49:46 +02:00
public async stop() {
2025-03-03 01:42:16 +00:00
console . log ( "PortProxy shutting down..." ) ;
this . isShuttingDown = true ;
// Stop accepting new connections
const closeServerPromises : Promise < void > [ ] = this . netServers . map (
2025-02-27 13:04:01 +00:00
server = >
new Promise < void > ( ( resolve ) = > {
2025-03-05 17:06:51 +00:00
if ( ! server . listening ) {
resolve ( ) ;
return ;
}
server . close ( ( err ) = > {
if ( err ) {
console . log ( ` Error closing server: ${ err . message } ` ) ;
}
resolve ( ) ;
} ) ;
2025-02-27 13:04:01 +00:00
} )
) ;
2025-03-03 01:42:16 +00:00
// Stop the connection logger
2025-02-21 23:05:17 +00:00
if ( this . connectionLogger ) {
clearInterval ( this . connectionLogger ) ;
this . connectionLogger = null ;
}
2025-03-03 01:42:16 +00:00
// Wait for servers to close
await Promise . all ( closeServerPromises ) ;
console . log ( "All servers closed. Cleaning up active connections..." ) ;
2025-03-05 17:06:51 +00:00
// Force destroy all active connections immediately
2025-03-03 01:42:16 +00:00
const connectionIds = [ . . . this . connectionRecords . keys ( ) ] ;
console . log ( ` Cleaning up ${ connectionIds . length } active connections... ` ) ;
2025-03-05 17:06:51 +00:00
// First pass: End all connections gracefully
2025-03-03 01:42:16 +00:00
for ( const id of connectionIds ) {
const record = this . connectionRecords . get ( id ) ;
2025-03-05 17:06:51 +00:00
if ( record ) {
try {
// Clear any timers
if ( record . cleanupTimer ) {
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 ( ) ;
}
} catch ( err ) {
console . log ( ` Error during graceful connection end for ${ id } : ${ err } ` ) ;
}
2025-03-03 01:42:16 +00:00
}
}
2025-03-05 17:06:51 +00:00
// Short delay to allow graceful ends to process
await new Promise ( resolve = > setTimeout ( resolve , 100 ) ) ;
// Second pass: Force destroy everything
for ( const id of connectionIds ) {
const record = this . connectionRecords . get ( id ) ;
if ( record ) {
try {
// Remove all listeners to prevent memory leaks
if ( record . incoming ) {
record . incoming . removeAllListeners ( ) ;
2025-03-03 01:42:16 +00:00
if ( ! record . incoming . destroyed ) {
record . incoming . destroy ( ) ;
}
2025-03-05 17:06:51 +00:00
}
if ( record . outgoing ) {
record . outgoing . removeAllListeners ( ) ;
if ( ! record . outgoing . destroyed ) {
2025-03-03 01:42:16 +00:00
record . outgoing . destroy ( ) ;
}
}
2025-03-05 17:06:51 +00:00
} catch ( err ) {
console . log ( ` Error during forced connection destruction for ${ id } : ${ err } ` ) ;
2025-03-03 01:42:16 +00:00
}
2025-03-05 17:06:51 +00:00
}
}
2025-03-05 17:46:25 +00:00
// Clear all tracking maps
2025-03-05 17:06:51 +00:00
this . connectionRecords . clear ( ) ;
this . domainTargetIndices . clear ( ) ;
2025-03-05 17:46:25 +00:00
this . connectionsByIP . clear ( ) ;
this . connectionRateByIP . clear ( ) ;
2025-03-05 17:06:51 +00:00
this . netServers = [ ] ;
// Reset termination stats
this . terminationStats = {
incoming : { } ,
outgoing : { }
} ;
2025-03-03 01:42:16 +00:00
console . log ( "PortProxy shutdown complete." ) ;
2022-07-29 00:49:46 +02:00
}
2025-03-01 13:17:05 +00:00
}