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 18:40:42 +00:00
// Allow domain-specific timeout override
connectionTimeout? : number ; // Connection timeout override (ms)
2025-02-21 15:14:02 +00:00
}
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
2025-03-05 18:40:42 +00:00
// 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)
2025-03-05 17:46:25 +00:00
2025-03-05 18:40:42 +00:00
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 18:40:42 +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 18:40:42 +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
2025-03-05 17:46:25 +00:00
2025-03-05 18:40:42 +00:00
// 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
enableRandomizedTimeouts? : boolean ; // Randomize timeouts slightly to prevent thundering herd
2025-03-05 17:46:25 +00:00
// Rate limiting and security
2025-03-05 18:40:42 +00:00
maxConnectionsPerIP? : number ; // Maximum simultaneous connections from a single IP
2025-03-05 17:46:25 +00:00
connectionRateLimitPerMinute? : number ; // Max new connections per minute from a single IP
}
/ * *
2025-03-05 18:40:42 +00:00
* Enhanced connection record
2025-03-05 17:46:25 +00:00
* /
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
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)
2025-03-05 18:40:42 +00:00
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
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 .
2025-03-05 18:40:42 +00:00
* Enhanced for robustness and detailed logging .
2025-02-23 17:30:41 +00:00
* @param buffer - Buffer containing the TLS ClientHello .
2025-03-05 18:40:42 +00:00
* @param enableLogging - Whether to enable detailed logging .
2025-02-23 17:30:41 +00:00
* @returns The server name if found , otherwise undefined .
2025-02-21 20:17:35 +00:00
* /
2025-03-05 18:40:42 +00:00
function extractSNI ( buffer : Buffer , enableLogging : boolean = false ) : string | undefined {
try {
// Check if buffer is too small for TLS
if ( buffer . length < 5 ) {
if ( enableLogging ) console . log ( "Buffer too small for TLS header" ) ;
return undefined ;
}
2025-02-21 20:17:35 +00:00
2025-03-05 18:40:42 +00:00
// Check record type (has to be handshake - 22)
const recordType = buffer . readUInt8 ( 0 ) ;
if ( recordType !== 22 ) {
if ( enableLogging ) console . log ( ` Not a TLS handshake. Record type: ${ recordType } ` ) ;
return undefined ;
}
2025-02-21 20:17:35 +00:00
2025-03-05 18:40:42 +00:00
// Check TLS version (has to be 3.1 or higher)
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 } ` ) ;
return undefined ;
}
2025-02-21 20:17:35 +00:00
2025-03-05 18:40:42 +00:00
let offset = 5 ;
const handshakeType = buffer . readUInt8 ( offset ) ;
if ( handshakeType !== 1 ) {
if ( enableLogging ) console . log ( ` Not a ClientHello. Handshake type: ${ handshakeType } ` ) ;
return undefined ;
}
2025-02-21 20:17:35 +00:00
2025-03-05 18:40:42 +00:00
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
2025-02-21 20:17:35 +00:00
2025-03-05 18:40:42 +00:00
// Session ID
const sessionIDLength = buffer . readUInt8 ( offset ) ;
if ( enableLogging ) console . log ( ` Session ID Length: ${ sessionIDLength } ` ) ;
offset += 1 + sessionIDLength ; // Skip session ID
2025-02-21 20:17:35 +00:00
2025-03-05 18:40:42 +00:00
// Cipher suites
if ( offset + 2 > buffer . length ) {
if ( enableLogging ) console . log ( "Buffer too small for cipher suites length" ) ;
return undefined ;
}
const cipherSuitesLength = buffer . readUInt16BE ( offset ) ;
if ( enableLogging ) console . log ( ` Cipher Suites Length: ${ cipherSuitesLength } ` ) ;
offset += 2 + cipherSuitesLength ; // Skip cipher suites
// Compression methods
if ( offset + 1 > buffer . length ) {
if ( enableLogging ) console . log ( "Buffer too small for compression methods length" ) ;
return undefined ;
}
const compressionMethodsLength = buffer . readUInt8 ( offset ) ;
if ( enableLogging ) console . log ( ` Compression Methods Length: ${ compressionMethodsLength } ` ) ;
offset += 1 + compressionMethodsLength ; // Skip compression methods
// Extensions
if ( offset + 2 > buffer . length ) {
if ( enableLogging ) console . log ( "Buffer too small for extensions length" ) ;
return undefined ;
}
const extensionsLength = buffer . readUInt16BE ( offset ) ;
if ( enableLogging ) console . log ( ` Extensions Length: ${ extensionsLength } ` ) ;
offset += 2 ;
const extensionsEnd = offset + extensionsLength ;
if ( extensionsEnd > buffer . length ) {
if ( enableLogging ) console . log ( ` Buffer too small for extensions. Expected end: ${ extensionsEnd } , Buffer length: ${ buffer . length } ` ) ;
return undefined ;
}
2025-02-21 20:17:35 +00:00
2025-03-05 18:40:42 +00:00
// Parse extensions
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 } ` ) ;
offset += 4 ;
if ( extensionType === 0x0000 ) { // SNI extension
if ( offset + 2 > buffer . 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 } ` ) ;
2025-02-21 20:17:35 +00:00
offset += 2 ;
2025-03-05 18:40:42 +00:00
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 } ` ) ;
return undefined ;
2025-02-21 20:17:35 +00:00
}
2025-03-05 18:40:42 +00:00
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 ( offset + nameLen > 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 ;
} else {
offset += extensionLength ;
2025-02-21 20:17:35 +00:00
}
}
2025-03-05 18:40:42 +00:00
if ( enableLogging ) console . log ( "No SNI extension found" ) ;
return undefined ;
} catch ( err ) {
console . log ( ` Error extracting SNI: ${ err } ` ) ;
return undefined ;
2025-02-21 20:17:35 +00:00
}
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 = > {
2025-03-05 18:40:42 +00:00
if ( ! ip || ! patterns || patterns . length === 0 ) return false ;
2025-03-01 13:17:05 +00:00
const normalizeIP = ( ip : string ) : string [ ] = > {
2025-03-05 18:40:42 +00:00
if ( ! ip ) return [ ] ;
2025-03-01 13:17:05 +00:00
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 ] ;
} ;
2025-03-05 18:40:42 +00:00
2025-03-01 13:17:05 +00:00
const normalizedIPVariants = normalizeIP ( ip ) ;
2025-03-05 18:40:42 +00:00
if ( normalizedIPVariants . length === 0 ) return false ;
2025-03-01 13:17:05 +00:00
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 = > {
2025-03-05 18:40:42 +00:00
if ( ! ip ) return false ;
2025-03-01 13:17:05 +00:00
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 18:40:42 +00:00
// Helper: Check if a buffer contains a TLS handshake
2025-03-05 17:46:25 +00:00
const isTlsHandshake = ( buffer : Buffer ) : boolean = > {
return buffer . length > 0 && buffer [ 0 ] === 22 ; // ContentType.handshake
} ;
2025-03-05 18:40:42 +00:00
// Helper: Generate a slightly randomized timeout to prevent thundering herd
const randomizeTimeout = ( baseTimeout : number , variationPercent : number = 5 ) : number = > {
const variation = baseTimeout * ( variationPercent / 100 ) ;
return baseTimeout + Math . floor ( Math . random ( ) * variation * 2 ) - variation ;
} ;
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
2025-03-05 18:40:42 +00:00
// Timeout settings with our enhanced defaults
initialDataTimeout : settingsArg.initialDataTimeout || 60000 , // 60 seconds for initial data
socketTimeout : settingsArg.socketTimeout || 3600000 , // 1 hour 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
2025-03-05 17:46:25 +00:00
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 18:40:42 +00:00
maxPendingDataSize : settingsArg.maxPendingDataSize || 10 * 1024 * 1024 , // 10MB to handle large TLS handshakes
2025-03-05 17:46:25 +00:00
// Feature flags
disableInactivityCheck : settingsArg.disableInactivityCheck || false ,
enableKeepAliveProbes : settingsArg.enableKeepAliveProbes || false ,
enableDetailedLogging : settingsArg.enableDetailedLogging || false ,
2025-03-05 18:40:42 +00:00
enableTlsDebugLogging : settingsArg.enableTlsDebugLogging || false ,
enableRandomizedTimeouts : settingsArg.enableRandomizedTimeouts || true ,
2025-03-05 17:46:25 +00:00
// 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
/ * *
2025-03-05 18:40:42 +00:00
* Get connection timeout based on domain config or default settings
2025-03-05 17:46:25 +00:00
* /
2025-03-05 18:40:42 +00:00
private getConnectionTimeout ( record : IConnectionRecord ) : number {
// If the connection has a domain-specific timeout, use that
if ( record . domainConfig ? . connectionTimeout ) {
return record . domainConfig . connectionTimeout ;
2025-03-05 17:46:25 +00:00
}
2025-03-05 18:40:42 +00:00
// Use default timeout, potentially randomized
const baseTimeout = this . settings . maxConnectionLifetime ! ;
if ( this . settings . enableRandomizedTimeouts ) {
return randomizeTimeout ( baseTimeout ) ;
2025-03-05 17:46:25 +00:00
}
2025-03-05 18:40:42 +00:00
return baseTimeout ;
2025-03-05 17:46:25 +00:00
}
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 ;
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 } , ` +
2025-03-05 18:40:42 +00:00
` TLS: ${ record . isTLS ? 'Yes' : 'No' } ` ) ;
2025-03-05 17:46:25 +00:00
} 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 18:40:42 +00:00
// Apply enhanced TCP options if available
if ( this . settings . enableKeepAliveProbes ) {
try {
// These are platform-specific and may not be available
if ( 'setKeepAliveProbes' in socket ) {
( socket as any ) . setKeepAliveProbes ( 10 ) ;
}
if ( 'setKeepAliveInterval' in socket ) {
( socket as any ) . setKeepAliveInterval ( 1000 ) ;
}
} catch ( err ) {
// Ignore errors - these are optional enhancements
}
}
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
bytesReceived : 0 ,
bytesSent : 0 ,
remoteIP : remoteIP ,
localPort : localPort ,
2025-03-05 18:40:42 +00:00
isTLS : false ,
tlsHandshakeComplete : false ,
hasReceivedInitialData : false
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 18:40:42 +00:00
} , this . settings . initialDataTimeout ! ) ;
// Make sure timeout doesn't keep the process alive
if ( initialTimeout . unref ) {
initialTimeout . unref ( ) ;
}
2025-03-03 01:57:52 +00:00
} else {
2025-03-03 01:50:30 +00:00
initialDataReceived = true ;
2025-03-05 18:40:42 +00:00
connectionRecord . hasReceivedInitialData = true ;
2025-03-03 01:50:30 +00:00
}
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 ) ;
2025-03-05 18:40:42 +00:00
// Check for TLS handshake if this is the first chunk
if ( ! connectionRecord . isTLS && isTlsHandshake ( chunk ) ) {
connectionRecord . isTLS = true ;
2025-03-05 17:46:25 +00:00
2025-03-05 18:40:42 +00:00
if ( this . settings . enableTlsDebugLogging ) {
console . log ( ` [ ${ connectionId } ] TLS handshake detected from ${ remoteIP } , ${ chunk . length } bytes ` ) ;
// Try to extract SNI and log detailed debug info
extractSNI ( chunk , true ) ;
2025-03-05 17:46:25 +00:00
}
}
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 18:40:42 +00:00
// 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 ` ) ;
}
2025-03-05 17:46:25 +00:00
}
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-05 18:40:42 +00:00
// Save domain config in connection record
connectionRecord . domainConfig = domainConfig ;
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 ;
2025-03-05 18:40:42 +00:00
// 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 ` ) ;
}
2025-03-05 17:46:25 +00:00
}
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
2025-03-05 18:40:42 +00:00
// Apply enhanced TCP options if available
if ( this . settings . enableKeepAliveProbes ) {
try {
if ( 'setKeepAliveProbes' in targetSocket ) {
( targetSocket as any ) . setKeepAliveProbes ( 10 ) ;
}
if ( 'setKeepAliveInterval' in targetSocket ) {
( targetSocket as any ) . setKeepAliveInterval ( 1000 ) ;
}
} catch ( err ) {
// Ignore errors - these are optional enhancements
}
}
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 18:40:42 +00:00
console . log ( ` [ ${ connectionId } ] Timeout on incoming side from ${ remoteIP } after ${ plugins . prettyMs ( this . settings . socketTimeout || 3600000 ) } ` ) ;
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 18:40:42 +00:00
console . log ( ` [ ${ connectionId } ] Timeout on outgoing side from ${ remoteIP } after ${ plugins . prettyMs ( this . settings . socketTimeout || 3600000 ) } ` ) ;
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
2025-03-05 18:40:42 +00:00
socket . setTimeout ( this . settings . socketTimeout || 3600000 ) ;
targetSocket . setTimeout ( this . settings . socketTimeout || 3600000 ) ;
2025-03-05 17:46:25 +00:00
// 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 ( ', ' ) } ) ` : '' } ` +
2025-03-05 18:40:42 +00:00
` TLS: ${ connectionRecord . isTLS ? 'Yes' : 'No' } `
2025-03-05 17:46:25 +00:00
) ;
} 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 ( ', ' ) } ) ` : '' } ` +
2025-03-05 18:40:42 +00:00
` TLS: ${ connectionRecord . isTLS ? 'Yes' : 'No' } `
2025-03-05 17:46:25 +00:00
) ;
} 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
2025-03-05 18:40:42 +00:00
const newSNI = extractSNI ( renegChunk , this . settings . enableTlsDebugLogging ) ;
2025-03-05 14:33:09 +00:00
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
2025-03-05 18:40:42 +00:00
// Set connection timeout
2025-03-05 17:46:25 +00:00
if ( connectionRecord . cleanupTimer ) {
clearTimeout ( connectionRecord . cleanupTimer ) ;
}
2025-03-05 18:40:42 +00:00
// Set timeout based on domain config or default
const connectionTimeout = this . getConnectionTimeout ( connectionRecord ) ;
2025-03-03 01:42:16 +00:00
connectionRecord . cleanupTimer = setTimeout ( ( ) = > {
2025-03-05 18:40:42 +00:00
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 } ` ) ;
}
}
2025-03-05 17:46:25 +00:00
} ) ;
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 ) {
2025-03-05 18:40:42 +00:00
if ( this . settings . defaultAllowedIPs && this . settings . defaultAllowedIPs . length > 0 && ! 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-05 18:40:42 +00:00
// 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 ` ) ;
}
serverName = extractSNI ( chunk , this . settings . enableTlsDebugLogging ) || '' ;
}
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-05 18:40:42 +00:00
connectionRecord . hasReceivedInitialData = true ;
if ( this . settings . defaultAllowedIPs && this . settings . defaultAllowedIPs . length > 0 && ! isAllowed ( remoteIP , this . settings . defaultAllowedIPs ) ) {
2025-03-03 01:57:52 +00:00
return rejectIncomingConnection ( 'rejected' , ` Connection rejected: IP ${ remoteIP } not allowed for non-SNI connection ` ) ;
2025-02-21 23:05:17 +00:00
}
2025-03-05 18:40:42 +00:00
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 tlsConnections = 0 ;
2025-03-05 18:40:42 +00:00
let nonTlsConnections = 0 ;
let completedTlsHandshakes = 0 ;
let pendingTlsHandshakes = 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 18:40:42 +00:00
// Track connection stats
if ( record . isTLS ) {
tlsConnections ++ ;
if ( record . tlsHandshakeComplete ) {
completedTlsHandshakes ++ ;
} else {
pendingTlsHandshakes ++ ;
}
} else {
nonTlsConnections ++ ;
2025-03-05 17:46:25 +00:00
}
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 18:40:42 +00:00
// 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 ) } ` ) ;
}
2025-03-05 17:46:25 +00:00
// Skip inactivity check if disabled
if ( ! this . settings . disableInactivityCheck ) {
2025-03-05 18:40:42 +00:00
// Inactivity check with configurable timeout
const inactivityThreshold = this . settings . inactivityTimeout ! ;
2025-03-05 17:46:25 +00:00
const inactivityTime = now - record . lastActivity ;
if ( inactivityTime > inactivityThreshold && ! record . connectionClosed ) {
2025-03-05 18:40:42 +00:00
console . log ( ` [ ${ id } ] Inactivity check: No activity on connection from ${ record . remoteIP } for ${ plugins . prettyMs ( inactivityTime ) } . ` ) ;
2025-03-05 17:46:25 +00:00
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 } . ` +
2025-03-05 18:40:42 +00:00
` Types: TLS= ${ tlsConnections } (Completed= ${ completedTlsHandshakes } , Pending= ${ pendingTlsHandshakes } ), Non-TLS= ${ nonTlsConnections } . ` +
2025-03-05 17:46:25 +00:00
` 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 18:40:42 +00:00
} , this . settings . inactivityCheckInterval || 60000 ) ;
2025-03-05 17:46:25 +00:00
// 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
}