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 12:25:48 +00:00
/** Domain configuration with per‐ domain allowed port ranges */
2025-02-21 20:17:35 +00:00
export interface IDomainConfig {
2025-02-27 15:46:14 +00:00
domain : string | string [ ] ; // Glob pattern or patterns for domain(s)
allowedIPs : string [ ] ; // Glob patterns for allowed IPs
targetIP? : string ; // Optional target IP for this domain
2025-02-27 12:41:20 +00:00
portRanges? : Array < { from : number ; to : number } > ; // Optional domain-specific allowed port ranges
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-21 20:17:35 +00:00
domains : IDomainConfig [ ] ;
2025-02-21 15:14:02 +00:00
sniEnabled? : boolean ;
2025-02-23 17:30:41 +00:00
defaultAllowedIPs? : string [ ] ;
preserveSourceIP? : boolean ;
2025-02-27 12:25:48 +00:00
maxConnectionLifetime? : number ; // (ms) force cleanup of long-lived connections
globalPortRanges : Array < { from : number ; to : number } > ; // Global allowed port ranges
2025-02-27 12:41:20 +00:00
forwardAllGlobalRanges? : boolean ; // When true, forwards all connections on global port ranges to the global targetIP
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-02-23 17:38:22 +00:00
interface IConnectionRecord {
incoming : plugins.net.Socket ;
outgoing : plugins.net.Socket | null ;
incomingStartTime : number ;
outgoingStartTime? : number ;
2025-02-27 19:57:27 +00:00
lockedDomain? : string ; // New field to lock this connection to the initial SNI
2025-02-23 17:38:22 +00:00
connectionClosed : boolean ;
2025-02-26 10:29:21 +00:00
cleanupTimer? : NodeJS.Timeout ; // Timer to force cleanup after max lifetime/inactivity
2025-02-23 17:38:22 +00:00
}
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-02-23 17:38:22 +00:00
// Unified record tracking each connection pair.
private connectionRecords : Set < IConnectionRecord > = new Set ( ) ;
2025-02-21 23:05:17 +00:00
private connectionLogger : NodeJS.Timeout | null = null ;
2022-07-29 00:49:46 +02:00
2025-02-22 05:46:30 +00:00
private terminationStats : {
incoming : Record < string , number > ;
outgoing : Record < string , number > ;
} = {
incoming : { } ,
outgoing : { } ,
} ;
2025-02-26 10:29:21 +00:00
constructor ( settingsArg : IPortProxySettings ) {
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-02-26 12:56:00 +00:00
maxConnectionLifetime : settingsArg.maxConnectionLifetime || 600000 ,
2025-02-21 17:01:02 +00:00
} ;
2022-07-29 00:49:46 +02:00
}
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
}
2022-07-29 01:52:34 +02:00
public async start() {
2025-02-27 13:04:01 +00:00
// Define a unified connection handler for all listening ports.
const connectionHandler = ( socket : plugins.net.Socket ) = > {
2025-02-21 20:17:35 +00:00
const remoteIP = socket . remoteAddress || '' ;
2025-02-27 12:25:48 +00:00
const localPort = socket . localPort ; // The port on which this connection was accepted.
2025-02-23 17:38:22 +00:00
const connectionRecord : IConnectionRecord = {
incoming : socket ,
outgoing : null ,
incomingStartTime : Date.now ( ) ,
connectionClosed : false ,
} ;
this . connectionRecords . add ( connectionRecord ) ;
2025-02-27 12:25:48 +00:00
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-02-23 17:38:22 +00:00
// Ensure cleanup happens only once for the entire connection record.
2025-02-27 15:05:38 +00:00
const cleanupOnce = async ( ) = > {
2025-02-23 17:38:22 +00:00
if ( ! connectionRecord . connectionClosed ) {
connectionRecord . connectionClosed = true ;
2025-02-26 10:29:21 +00:00
if ( connectionRecord . cleanupTimer ) {
clearTimeout ( connectionRecord . cleanupTimer ) ;
}
2025-02-27 13:04:01 +00:00
if ( ! socket . destroyed ) socket . destroy ( ) ;
if ( connectionRecord . outgoing && ! connectionRecord . outgoing . destroyed ) connectionRecord . outgoing . destroy ( ) ;
2025-02-23 17:38:22 +00:00
this . connectionRecords . delete ( connectionRecord ) ;
console . log ( ` Connection from ${ remoteIP } terminated. Active connections: ${ this . connectionRecords . size } ` ) ;
2025-02-21 23:05:17 +00:00
}
} ;
2025-02-23 17:30:41 +00:00
// Helper to reject an incoming connection.
const rejectIncomingConnection = ( reason : string , logMessage : string ) = > {
console . log ( logMessage ) ;
socket . end ( ) ;
if ( incomingTerminationReason === null ) {
incomingTerminationReason = reason ;
this . incrementTerminationStat ( 'incoming' , reason ) ;
}
cleanupOnce ( ) ;
} ;
socket . on ( 'error' , ( err : Error ) = > {
const errorMessage = initialDataReceived
? ` (Immediate) Incoming socket error from ${ remoteIP } : ${ err . message } `
: ` (Premature) Incoming socket error from ${ remoteIP } before data received: ${ err . message } ` ;
console . log ( errorMessage ) ;
} ) ;
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-02-21 23:05:17 +00:00
if ( code === 'ECONNRESET' ) {
2025-02-22 05:46:30 +00:00
reason = 'econnreset' ;
2025-02-21 23:05:17 +00:00
console . log ( ` ECONNRESET on ${ side } side from ${ remoteIP } : ${ err . message } ` ) ;
} else {
console . log ( ` Error on ${ side } side from ${ remoteIP } : ${ err . message } ` ) ;
}
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-02-21 23:05:17 +00:00
cleanupOnce ( ) ;
} ;
const handleClose = ( side : 'incoming' | 'outgoing' ) = > ( ) = > {
console . log ( ` 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-02-21 23:05:17 +00:00
cleanupOnce ( ) ;
} ;
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-02-27 14:23:44 +00:00
* @param overridePort - If provided , use this port for the outgoing connection ( typically the same as the incoming port ) .
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-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 15:46:14 +00:00
: ( serverName ? this . settings . domains . find ( config = > {
if ( typeof config . domain === 'string' ) {
return plugins . minimatch ( serverName , config . domain ) ;
} else {
return config . domain . some ( d = > plugins . minimatch ( serverName , d ) ) ;
}
} ) : undefined ) ;
2025-02-23 17:30:41 +00:00
2025-02-27 15:41:03 +00:00
// If a matching domain config exists, check its allowedIPs.
2025-02-27 15:32:06 +00:00
if ( domainConfig ) {
2025-02-21 23:05:17 +00:00
if ( ! isAllowed ( remoteIP , domainConfig . allowedIPs ) ) {
2025-02-27 15:46:14 +00:00
return rejectIncomingConnection ( 'rejected' , ` Connection rejected: IP ${ remoteIP } not allowed for domain ${ Array . isArray ( domainConfig . domain ) ? domainConfig . domain . join ( ', ' ) : domainConfig . domain } ` ) ;
2025-02-27 15:32:06 +00:00
}
} else if ( this . settings . defaultAllowedIPs ) {
2025-02-27 15:41:03 +00:00
// Only check default allowed IPs if no domain config matched.
2025-02-27 15:32:06 +00:00
if ( ! isAllowed ( remoteIP , this . settings . defaultAllowedIPs ) ) {
return rejectIncomingConnection ( 'rejected' , ` Connection rejected: IP ${ remoteIP } not allowed by default allowed list ` ) ;
2025-02-22 05:46:30 +00:00
}
2025-02-21 18:54:40 +00:00
}
2025-02-27 12:41:20 +00:00
const targetHost = domainConfig ? . targetIP || 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-02-23 17:38:22 +00:00
const targetSocket = plugins . net . connect ( connectionOptions ) ;
connectionRecord . outgoing = targetSocket ;
connectionRecord . outgoingStartTime = Date . now ( ) ;
2025-02-23 17:30:41 +00:00
console . log (
2025-02-27 14:23:44 +00:00
` Connection established: ${ remoteIP } -> ${ targetHost } : ${ connectionOptions . port } ` +
2025-02-27 15:46:14 +00:00
` ${ serverName ? ` (SNI: ${ serverName } ) ` : forcedDomain ? ` (Port-based for domain: ${ Array . isArray ( forcedDomain . domain ) ? forcedDomain . domain . join ( ', ' ) : forcedDomain . domain } ) ` : '' } `
2025-02-23 17:30:41 +00:00
) ;
2025-02-21 23:05:17 +00:00
if ( initialChunk ) {
socket . unshift ( initialChunk ) ;
}
2025-02-21 20:17:35 +00:00
socket . setTimeout ( 120000 ) ;
2025-02-23 17:30:41 +00:00
socket . pipe ( targetSocket ) ;
targetSocket . pipe ( socket ) ;
2025-02-21 23:05:17 +00:00
2025-02-26 10:29:21 +00:00
// Attach error and close handlers.
2025-02-21 23:05:17 +00:00
socket . on ( 'error' , handleError ( 'incoming' ) ) ;
2025-02-23 17:30:41 +00:00
targetSocket . on ( 'error' , handleError ( 'outgoing' ) ) ;
2025-02-21 23:05:17 +00:00
socket . on ( 'close' , handleClose ( 'incoming' ) ) ;
2025-02-23 17:30:41 +00:00
targetSocket . on ( 'close' , handleClose ( 'outgoing' ) ) ;
2025-02-21 23:33:15 +00:00
socket . on ( 'timeout' , ( ) = > {
console . log ( ` Timeout on incoming side from ${ remoteIP } ` ) ;
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-02-21 23:33:15 +00:00
cleanupOnce ( ) ;
} ) ;
2025-02-23 17:30:41 +00:00
targetSocket . on ( 'timeout' , ( ) = > {
2025-02-21 23:33:15 +00:00
console . log ( ` Timeout on outgoing side from ${ remoteIP } ` ) ;
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-02-21 23:33:15 +00:00
cleanupOnce ( ) ;
} ) ;
2025-02-21 23:05:17 +00:00
socket . on ( 'end' , handleClose ( 'incoming' ) ) ;
2025-02-23 17:30:41 +00:00
targetSocket . on ( 'end' , handleClose ( 'outgoing' ) ) ;
2025-02-26 10:29:21 +00:00
2025-02-27 12:25:48 +00:00
// Initialize a cleanup timer for max connection lifetime.
2025-02-26 10:29:21 +00:00
if ( this . settings . maxConnectionLifetime ) {
let incomingActive = false ;
let outgoingActive = false ;
const resetCleanupTimer = ( ) = > {
if ( this . settings . maxConnectionLifetime ) {
if ( connectionRecord . cleanupTimer ) {
clearTimeout ( connectionRecord . cleanupTimer ) ;
}
connectionRecord . cleanupTimer = setTimeout ( ( ) = > {
console . log ( ` Connection from ${ remoteIP } exceeded max lifetime with inactivity ( ${ this . settings . maxConnectionLifetime } ms), forcing cleanup. ` ) ;
cleanupOnce ( ) ;
} , this . settings . maxConnectionLifetime ) ;
}
} ;
resetCleanupTimer ( ) ;
socket . on ( 'data' , ( ) = > {
incomingActive = true ;
if ( incomingActive && outgoingActive ) {
resetCleanupTimer ( ) ;
2025-02-26 19:00:09 +00:00
incomingActive = false ;
outgoingActive = false ;
2025-02-26 10:29:21 +00:00
}
} ) ;
targetSocket . on ( 'data' , ( ) = > {
outgoingActive = true ;
if ( incomingActive && outgoingActive ) {
resetCleanupTimer ( ) ;
2025-02-26 19:00:09 +00:00
incomingActive = false ;
outgoingActive = false ;
2025-02-26 10:29:21 +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 ) {
if ( this . settings . defaultAllowedIPs && ! isAllowed ( remoteIP , this . settings . defaultAllowedIPs ) ) {
console . log ( ` Connection from ${ remoteIP } rejected: IP ${ remoteIP } not allowed in global default allowed list. ` ) ;
socket . end ( ) ;
return ;
}
console . log ( ` Port-based connection from ${ remoteIP } on port ${ localPort } forwarded to global target IP ${ this . settings . targetIP } . ` ) ;
setupConnection ( '' , undefined , {
domain : 'global' ,
allowedIPs : this.settings.defaultAllowedIPs || [ ] ,
targetIP : this.settings.targetIP ,
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 12:41:20 +00:00
const forcedDomain = this . settings . domains . find (
domain = > domain . portRanges && domain . portRanges . length > 0 && isPortInRanges ( localPort , domain . portRanges )
) ;
2025-02-27 12:54:14 +00:00
if ( forcedDomain ) {
2025-02-27 15:32:06 +00:00
if ( ! isAllowed ( remoteIP , forcedDomain . allowedIPs ) ) {
2025-02-27 15:46:14 +00:00
console . log ( ` Connection from ${ remoteIP } rejected: IP not allowed for domain ${ Array . isArray ( forcedDomain . domain ) ? forcedDomain . domain . join ( ', ' ) : forcedDomain . domain } on port ${ localPort } . ` ) ;
2025-02-27 12:54:14 +00:00
socket . end ( ) ;
return ;
}
2025-02-27 15:46:14 +00:00
console . log ( ` Port-based connection from ${ remoteIP } on port ${ localPort } matched domain ${ Array . isArray ( forcedDomain . domain ) ? forcedDomain . domain . join ( ', ' ) : forcedDomain . domain } . ` ) ;
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-02-23 11:43:21 +00:00
socket . setTimeout ( 5000 , ( ) = > {
console . log ( ` Initial data timeout for ${ remoteIP } ` ) ;
socket . end ( ) ;
cleanupOnce ( ) ;
} ) ;
2025-02-21 23:05:17 +00:00
socket . once ( 'data' , ( chunk : Buffer ) = > {
2025-02-23 11:43:21 +00:00
socket . setTimeout ( 0 ) ;
2025-02-21 23:11:13 +00:00
initialDataReceived = true ;
2025-02-21 23:05:17 +00:00
const serverName = extractSNI ( chunk ) || '' ;
2025-02-27 19:57:27 +00:00
// Lock the connection to the negotiated SNI.
connectionRecord . lockedDomain = serverName ;
2025-02-21 23:05:17 +00:00
console . log ( ` Received connection from ${ remoteIP } with SNI: ${ serverName } ` ) ;
2025-02-27 19:57:27 +00:00
// Add an extra data listener to check for a renegotiated ClientHello.
socket . on ( 'data' , ( chunk : Buffer ) = > {
if ( chunk . length > 0 && chunk . readUInt8 ( 0 ) === 22 ) {
const newSNI = extractSNI ( chunk ) ;
if ( newSNI && newSNI !== connectionRecord . lockedDomain ) {
console . log ( ` Rehandshake detected with different SNI: ${ newSNI } vs locked ${ connectionRecord . lockedDomain } . Terminating connection. ` ) ;
cleanupOnce ( ) ;
}
}
} ) ;
2025-02-21 23:05:17 +00:00
setupConnection ( serverName , chunk ) ;
} ) ;
} else {
2025-02-21 23:11:13 +00:00
initialDataReceived = true ;
2025-02-21 23:05:17 +00:00
if ( ! this . settings . defaultAllowedIPs || ! isAllowed ( remoteIP , this . settings . defaultAllowedIPs ) ) {
2025-02-23 17:30:41 +00:00
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 ) ;
}
}
// Also ensure the default fromPort is listened to if it isn’ t already in the ranges.
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-02-27 12:25:48 +00:00
// Log active connection count and longest running durations every 10 seconds.
2025-02-21 23:05:17 +00:00
this . connectionLogger = setInterval ( ( ) = > {
2025-02-21 23:18:17 +00:00
const now = Date . now ( ) ;
let maxIncoming = 0 ;
let maxOutgoing = 0 ;
2025-02-23 17:38:22 +00:00
for ( const record of this . connectionRecords ) {
maxIncoming = Math . max ( maxIncoming , now - record . incomingStartTime ) ;
if ( record . outgoingStartTime ) {
maxOutgoing = Math . max ( maxOutgoing , now - record . outgoingStartTime ) ;
}
2025-02-21 23:18:17 +00:00
}
2025-02-23 17:30:41 +00:00
console . log (
2025-02-23 17:38:22 +00:00
` (Interval Log) Active connections: ${ this . connectionRecords . size } . ` +
2025-02-23 17:30:41 +00:00
` Longest running incoming: ${ plugins . prettyMs ( maxIncoming ) } , outgoing: ${ plugins . prettyMs ( maxOutgoing ) } . ` +
` Termination stats (incoming): ${ JSON . stringify ( this . terminationStats . incoming ) } , ` +
` (outgoing): ${ JSON . stringify ( this . terminationStats . outgoing ) } `
) ;
2025-02-21 23:05:17 +00:00
} , 10000 ) ;
2022-07-29 00:49:46 +02:00
}
public async stop() {
2025-02-27 13:04:01 +00:00
// Close all servers.
const closePromises : Promise < void > [ ] = this . netServers . map (
server = >
new Promise < void > ( ( resolve ) = > {
server . close ( ( ) = > resolve ( ) ) ;
} )
) ;
2025-02-21 23:05:17 +00:00
if ( this . connectionLogger ) {
clearInterval ( this . connectionLogger ) ;
this . connectionLogger = null ;
}
2025-02-27 13:04:01 +00:00
await Promise . all ( closePromises ) ;
2022-07-29 00:49:46 +02:00
}
2025-02-27 13:04:01 +00:00
}
// Helper: Check if a port falls within any of the given port ranges.
const isPortInRanges = ( port : number , ranges : Array < { from : number ; to : number } > ) : boolean = > {
return ranges . some ( range = > port >= range . from && port <= range . to ) ;
} ;
// Helper: Check if a given IP matches any of the glob patterns.
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 ) )
) ;
} ;