@ -1,20 +1,25 @@
import * as plugins from './plugins.js' ;
/** Domain configuration with per‐ domain allowed port ranges */
export interface IDomainConfig {
domain : string ; // Glob pattern for domain
allowedIPs : string [ ] ; // Glob patterns for allowed IPs
targetIP? : string ; // Optional target IP for this domain
portRanges? : Array < { from : number ; to : number } > ; // Optional domain-specific allowed port ranges
}
/** Port proxy settings including global allowed port ranges */
export interface IPortProxySettings extends plugins . tls . TlsOptions {
fromPort : number ;
toPort : number ;
toHost ? : string ; // T arget host to proxy to, defaults to 'localhost'
targetIP ? : string ; // Global t arget host to proxy to, defaults to 'localhost'
domains : IDomainConfig [ ] ;
sniEnabled? : boolean ;
defaultAllowedIPs? : string [ ] ;
preserveSourceIP? : boolean ;
maxConnectionLifetime? : number ; // New option (in milliseconds) to force cleanup of long-lived connections
maxConnectionLifetime? : number ; // (ms) force cleanup of long-lived connections
globalPortRanges : Array < { from : number ; to : number } > ; // Global allowed port ranges
forwardAllGlobalRanges? : boolean ; // When true, forwards all connections on global port ranges to the global targetIP
}
/**
@ -107,7 +112,7 @@ export class PortProxy {
constructor ( settingsArg : IPortProxySettings ) {
this . settings = {
. . . settingsArg ,
toHost : settingsArg.toHost || 'localhost' ,
targetIP : settingsArg.targetIP || 'localhost' ,
maxConnectionLifetime : settingsArg.maxConnectionLifetime || 600000 ,
} ;
}
@ -144,12 +149,18 @@ export class PortProxy {
) ;
} ;
// Find a matching domain config based on the SNI .
// 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 ) = > {
const remoteIP = socket . remoteAddress || '' ;
const localPort = socket . localPort ; // The port on which this connection was accepted.
const connectionRecord : IConnectionRecord = {
incoming : socket ,
outgoing : null ,
@ -157,7 +168,7 @@ export class PortProxy {
connectionClosed : false ,
} ;
this . connectionRecords . add ( connectionRecord ) ;
console . log ( ` New connection from ${ remoteIP } . Active connections: ${ this . connectionRecords . size } ` ) ;
console . log ( ` New connection from ${ remoteIP } on port ${ localPort } . Active connections: ${ this . connectionRecords . size } ` ) ;
let initialDataReceived = false ;
let incomingTerminationReason : string | null = null ;
@ -225,25 +236,28 @@ export class PortProxy {
cleanupOnce ( ) ;
} ;
const setupConnection = ( serverName : string , initialChunk? : Buffer ) = > {
/**
* 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).
*/
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 defaultAllowed = this . settings . defaultAllowedIPs && isAllowed ( remoteIP , this . settings . defaultAllowedIPs ) ;
if ( ! defaultAllowed && serverName ) {
const domainConfig = findMatchingDomain ( serverName ) ;
if ( ! defaultAllowed && serverName && ! forcedDomain ) {
if ( ! domainConfig ) {
return rejectIncomingConnection ( 'rejected' , ` Connection rejected: No matching domain config for ${ serverName } from ${ remoteIP } ` ) ;
}
if ( ! isAllowed ( remoteIP , domainConfig . allowedIPs ) ) {
return rejectIncomingConnection ( 'rejected' , ` Connection rejected: IP ${ remoteIP } not allowed for domain ${ serverName } ` ) ;
}
} else if ( ! defaultAllowed && ! serverName ) {
return rejectIncomingConnection ( 'rejected' , ` Connection rejected: No SNI and IP ${ remoteIP } not in default allowed list ` ) ;
} else if ( defaultAllowed && ! serverName ) {
console . log ( ` Connection allowed: IP ${ remoteIP } is in default allowed list ` ) ;
}
const domainConfig = serverName ? findMatchingDomain ( serverName ) : undefined ;
const targetHost = domainConfig ? . targetIP || this . settings . toHost ! ;
const targetHost = domainConfig ? . targetIP || this . settings . targetIP ! ;
const connectionOptions : plugins.net.NetConnectOpts = {
host : targetHost ,
port : this.settings.toPort ,
@ -258,7 +272,7 @@ export class PortProxy {
console . log (
` Connection established: ${ remoteIP } -> ${ targetHost } : ${ this . settings . toPort } ` +
` ${ serverName ? ` (SNI: ${ serverName } ) ` : '' } `
` ${ serverName ? ` (SNI: ${ serverName } ) ` : forcedDomain ? ` (Port-based for domain: ${ forcedDomain . domain } ) ` : '' } `
) ;
if ( initialChunk ) {
@ -292,7 +306,7 @@ export class PortProxy {
socket . on ( 'end' , handleClose ( 'incoming' ) ) ;
targetSocket . on ( 'end' , handleClose ( 'outgoing' ) ) ;
// If maxConnectionLifetime is set, initialize a cleanup timer that will be reset on data flow .
// Initialize a cleanup timer for max connection lifetime .
if ( this . settings . maxConnectionLifetime ) {
let incomingActive = false ;
let outgoingActive = false ;
@ -308,10 +322,8 @@ export class PortProxy {
}
} ;
// Start the cleanup timer.
resetCleanupTimer ( ) ;
// Listen for data events on both sides and reset the timer when both are active.
socket . on ( 'data' , ( ) = > {
incomingActive = true ;
if ( incomingActive && outgoingActive ) {
@ -331,6 +343,48 @@ export class PortProxy {
}
} ;
// --- PORT RANGE-BASED HANDLING ---
// Check if the local port falls within any of the global port ranges.
const isLocalPortInGlobalRange =
this . settings . globalPortRanges && isPortInRanges ( localPort , this . settings . globalPortRanges ) ;
if ( isLocalPortInGlobalRange ) {
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 ( ) ;
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 : [ ]
} ) ;
return ;
} else {
// Attempt to find 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 ) {
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 ;
}
// If no forced domain config is found for this port, fall through to SNI/default handling.
}
}
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
if ( this . settings . sniEnabled ) {
socket . setTimeout ( 5000 , ( ) = > {
console . log ( ` Initial data timeout for ${ remoteIP } ` ) ;
@ -363,7 +417,7 @@ export class PortProxy {
) ;
} ) ;
// Every 10 seconds l og active connection count and longest running durations.
// L og active connection count and longest running durations every 10 seconds .
this . connectionLogger = setInterval ( ( ) = > {
const now = Date . now ( ) ;
let maxIncoming = 0 ;