|
|
|
@ -23,6 +23,7 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
|
|
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
|
|
|
|
|
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
|
|
|
|
|
initialDataTimeout?: number; // (ms) timeout for receiving initial data, useful for chained proxies
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -163,6 +164,9 @@ export class PortProxy {
|
|
|
|
|
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
|
|
|
|
|
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Debug logging for constructor settings
|
|
|
|
|
console.log(`PortProxy initialized with targetIP: ${this.settings.targetIP}, toPort: ${this.settings.toPort}, fromPort: ${this.settings.fromPort}, sniEnabled: ${this.settings.sniEnabled}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
|
|
|
|
@ -320,17 +324,45 @@ export class PortProxy {
|
|
|
|
|
initiateCleanupOnce(reason);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Set an initial timeout immediately
|
|
|
|
|
const initialTimeout = setTimeout(() => {
|
|
|
|
|
if (!initialDataReceived) {
|
|
|
|
|
console.log(`Initial connection timeout for ${remoteIP} (no data received)`);
|
|
|
|
|
if (incomingTerminationReason === null) {
|
|
|
|
|
incomingTerminationReason = 'initial_timeout';
|
|
|
|
|
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
|
|
|
// IMPORTANT: We won't set any initial timeout for a chained proxy scenario
|
|
|
|
|
// The code below is commented out to restore original behavior
|
|
|
|
|
/*
|
|
|
|
|
let initialTimeout: NodeJS.Timeout | null = null;
|
|
|
|
|
const initialTimeoutMs = this.settings.initialDataTimeout ||
|
|
|
|
|
(this.settings.sniEnabled ? 15000 : 0);
|
|
|
|
|
|
|
|
|
|
if (initialTimeoutMs > 0) {
|
|
|
|
|
console.log(`Setting initial data timeout of ${initialTimeoutMs}ms for connection from ${remoteIP}`);
|
|
|
|
|
initialTimeout = setTimeout(() => {
|
|
|
|
|
if (!initialDataReceived) {
|
|
|
|
|
console.log(`Initial connection timeout for ${remoteIP} (no data received after ${initialTimeoutMs}ms)`);
|
|
|
|
|
if (incomingTerminationReason === null) {
|
|
|
|
|
incomingTerminationReason = 'initial_timeout';
|
|
|
|
|
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
|
|
|
}
|
|
|
|
|
initiateCleanupOnce('initial_timeout');
|
|
|
|
|
}
|
|
|
|
|
initiateCleanupOnce('initial_timeout');
|
|
|
|
|
}
|
|
|
|
|
}, 5000);
|
|
|
|
|
}, initialTimeoutMs);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`No initial timeout set for connection from ${remoteIP} (likely chained proxy)`);
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Original behavior: only set timeout if SNI is enabled, and use a fixed 5 second timeout
|
|
|
|
|
let initialTimeout: NodeJS.Timeout | null = null;
|
|
|
|
|
if (this.settings.sniEnabled) {
|
|
|
|
|
console.log(`Setting 5 second initial timeout for SNI extraction from ${remoteIP}`);
|
|
|
|
|
initialTimeout = setTimeout(() => {
|
|
|
|
|
if (!initialDataReceived) {
|
|
|
|
|
console.log(`Initial data timeout for ${remoteIP}`);
|
|
|
|
|
socket.end();
|
|
|
|
|
initiateCleanupOnce('initial_timeout');
|
|
|
|
|
}
|
|
|
|
|
}, 5000);
|
|
|
|
|
} else {
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socket.on('error', (err: Error) => {
|
|
|
|
|
const errorMessage = initialDataReceived
|
|
|
|
@ -341,6 +373,17 @@ export class PortProxy {
|
|
|
|
|
// Clear the initial timeout if it exists
|
|
|
|
|
if (initialTimeout) {
|
|
|
|
|
clearTimeout(initialTimeout);
|
|
|
|
|
initialTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For premature errors, we need to handle them explicitly
|
|
|
|
|
// since the standard error handlers might not be set up yet
|
|
|
|
|
if (!initialDataReceived) {
|
|
|
|
|
if (incomingTerminationReason === null) {
|
|
|
|
|
incomingTerminationReason = 'premature_error';
|
|
|
|
|
this.incrementTerminationStat('incoming', 'premature_error');
|
|
|
|
|
}
|
|
|
|
|
initiateCleanupOnce('premature_error');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@ -420,6 +463,8 @@ export class PortProxy {
|
|
|
|
|
) : undefined);
|
|
|
|
|
|
|
|
|
|
// Effective IP check: merge allowed IPs with default allowed, and remove blocked IPs.
|
|
|
|
|
// Use original domain configuration and IP validation logic
|
|
|
|
|
// This restores the behavior that was working before
|
|
|
|
|
if (domainConfig) {
|
|
|
|
|
const effectiveAllowedIPs: string[] = [
|
|
|
|
|
...domainConfig.allowedIPs,
|
|
|
|
@ -429,17 +474,17 @@ export class PortProxy {
|
|
|
|
|
...(domainConfig.blockedIPs || []),
|
|
|
|
|
...(this.settings.defaultBlockedIPs || [])
|
|
|
|
|
];
|
|
|
|
|
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
|
|
|
|
|
|
|
|
// Special case: if allowedIPs is empty, skip IP validation for backward compatibility
|
|
|
|
|
if (domainConfig.allowedIPs.length > 0 && !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
|
|
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
|
|
|
|
|
}
|
|
|
|
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
|
|
|
if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
|
|
|
|
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No domain config and no default allowed IPs - reject the connection
|
|
|
|
|
return rejectIncomingConnection('no_config', `Connection rejected: No matching domain configuration or default allowed IPs for ${remoteIP}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If no IP validation rules, allow the connection (original behavior)
|
|
|
|
|
|
|
|
|
|
const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
|
|
|
|
|
const connectionOptions: plugins.net.NetConnectOpts = {
|
|
|
|
@ -454,7 +499,7 @@ export class PortProxy {
|
|
|
|
|
let connectionTimeout: NodeJS.Timeout | null = null;
|
|
|
|
|
let connectionSucceeded = false;
|
|
|
|
|
|
|
|
|
|
// Set connection timeout
|
|
|
|
|
// Set connection timeout - longer for chained proxies
|
|
|
|
|
connectionTimeout = setTimeout(() => {
|
|
|
|
|
if (!connectionSucceeded) {
|
|
|
|
|
console.log(`Connection timeout connecting to ${targetHost}:${connectionOptions.port} for ${remoteIP}`);
|
|
|
|
@ -464,7 +509,7 @@ export class PortProxy {
|
|
|
|
|
}
|
|
|
|
|
initiateCleanupOnce('connection_timeout');
|
|
|
|
|
}
|
|
|
|
|
}, 5000);
|
|
|
|
|
}, 10000); // Increased from 5s to 10s to accommodate chained proxies
|
|
|
|
|
|
|
|
|
|
console.log(`Attempting to connect to ${targetHost}:${connectionOptions.port} for client ${remoteIP}...`);
|
|
|
|
|
|
|
|
|
@ -623,6 +668,11 @@ export class PortProxy {
|
|
|
|
|
initialDataReceived = false;
|
|
|
|
|
|
|
|
|
|
socket.once('data', (chunk: Buffer) => {
|
|
|
|
|
if (initialTimeout) {
|
|
|
|
|
clearTimeout(initialTimeout);
|
|
|
|
|
initialTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
const serverName = extractSNI(chunk) || '';
|
|
|
|
|
// Lock the connection to the negotiated SNI.
|
|
|
|
@ -654,7 +704,7 @@ export class PortProxy {
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
initialDataReceived = true;
|
|
|
|
|
if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
|
|
|
if (!this.settings.defaultAllowedIPs || this.settings.defaultAllowedIPs.length === 0 || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
|
|
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
|
|
|
|
}
|
|
|
|
|
setupConnection('');
|
|
|
|
|