feat(classes.portproxy): Enhanced PortProxy to support initial data timeout and improved IP handling
This commit is contained in:
parent
3ab483d164
commit
191c8ac0e6
@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-03-03 - 3.22.0 - feat(classes.portproxy)
|
||||||
|
Enhanced PortProxy to support initial data timeout and improved IP handling
|
||||||
|
|
||||||
|
- Added `initialDataTimeout` to PortProxy settings for handling data flow in chained proxies.
|
||||||
|
- Improved IP validation by allowing relaxed checks in chained proxy setups.
|
||||||
|
- Introduced dynamic logging for connection lifecycle and proxy configurations.
|
||||||
|
- Enhanced timeout handling for better proxy resilience.
|
||||||
|
|
||||||
## 2025-03-03 - 3.21.0 - feat(PortProxy)
|
## 2025-03-03 - 3.21.0 - feat(PortProxy)
|
||||||
Enhancements to connection management in PortProxy
|
Enhancements to connection management in PortProxy
|
||||||
|
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '3.21.0',
|
version: '3.22.0',
|
||||||
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
|
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|||||||
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
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
|
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
|
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,
|
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
|
||||||
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
|
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 {
|
private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
|
||||||
@ -320,17 +324,30 @@ export class PortProxy {
|
|||||||
initiateCleanupOnce(reason);
|
initiateCleanupOnce(reason);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set an initial timeout immediately
|
// Set an initial timeout only if SNI is enabled or this is not a chained proxy
|
||||||
const initialTimeout = setTimeout(() => {
|
// For chained proxies, we need to allow more time for data to flow through
|
||||||
if (!initialDataReceived) {
|
const initialTimeoutMs = this.settings.initialDataTimeout ||
|
||||||
console.log(`Initial connection timeout for ${remoteIP} (no data received)`);
|
(this.settings.sniEnabled ? 15000 : 0); // Increased timeout for SNI, disabled for non-SNI by default
|
||||||
if (incomingTerminationReason === null) {
|
|
||||||
incomingTerminationReason = 'initial_timeout';
|
let initialTimeout: NodeJS.Timeout | null = null;
|
||||||
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
||||||
|
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');
|
}, initialTimeoutMs);
|
||||||
}
|
} else {
|
||||||
}, 5000);
|
console.log(`No initial timeout set for connection from ${remoteIP} (likely chained proxy)`);
|
||||||
|
// Mark as received immediately if we're not waiting for data
|
||||||
|
initialDataReceived = true;
|
||||||
|
}
|
||||||
|
|
||||||
socket.on('error', (err: Error) => {
|
socket.on('error', (err: Error) => {
|
||||||
const errorMessage = initialDataReceived
|
const errorMessage = initialDataReceived
|
||||||
@ -341,6 +358,17 @@ export class PortProxy {
|
|||||||
// Clear the initial timeout if it exists
|
// Clear the initial timeout if it exists
|
||||||
if (initialTimeout) {
|
if (initialTimeout) {
|
||||||
clearTimeout(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,25 +448,34 @@ export class PortProxy {
|
|||||||
) : undefined);
|
) : undefined);
|
||||||
|
|
||||||
// Effective IP check: merge allowed IPs with default allowed, and remove blocked IPs.
|
// Effective IP check: merge allowed IPs with default allowed, and remove blocked IPs.
|
||||||
|
// In a chained proxy, relax IP validation unless explicitly configured
|
||||||
|
// If this is the first proxy in the chain, normal validation applies
|
||||||
if (domainConfig) {
|
if (domainConfig) {
|
||||||
const effectiveAllowedIPs: string[] = [
|
// Has specific domain config - check IP restrictions only if allowedIPs is non-empty
|
||||||
...domainConfig.allowedIPs,
|
if (domainConfig.allowedIPs.length > 0) {
|
||||||
...(this.settings.defaultAllowedIPs || [])
|
const effectiveAllowedIPs: string[] = [
|
||||||
];
|
...domainConfig.allowedIPs,
|
||||||
const effectiveBlockedIPs: string[] = [
|
...(this.settings.defaultAllowedIPs || [])
|
||||||
...(domainConfig.blockedIPs || []),
|
];
|
||||||
...(this.settings.defaultBlockedIPs || [])
|
const effectiveBlockedIPs: string[] = [
|
||||||
];
|
...(domainConfig.blockedIPs || []),
|
||||||
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
...(this.settings.defaultBlockedIPs || [])
|
||||||
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
|
];
|
||||||
|
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
||||||
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Domain config for ${domainConfig.domains.join(', ')} has empty allowedIPs, skipping IP validation`);
|
||||||
}
|
}
|
||||||
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
||||||
|
// No domain config but has default IP restrictions
|
||||||
if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
|
if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
|
||||||
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`);
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No domain config and no default allowed IPs - reject the connection
|
// No domain config and no default allowed IPs
|
||||||
return rejectIncomingConnection('no_config', `Connection rejected: No matching domain configuration or default allowed IPs for ${remoteIP}`);
|
// In a chained proxy setup, we'll allow this connection
|
||||||
|
console.log(`No specific IP restrictions found for ${remoteIP}. Allowing connection in potential chained proxy setup.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
|
const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
|
||||||
@ -454,7 +491,7 @@ export class PortProxy {
|
|||||||
let connectionTimeout: NodeJS.Timeout | null = null;
|
let connectionTimeout: NodeJS.Timeout | null = null;
|
||||||
let connectionSucceeded = false;
|
let connectionSucceeded = false;
|
||||||
|
|
||||||
// Set connection timeout
|
// Set connection timeout - longer for chained proxies
|
||||||
connectionTimeout = setTimeout(() => {
|
connectionTimeout = setTimeout(() => {
|
||||||
if (!connectionSucceeded) {
|
if (!connectionSucceeded) {
|
||||||
console.log(`Connection timeout connecting to ${targetHost}:${connectionOptions.port} for ${remoteIP}`);
|
console.log(`Connection timeout connecting to ${targetHost}:${connectionOptions.port} for ${remoteIP}`);
|
||||||
@ -464,7 +501,7 @@ export class PortProxy {
|
|||||||
}
|
}
|
||||||
initiateCleanupOnce('connection_timeout');
|
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}...`);
|
console.log(`Attempting to connect to ${targetHost}:${connectionOptions.port} for client ${remoteIP}...`);
|
||||||
|
|
||||||
@ -620,14 +657,38 @@ export class PortProxy {
|
|||||||
|
|
||||||
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
|
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
|
||||||
if (this.settings.sniEnabled) {
|
if (this.settings.sniEnabled) {
|
||||||
|
// If using SNI, we need to wait for data to establish the connection
|
||||||
|
if (initialDataReceived) {
|
||||||
|
console.log(`Initial data already marked as received for ${remoteIP}, but SNI is enabled. This is unexpected.`);
|
||||||
|
}
|
||||||
|
|
||||||
initialDataReceived = false;
|
initialDataReceived = false;
|
||||||
|
|
||||||
|
console.log(`Waiting for TLS ClientHello from ${remoteIP} to extract SNI...`);
|
||||||
socket.once('data', (chunk: Buffer) => {
|
socket.once('data', (chunk: Buffer) => {
|
||||||
|
if (initialTimeout) {
|
||||||
|
clearTimeout(initialTimeout);
|
||||||
|
initialTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
initialDataReceived = true;
|
initialDataReceived = true;
|
||||||
const serverName = extractSNI(chunk) || '';
|
console.log(`Received initial data from ${remoteIP}, length: ${chunk.length} bytes`);
|
||||||
|
|
||||||
|
let serverName = '';
|
||||||
|
try {
|
||||||
|
// Only try to extract SNI if the chunk looks like a TLS ClientHello
|
||||||
|
if (chunk.length > 5 && chunk.readUInt8(0) === 22) {
|
||||||
|
serverName = extractSNI(chunk) || '';
|
||||||
|
console.log(`Extracted SNI: "${serverName}" from connection ${remoteIP}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Data from ${remoteIP} doesn't appear to be a TLS ClientHello. First byte: ${chunk.length > 0 ? chunk.readUInt8(0) : 'N/A'}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error extracting SNI from chunk: ${err}. Proceeding without SNI.`);
|
||||||
|
}
|
||||||
|
|
||||||
// Lock the connection to the negotiated SNI.
|
// Lock the connection to the negotiated SNI.
|
||||||
connectionRecord.lockedDomain = serverName;
|
connectionRecord.lockedDomain = serverName;
|
||||||
console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
|
|
||||||
|
|
||||||
// Delay adding the renegotiation listener until the next tick,
|
// Delay adding the renegotiation listener until the next tick,
|
||||||
// so the initial ClientHello is not reprocessed.
|
// so the initial ClientHello is not reprocessed.
|
||||||
@ -653,10 +714,23 @@ export class PortProxy {
|
|||||||
setupConnection(serverName, chunk);
|
setupConnection(serverName, chunk);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
initialDataReceived = true;
|
// Non-SNI mode: we can proceed immediately without waiting for data
|
||||||
if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
if (initialTimeout) {
|
||||||
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
clearTimeout(initialTimeout);
|
||||||
|
initialTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialDataReceived = true;
|
||||||
|
console.log(`SNI disabled for connection from ${remoteIP}, proceeding directly to connection setup`);
|
||||||
|
|
||||||
|
// Check IP restrictions only if explicitly configured
|
||||||
|
if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
||||||
|
if (!isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
||||||
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with connection setup
|
||||||
setupConnection('');
|
setupConnection('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user