@ -95,7 +95,7 @@ interface IConnectionRecord {
}
export class PortProxy {
netServer : plugins.net.Server ;
private netServers : plugins.net.Server [ ] = [ ] ;
settings : IPortProxySettings ;
// Unified record tracking each connection pair.
private connectionRecords : Set < IConnectionRecord > = new Set ( ) ;
@ -122,43 +122,8 @@ export class PortProxy {
}
public async start() {
// Helper to forcefully destroy socke ts.
const cleanUpSockets = ( socketA : plugins.net.Socket , socketB? : plugins.net.Socket ) = > {
if ( ! socketA . destroyed ) socketA . destroy ( ) ;
if ( socketB && ! socketB . destroyed ) socketB . destroy ( ) ;
} ;
// Normalize an IP to include both IPv4 and IPv6 representations.
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 ] ;
} ;
// Check if a given IP matches any of the glob patterns.
const isAllowed = ( ip : string , patterns : string [ ] ) : boolean = > {
const normalizedIPVariants = normalizeIP ( ip ) ;
const expandedPatterns = patterns . flatMap ( normalizeIP ) ;
return normalizedIPVariants . some ( ipVariant = >
expandedPatterns . some ( pattern = > plugins . minimatch ( ipVariant , pattern ) )
) ;
} ;
// 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 ) ;
} ;
// Find a matching domain config based on SNI (fallback when port ranges aren’ t used)
const findMatchingDomain = ( serverName : string ) : IDomainConfig | undefined = >
this . settings . domains . find ( config = > plugins . minimatch ( serverName , config . domain ) ) ;
this . netServer = plugins . net . createServer ( ( socket : plugins.net.Socket ) = > {
// Define a unified connection handler for all listening por ts.
const connectionHandler = ( socket : plugins.net.Socket ) = > {
const remoteIP = socket . remoteAddress || '' ;
const localPort = socket . localPort ; // The port on which this connection was accepted.
const connectionRecord : IConnectionRecord = {
@ -181,7 +146,8 @@ export class PortProxy {
if ( connectionRecord . cleanupTimer ) {
clearTimeout ( connectionRecord . cleanupTimer ) ;
}
cleanUpS ockets ( connectionRecord . incoming , connectionRecord . outgoing || undefined ) ;
if ( ! s ocket. destroyed ) socket . destroy ( ) ;
if ( connectionRecord . outgoing && ! connectionRecord . outgoing . destroyed ) connectionRecord . outgoing . destroy ( ) ;
this . connectionRecords . delete ( connectionRecord ) ;
console . log ( ` Connection from ${ remoteIP } terminated. Active connections: ${ this . connectionRecords . size } ` ) ;
}
@ -244,7 +210,7 @@ export class PortProxy {
*/
const setupConnection = ( serverName : string , initialChunk? : Buffer , forcedDomain? : IDomainConfig ) = > {
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
const domainConfig = forcedDomain ? forcedDomain : ( serverName ? findMatchingDomain ( serverName ) : undefined ) ;
const domainConfig = forcedDomain ? forcedDomain : ( serverName ? this . settings . domains . find ( config = > plugins . minimatch ( serverName , config . domain ) ) : undefined ) ;
const defaultAllowed = this . settings . defaultAllowedIPs && isAllowed ( remoteIP , this . settings . defaultAllowedIPs ) ;
if ( ! defaultAllowed && serverName && ! forcedDomain ) {
@ -344,15 +310,10 @@ export class PortProxy {
} ;
// --- PORT RANGE-BASED HANDLING ---
// If g lob al port ranges are defined, enforce port-based routing and ignore SNI .
// If the loc al port is one of the globally listened ports, we may have port-based rules .
if ( this . settings . globalPortRanges && this . settings . globalPortRanges . length > 0 ) {
if ( ! isPortInRanges ( localPort , this . settings . globalPortRanges ) ) {
console . log ( ` Connection from ${ remoteIP } rejected: port ${ localPort } is not in global allowed ranges. ` ) ;
socket . destroy ( ) ;
return ;
}
// If forwardAllGlobalRanges is enabled, always forward using the global targetIP.
if ( this . settings . forwardAllGlobalRanges ) {
// Forward connection to the global targetIP regardless of domain config.
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 ( ) ;
@ -367,30 +328,26 @@ export class PortProxy {
} ) ;
return ;
} else {
// F ind a matching domain config based on the incoming local port.
// Attempt to f ind a matching forced domain config based on the local port.
const forcedDomain = this . settings . domains . find (
domain = > domain . portRanges && domain . portRanges . length > 0 && isPortInRanges ( localPort , domain . portRanges )
) ;
if ( ! forcedDomain ) {
console . log ( ` Connection from ${ remoteIP } rejected: port ${ localPort } not configured in any domain's portRanges. ` ) ;
socket . destroy ( ) ;
if ( forcedDomain ) {
const defaultAllowed = this . settings . defaultAllowedIPs && isAllowed ( remoteIP , this . settings . defaultAllowedIPs ) ;
if ( ! defaultAllowed && ! isAllowed ( remoteIP , forcedDomain . allowedIPs ) ) {
console . log ( ` Connection from ${ remoteIP } rejected: IP not allowed for domain ${ forcedDomain . domain } on port ${ localPort } . ` ) ;
socket . end ( ) ;
return ;
}
console . log ( ` Port-based connection from ${ remoteIP } on port ${ localPort } matched domain ${ forcedDomain . domain } . ` ) ;
setupConnection ( '' , undefined , forcedDomain ) ;
return ;
}
// Check allowed IPs for the forced domain .
const defaultAllowed = this . settings . defaultAllowedIPs && isAllowed ( remoteIP , this . settings . defaultAllowedIPs ) ;
if ( ! defaultAllowed && ! isAllowed ( remoteIP , forcedDomain . allowedIPs ) ) {
console . log ( ` Connection from ${ remoteIP } rejected: IP not allowed for domain ${ forcedDomain . domain } on port ${ localPort } . ` ) ;
socket . end ( ) ;
return ;
}
console . log ( ` Port-based connection from ${ remoteIP } on port ${ localPort } matched domain ${ forcedDomain . domain } . ` ) ;
// Proceed immediately using the forced domain; ignore SNI.
setupConnection ( '' , undefined , forcedDomain ) ;
return ;
// Fall through to SNI/default handling if no forced domain config is found .
}
}
// --- FALLBACK: SNI-BASED HANDLING (if no global port ranges are defin ed) ---
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabl ed) ---
if ( this . settings . sniEnabled ) {
socket . setTimeout ( 5000 , ( ) = > {
console . log ( ` Initial data timeout for ${ remoteIP } ` ) ;
@ -412,16 +369,36 @@ export class PortProxy {
}
setupConnection ( '' ) ;
}
} )
. on ( 'error' , ( err : Error ) = > {
console . log ( ` Server Error: ${ err . message } ` ) ;
} )
. listen ( this . settings . fromPort , ( ) = > {
console . log (
` PortProxy -> OK: Now l istening on port ${ this . settings . fromPort } ` +
` ${ this . settings . sniEnabled ? ' (SNI passthrough enabled)' : '' } `
) ;
} ;
// --- SETUP LISTENERS ---
// Determine which ports to listen on.
const listeningPorts = new Set < number > ( ) ;
if ( this . settings . globalPortRanges && this . settings . globalPortRanges . length > 0 ) {
// L isten 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)' : '' } ` ) ;
} ) ;
this . netServers . push ( server ) ;
}
// Log active connection count and longest running durations every 10 seconds.
this . connectionLogger = setInterval ( ( ) = > {
@ -444,14 +421,41 @@ export class PortProxy {
}
public async stop() {
const done = plugins . smartpromise . defer ( ) ;
this . netServer . close ( ( ) = > {
done . resolve ( ) ;
} ) ;
// Close all servers.
const closePromises : Promise < void > [ ] = this . netServers . map (
server = >
new Promise < void > ( ( resolve ) = > {
server . close ( ( ) = > resolve ( ) ) ;
} )
) ;
if ( this . connectionLogger ) {
clearInterval ( this . connectionLogger ) ;
this . connectionLogger = null ;
}
await done . p romise;
await Promise . all ( closeP romises ) ;
}
}
}
// 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 ) )
) ;
} ;