|
|
|
@ -10,10 +10,6 @@ export interface IDomainConfig {
|
|
|
|
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
|
|
|
// Allow domain-specific timeout override
|
|
|
|
|
connectionTimeout?: number; // Connection timeout override (ms)
|
|
|
|
|
|
|
|
|
|
// New properties for NetworkProxy integration
|
|
|
|
|
useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
|
|
|
|
|
networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Port proxy settings including global allowed port ranges */
|
|
|
|
@ -61,7 +57,8 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
|
|
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
|
|
|
|
|
|
|
|
|
// New property for NetworkProxy integration
|
|
|
|
|
networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
|
|
|
|
|
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
|
|
|
|
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -97,9 +94,15 @@ interface IConnectionRecord {
|
|
|
|
|
incomingTerminationReason?: string | null; // Reason for incoming termination
|
|
|
|
|
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
|
|
|
|
|
|
|
|
|
// New field for NetworkProxy tracking
|
|
|
|
|
// NetworkProxy tracking
|
|
|
|
|
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
|
|
|
|
networkProxyIndex?: number; // Which NetworkProxy instance is being used
|
|
|
|
|
|
|
|
|
|
// Renegotiation handler
|
|
|
|
|
renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
|
|
|
|
|
|
|
|
|
|
// Browser connection tracking
|
|
|
|
|
isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
|
|
|
|
|
domainSwitches?: number; // Number of times the domain has been switched on this connection
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -266,6 +269,29 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks if a TLS record is a proper ClientHello message (more accurate than just checking record type)
|
|
|
|
|
* @param buffer - Buffer containing the TLS record
|
|
|
|
|
* @returns true if the buffer contains a proper ClientHello message
|
|
|
|
|
*/
|
|
|
|
|
function isClientHello(buffer: Buffer): boolean {
|
|
|
|
|
try {
|
|
|
|
|
if (buffer.length < 9) return false; // Too small for a proper ClientHello
|
|
|
|
|
|
|
|
|
|
// Check record type (has to be handshake - 22)
|
|
|
|
|
if (buffer.readUInt8(0) !== 22) return false;
|
|
|
|
|
|
|
|
|
|
// After the TLS record header (5 bytes), check the handshake type (1 for ClientHello)
|
|
|
|
|
if (buffer.readUInt8(5) !== 1) return false;
|
|
|
|
|
|
|
|
|
|
// Basic checks passed, this appears to be a ClientHello
|
|
|
|
|
return true;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(`Error checking for ClientHello: ${err}`);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
@ -349,8 +375,8 @@ export class PortProxy {
|
|
|
|
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
|
|
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
|
|
|
|
|
|
|
|
// New property to store NetworkProxy instances
|
|
|
|
|
private networkProxies: NetworkProxy[] = [];
|
|
|
|
|
// NetworkProxy instance for TLS termination
|
|
|
|
|
private networkProxy: NetworkProxy | null = null;
|
|
|
|
|
|
|
|
|
|
constructor(settingsArg: IPortProxySettings) {
|
|
|
|
|
// Set reasonable defaults for all settings
|
|
|
|
@ -370,29 +396,49 @@ export class PortProxy {
|
|
|
|
|
// Socket optimization settings
|
|
|
|
|
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
|
|
|
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
|
|
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness)
|
|
|
|
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
|
|
|
|
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds
|
|
|
|
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB
|
|
|
|
|
|
|
|
|
|
// Feature flags
|
|
|
|
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
|
|
|
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
|
|
|
|
|
? settingsArg.enableKeepAliveProbes : true, // Enable by default
|
|
|
|
|
? settingsArg.enableKeepAliveProbes : true,
|
|
|
|
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
|
|
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
|
|
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
|
|
|
|
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
|
|
|
|
|
|
|
|
|
// Rate limiting defaults
|
|
|
|
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
|
|
|
|
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
|
|
|
|
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
|
|
|
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
|
|
|
|
|
|
|
|
|
|
// Enhanced keep-alive settings
|
|
|
|
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
|
|
|
|
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
|
|
|
|
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
|
|
|
|
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
|
|
|
|
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
|
|
|
|
|
|
|
|
// NetworkProxy settings
|
|
|
|
|
networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Store NetworkProxy instances if provided
|
|
|
|
|
this.networkProxies = settingsArg.networkProxies || [];
|
|
|
|
|
// Initialize NetworkProxy if enabled
|
|
|
|
|
if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
|
|
|
|
|
this.initializeNetworkProxy();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize NetworkProxy instance
|
|
|
|
|
*/
|
|
|
|
|
private initializeNetworkProxy(): void {
|
|
|
|
|
if (!this.networkProxy) {
|
|
|
|
|
this.networkProxy = new NetworkProxy({
|
|
|
|
|
port: this.settings.networkProxyPort!,
|
|
|
|
|
portProxyIntegration: true,
|
|
|
|
|
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -400,51 +446,49 @@ export class PortProxy {
|
|
|
|
|
* @param connectionId - Unique connection identifier
|
|
|
|
|
* @param socket - The incoming client socket
|
|
|
|
|
* @param record - The connection record
|
|
|
|
|
* @param domainConfig - The domain configuration
|
|
|
|
|
* @param initialData - Initial data chunk (TLS ClientHello)
|
|
|
|
|
* @param serverName - SNI hostname (if available)
|
|
|
|
|
*/
|
|
|
|
|
private forwardToNetworkProxy(
|
|
|
|
|
connectionId: string,
|
|
|
|
|
socket: plugins.net.Socket,
|
|
|
|
|
record: IConnectionRecord,
|
|
|
|
|
domainConfig: IDomainConfig,
|
|
|
|
|
initialData: Buffer,
|
|
|
|
|
serverName?: string
|
|
|
|
|
initialData: Buffer
|
|
|
|
|
): void {
|
|
|
|
|
// Determine which NetworkProxy to use
|
|
|
|
|
const proxyIndex = domainConfig.networkProxyIndex !== undefined
|
|
|
|
|
? domainConfig.networkProxyIndex
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
// Validate the NetworkProxy index
|
|
|
|
|
if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
|
|
|
|
|
console.log(`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`);
|
|
|
|
|
// Ensure NetworkProxy is initialized
|
|
|
|
|
if (!this.networkProxy) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] NetworkProxy not initialized. Using fallback direct connection.`
|
|
|
|
|
);
|
|
|
|
|
// Fall back to direct connection
|
|
|
|
|
return this.setupDirectConnection(connectionId, socket, record, domainConfig, serverName, initialData);
|
|
|
|
|
return this.setupDirectConnection(
|
|
|
|
|
connectionId,
|
|
|
|
|
socket,
|
|
|
|
|
record,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
initialData
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const networkProxy = this.networkProxies[proxyIndex];
|
|
|
|
|
const proxyPort = networkProxy.getListeningPort();
|
|
|
|
|
const proxyPort = this.networkProxy.getListeningPort();
|
|
|
|
|
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
|
|
|
|
|
`[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a connection to the NetworkProxy
|
|
|
|
|
const proxySocket = plugins.net.connect({
|
|
|
|
|
host: proxyHost,
|
|
|
|
|
port: proxyPort
|
|
|
|
|
port: proxyPort,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Store the outgoing socket in the record
|
|
|
|
|
record.outgoing = proxySocket;
|
|
|
|
|
record.outgoingStartTime = Date.now();
|
|
|
|
|
record.usingNetworkProxy = true;
|
|
|
|
|
record.networkProxyIndex = proxyIndex;
|
|
|
|
|
|
|
|
|
|
// Set up error handlers
|
|
|
|
|
proxySocket.on('error', (err) => {
|
|
|
|
@ -475,7 +519,9 @@ export class PortProxy {
|
|
|
|
|
|
|
|
|
|
socket.on('close', () => {
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(`[${connectionId}] Client connection closed after forwarding to NetworkProxy`);
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Client connection closed after forwarding to NetworkProxy`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
this.cleanupConnection(record, 'client_closed');
|
|
|
|
|
});
|
|
|
|
@ -486,7 +532,7 @@ export class PortProxy {
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
|
|
|
|
|
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
@ -585,7 +631,9 @@ export class PortProxy {
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Ignore errors - these are optional enhancements
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -642,7 +690,9 @@ export class PortProxy {
|
|
|
|
|
// For keep-alive connections, just log a warning instead of closing
|
|
|
|
|
if (record.hasKeepAlive) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Timeout event on incoming keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
|
|
|
|
|
`[${connectionId}] Timeout event on incoming keep-alive connection from ${
|
|
|
|
|
record.remoteIP
|
|
|
|
|
} after ${plugins.prettyMs(
|
|
|
|
|
this.settings.socketTimeout || 3600000
|
|
|
|
|
)}. Connection preserved.`
|
|
|
|
|
);
|
|
|
|
@ -652,9 +702,9 @@ export class PortProxy {
|
|
|
|
|
|
|
|
|
|
// For non-keep-alive connections, proceed with normal cleanup
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs(
|
|
|
|
|
this.settings.socketTimeout || 3600000
|
|
|
|
|
)}`
|
|
|
|
|
`[${connectionId}] Timeout on incoming side from ${
|
|
|
|
|
record.remoteIP
|
|
|
|
|
} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
|
|
|
|
|
);
|
|
|
|
|
if (record.incomingTerminationReason === null) {
|
|
|
|
|
record.incomingTerminationReason = 'timeout';
|
|
|
|
@ -667,7 +717,9 @@ export class PortProxy {
|
|
|
|
|
// For keep-alive connections, just log a warning instead of closing
|
|
|
|
|
if (record.hasKeepAlive) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
|
|
|
|
|
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${
|
|
|
|
|
record.remoteIP
|
|
|
|
|
} after ${plugins.prettyMs(
|
|
|
|
|
this.settings.socketTimeout || 3600000
|
|
|
|
|
)}. Connection preserved.`
|
|
|
|
|
);
|
|
|
|
@ -677,9 +729,9 @@ export class PortProxy {
|
|
|
|
|
|
|
|
|
|
// For non-keep-alive connections, proceed with normal cleanup
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs(
|
|
|
|
|
this.settings.socketTimeout || 3600000
|
|
|
|
|
)}`
|
|
|
|
|
`[${connectionId}] Timeout on outgoing side from ${
|
|
|
|
|
record.remoteIP
|
|
|
|
|
} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
|
|
|
|
|
);
|
|
|
|
|
if (record.outgoingTerminationReason === null) {
|
|
|
|
|
record.outgoingTerminationReason = 'timeout';
|
|
|
|
@ -695,7 +747,9 @@ export class PortProxy {
|
|
|
|
|
targetSocket.setTimeout(0);
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`);
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Set normal timeouts for other connections
|
|
|
|
@ -725,9 +779,7 @@ export class PortProxy {
|
|
|
|
|
const combinedData = Buffer.concat(record.pendingData);
|
|
|
|
|
targetSocket.write(combinedData, (err) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Error writing pending data to target: ${err.message}`
|
|
|
|
|
);
|
|
|
|
|
console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
|
|
|
|
|
return this.initiateCleanupOnce(record, 'write_error');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -746,7 +798,9 @@ export class PortProxy {
|
|
|
|
|
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
|
|
|
: ''
|
|
|
|
|
}` +
|
|
|
|
|
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
|
|
|
|
|
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
|
|
|
record.hasKeepAlive ? 'Yes' : 'No'
|
|
|
|
|
}`
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(
|
|
|
|
@ -777,7 +831,9 @@ export class PortProxy {
|
|
|
|
|
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
|
|
|
: ''
|
|
|
|
|
}` +
|
|
|
|
|
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
|
|
|
|
|
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
|
|
|
record.hasKeepAlive ? 'Yes' : 'No'
|
|
|
|
|
}`
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(
|
|
|
|
@ -797,30 +853,45 @@ export class PortProxy {
|
|
|
|
|
record.pendingData = [];
|
|
|
|
|
record.pendingDataSize = 0;
|
|
|
|
|
|
|
|
|
|
// Add the renegotiation listener for SNI validation
|
|
|
|
|
// Add the renegotiation handler for SNI validation with strict domain enforcement
|
|
|
|
|
if (serverName) {
|
|
|
|
|
socket.on('data', (renegChunk: Buffer) => {
|
|
|
|
|
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
|
|
|
// Define a handler for checking renegotiation with improved detection
|
|
|
|
|
const renegotiationHandler = (renegChunk: Buffer) => {
|
|
|
|
|
// Only process if this looks like a TLS ClientHello
|
|
|
|
|
if (isClientHello(renegChunk)) {
|
|
|
|
|
try {
|
|
|
|
|
// Try to extract SNI from potential renegotiation
|
|
|
|
|
// Extract SNI from ClientHello
|
|
|
|
|
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
|
|
|
|
if (newSNI && newSNI !== record.lockedDomain) {
|
|
|
|
|
|
|
|
|
|
// Skip if no SNI was found
|
|
|
|
|
if (!newSNI) return;
|
|
|
|
|
|
|
|
|
|
// Handle SNI change during renegotiation - always terminate for domain switches
|
|
|
|
|
if (newSNI !== record.lockedDomain) {
|
|
|
|
|
// Log and terminate the connection for any SNI change
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.`
|
|
|
|
|
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
|
|
|
`Terminating connection - SNI domain switching is not allowed.`
|
|
|
|
|
);
|
|
|
|
|
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
|
|
|
} else if (newSNI && this.settings.enableDetailedLogging) {
|
|
|
|
|
} else if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
|
|
|
|
|
`[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
|
|
|
|
|
`[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Store the handler in the connection record so we can remove it during cleanup
|
|
|
|
|
record.renegotiationHandler = renegotiationHandler;
|
|
|
|
|
|
|
|
|
|
// Add the listener
|
|
|
|
|
socket.on('data', renegotiationHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set connection timeout with simpler logic
|
|
|
|
@ -831,7 +902,9 @@ export class PortProxy {
|
|
|
|
|
// For immortal keep-alive connections, skip setting a timeout completely
|
|
|
|
|
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`);
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
// No cleanup timer for immortal connections
|
|
|
|
|
}
|
|
|
|
@ -842,9 +915,9 @@ export class PortProxy {
|
|
|
|
|
|
|
|
|
|
record.cleanupTimer = setTimeout(() => {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs(
|
|
|
|
|
extendedTimeout
|
|
|
|
|
)}), forcing cleanup.`
|
|
|
|
|
`[${connectionId}] Keep-alive connection from ${
|
|
|
|
|
record.remoteIP
|
|
|
|
|
} exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.`
|
|
|
|
|
);
|
|
|
|
|
this.initiateCleanupOnce(record, 'extended_lifetime');
|
|
|
|
|
}, safeTimeout);
|
|
|
|
@ -855,20 +928,25 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`);
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(
|
|
|
|
|
extendedTimeout
|
|
|
|
|
)}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// For standard connections, use normal timeout
|
|
|
|
|
else {
|
|
|
|
|
// Use domain-specific timeout if available, otherwise use default
|
|
|
|
|
const connectionTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
|
|
|
|
|
const connectionTimeout =
|
|
|
|
|
record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
|
|
|
|
|
const safeTimeout = ensureSafeTimeout(connectionTimeout);
|
|
|
|
|
|
|
|
|
|
record.cleanupTimer = setTimeout(() => {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs(
|
|
|
|
|
connectionTimeout
|
|
|
|
|
)}), forcing cleanup.`
|
|
|
|
|
`[${connectionId}] Connection from ${
|
|
|
|
|
record.remoteIP
|
|
|
|
|
} exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`
|
|
|
|
|
);
|
|
|
|
|
this.initiateCleanupOnce(record, 'connection_timeout');
|
|
|
|
|
}, safeTimeout);
|
|
|
|
@ -973,6 +1051,16 @@ export class PortProxy {
|
|
|
|
|
const bytesReceived = record.bytesReceived;
|
|
|
|
|
const bytesSent = record.bytesSent;
|
|
|
|
|
|
|
|
|
|
// Remove the renegotiation handler if present
|
|
|
|
|
if (record.renegotiationHandler && record.incoming) {
|
|
|
|
|
try {
|
|
|
|
|
record.incoming.removeListener('data', record.renegotiationHandler);
|
|
|
|
|
record.renegotiationHandler = undefined;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(`[${record.id}] Error removing renegotiation handler: ${err}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!record.incoming.destroyed) {
|
|
|
|
|
// Try graceful shutdown first, then force destroy after a short timeout
|
|
|
|
@ -1047,8 +1135,11 @@ export class PortProxy {
|
|
|
|
|
` Duration: ${plugins.prettyMs(
|
|
|
|
|
duration
|
|
|
|
|
)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
|
|
|
|
|
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
|
|
|
|
|
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}`
|
|
|
|
|
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
|
|
|
record.hasKeepAlive ? 'Yes' : 'No'
|
|
|
|
|
}` +
|
|
|
|
|
`${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
|
|
|
|
|
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(
|
|
|
|
@ -1091,7 +1182,10 @@ export class PortProxy {
|
|
|
|
|
console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) {
|
|
|
|
|
if (
|
|
|
|
|
record.incomingTerminationReason === null ||
|
|
|
|
|
record.incomingTerminationReason === undefined
|
|
|
|
|
) {
|
|
|
|
|
record.incomingTerminationReason = reason;
|
|
|
|
|
this.incrementTerminationStat('incoming', reason);
|
|
|
|
|
}
|
|
|
|
@ -1184,6 +1278,12 @@ export class PortProxy {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start NetworkProxy if configured
|
|
|
|
|
if (this.networkProxy) {
|
|
|
|
|
await this.networkProxy.start();
|
|
|
|
|
console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Define a unified connection handler for all listening ports.
|
|
|
|
|
const connectionHandler = (socket: plugins.net.Socket) => {
|
|
|
|
|
if (this.isShuttingDown) {
|
|
|
|
@ -1244,8 +1344,12 @@ export class PortProxy {
|
|
|
|
|
incomingTerminationReason: null,
|
|
|
|
|
outgoingTerminationReason: null,
|
|
|
|
|
|
|
|
|
|
// Initialize NetworkProxy tracking fields
|
|
|
|
|
usingNetworkProxy: false
|
|
|
|
|
// Initialize NetworkProxy tracking
|
|
|
|
|
usingNetworkProxy: false,
|
|
|
|
|
|
|
|
|
|
// Initialize browser connection tracking
|
|
|
|
|
isBrowserConnection: false,
|
|
|
|
|
domainSwitches: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Apply keep-alive settings if enabled
|
|
|
|
@ -1266,7 +1370,9 @@ export class PortProxy {
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Ignore errors - these are optional enhancements
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -1288,8 +1394,63 @@ export class PortProxy {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if this connection should be forwarded directly to NetworkProxy based on port
|
|
|
|
|
const shouldUseNetworkProxy = this.settings.useNetworkProxy &&
|
|
|
|
|
this.settings.useNetworkProxy.includes(localPort);
|
|
|
|
|
|
|
|
|
|
if (shouldUseNetworkProxy) {
|
|
|
|
|
// For NetworkProxy ports, we want to capture the TLS handshake and forward directly
|
|
|
|
|
let initialDataReceived = false;
|
|
|
|
|
|
|
|
|
|
// Set an initial timeout for handshake data
|
|
|
|
|
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
|
|
|
|
|
if (!initialDataReceived) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
|
|
|
|
|
);
|
|
|
|
|
if (connectionRecord.incomingTerminationReason === null) {
|
|
|
|
|
connectionRecord.incomingTerminationReason = 'initial_timeout';
|
|
|
|
|
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
|
|
|
}
|
|
|
|
|
socket.end();
|
|
|
|
|
this.cleanupConnection(connectionRecord, 'initial_timeout');
|
|
|
|
|
}
|
|
|
|
|
}, this.settings.initialDataTimeout!);
|
|
|
|
|
|
|
|
|
|
// Make sure timeout doesn't keep the process alive
|
|
|
|
|
if (initialTimeout.unref) {
|
|
|
|
|
initialTimeout.unref();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socket.on('error', this.handleError('incoming', connectionRecord));
|
|
|
|
|
|
|
|
|
|
// First data handler to capture initial TLS handshake for NetworkProxy
|
|
|
|
|
socket.once('data', (chunk: Buffer) => {
|
|
|
|
|
// Clear the initial timeout since we've received data
|
|
|
|
|
if (initialTimeout) {
|
|
|
|
|
clearTimeout(initialTimeout);
|
|
|
|
|
initialTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
connectionRecord.hasReceivedInitialData = true;
|
|
|
|
|
|
|
|
|
|
// Check if this looks like a TLS handshake
|
|
|
|
|
if (isTlsHandshake(chunk)) {
|
|
|
|
|
connectionRecord.isTLS = true;
|
|
|
|
|
|
|
|
|
|
// Forward directly to NetworkProxy without SNI processing
|
|
|
|
|
this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk);
|
|
|
|
|
} else {
|
|
|
|
|
// If not TLS, use normal direct connection
|
|
|
|
|
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`);
|
|
|
|
|
this.setupDirectConnection(connectionId, socket, connectionRecord, undefined, undefined, chunk);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// For non-NetworkProxy ports, proceed with normal processing
|
|
|
|
|
|
|
|
|
|
// Define helpers for rejecting connections
|
|
|
|
|
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
|
|
|
|
console.log(`[${connectionId}] ${logMessage}`);
|
|
|
|
@ -1301,6 +1462,8 @@ export class PortProxy {
|
|
|
|
|
this.cleanupConnection(connectionRecord, reason);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let initialDataReceived = false;
|
|
|
|
|
|
|
|
|
|
// Set an initial timeout for SNI data if needed
|
|
|
|
|
let initialTimeout: NodeJS.Timeout | null = null;
|
|
|
|
|
if (this.settings.sniEnabled) {
|
|
|
|
@ -1349,7 +1512,7 @@ export class PortProxy {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets up the connection to the target host or NetworkProxy.
|
|
|
|
|
* 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).
|
|
|
|
@ -1418,23 +1581,6 @@ export class PortProxy {
|
|
|
|
|
)}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if we should forward this to a NetworkProxy
|
|
|
|
|
if (
|
|
|
|
|
isTlsHandshakeDetected &&
|
|
|
|
|
domainConfig.useNetworkProxy === true &&
|
|
|
|
|
initialChunk &&
|
|
|
|
|
this.networkProxies.length > 0
|
|
|
|
|
) {
|
|
|
|
|
return this.forwardToNetworkProxy(
|
|
|
|
|
connectionId,
|
|
|
|
|
socket,
|
|
|
|
|
connectionRecord,
|
|
|
|
|
domainConfig,
|
|
|
|
|
initialChunk,
|
|
|
|
|
serverName
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
|
|
|
if (
|
|
|
|
|
!isGlobIPAllowed(
|
|
|
|
@ -1450,7 +1596,12 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we didn't forward to NetworkProxy, proceed with direct connection
|
|
|
|
|
// Save the initial SNI
|
|
|
|
|
if (serverName) {
|
|
|
|
|
connectionRecord.lockedDomain = serverName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set up the direct connection
|
|
|
|
|
return this.setupDirectConnection(
|
|
|
|
|
connectionId,
|
|
|
|
|
socket,
|
|
|
|
@ -1595,6 +1746,7 @@ export class PortProxy {
|
|
|
|
|
|
|
|
|
|
setupConnection('');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- SETUP LISTENERS ---
|
|
|
|
@ -1619,10 +1771,11 @@ export class PortProxy {
|
|
|
|
|
console.log(`Server Error on port ${port}: ${err.message}`);
|
|
|
|
|
});
|
|
|
|
|
server.listen(port, () => {
|
|
|
|
|
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
|
|
|
|
|
console.log(
|
|
|
|
|
`PortProxy -> OK: Now listening on port ${port}${
|
|
|
|
|
this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
|
|
|
|
|
}${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}`
|
|
|
|
|
this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
|
|
|
|
|
}${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
this.netServers.push(server);
|
|
|
|
@ -1642,6 +1795,7 @@ export class PortProxy {
|
|
|
|
|
let pendingTlsHandshakes = 0;
|
|
|
|
|
let keepAliveConnections = 0;
|
|
|
|
|
let networkProxyConnections = 0;
|
|
|
|
|
let domainSwitchedConnections = 0;
|
|
|
|
|
|
|
|
|
|
// Create a copy of the keys to avoid modification during iteration
|
|
|
|
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
|
|
@ -1670,11 +1824,14 @@ export class PortProxy {
|
|
|
|
|
networkProxyConnections++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (record.domainSwitches && record.domainSwitches > 0) {
|
|
|
|
|
domainSwitchedConnections++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
|
|
|
|
if (record.outgoingStartTime) {
|
|
|
|
|
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parity check: if outgoing socket closed and incoming remains active
|
|
|
|
|
if (
|
|
|
|
|
record.outgoingClosedTime &&
|
|
|
|
@ -1706,9 +1863,10 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip inactivity check if disabled or for immortal keep-alive connections
|
|
|
|
|
if (!this.settings.disableInactivityCheck &&
|
|
|
|
|
!(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) {
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!this.settings.disableInactivityCheck &&
|
|
|
|
|
!(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
|
|
|
|
|
) {
|
|
|
|
|
const inactivityTime = now - record.lastActivity;
|
|
|
|
|
|
|
|
|
|
// Use extended timeout for extended-treatment keep-alive connections
|
|
|
|
@ -1722,7 +1880,9 @@ export class PortProxy {
|
|
|
|
|
// For keep-alive connections, issue a warning first
|
|
|
|
|
if (record.hasKeepAlive && !record.inactivityWarningIssued) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
|
|
|
`[${id}] Warning: Keep-alive connection from ${
|
|
|
|
|
record.remoteIP
|
|
|
|
|
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
|
|
|
`Will close in 10 minutes if no activity.`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
@ -1754,7 +1914,9 @@ export class PortProxy {
|
|
|
|
|
} else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
|
|
|
|
|
// If activity detected after warning, clear the warning
|
|
|
|
|
if (this.settings.enableDetailedLogging) {
|
|
|
|
|
console.log(`[${id}] Connection activity detected after inactivity warning, resetting warning`);
|
|
|
|
|
console.log(
|
|
|
|
|
`[${id}] Connection activity detected after inactivity warning, resetting warning`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
record.inactivityWarningIssued = false;
|
|
|
|
|
}
|
|
|
|
@ -1765,7 +1927,8 @@ export class PortProxy {
|
|
|
|
|
console.log(
|
|
|
|
|
`Active connections: ${this.connectionRecords.size}. ` +
|
|
|
|
|
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
|
|
|
|
|
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` +
|
|
|
|
|
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}, ` +
|
|
|
|
|
`DomainSwitched=${domainSwitchedConnections}. ` +
|
|
|
|
|
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
|
|
|
|
|
maxOutgoing
|
|
|
|
|
)}. ` +
|
|
|
|
@ -1782,21 +1945,6 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add or replace NetworkProxy instances
|
|
|
|
|
*/
|
|
|
|
|
public setNetworkProxies(networkProxies: NetworkProxy[]): void {
|
|
|
|
|
this.networkProxies = networkProxies;
|
|
|
|
|
console.log(`Updated NetworkProxy instances: ${this.networkProxies.length} proxies configured`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a list of configured NetworkProxy instances
|
|
|
|
|
*/
|
|
|
|
|
public getNetworkProxies(): NetworkProxy[] {
|
|
|
|
|
return this.networkProxies;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gracefully shut down the proxy
|
|
|
|
|
*/
|
|
|
|
@ -1888,6 +2036,16 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop NetworkProxy if it was started
|
|
|
|
|
if (this.networkProxy) {
|
|
|
|
|
try {
|
|
|
|
|
await this.networkProxy.stop();
|
|
|
|
|
console.log('NetworkProxy stopped successfully');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.log(`Error stopping NetworkProxy: ${err}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear all tracking maps
|
|
|
|
|
this.connectionRecords.clear();
|
|
|
|
|
this.domainTargetIndices.clear();
|
|
|
|
|