From 94266222febbe6d9e23c429d6f81b828eee73074 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 26 Feb 2026 16:58:30 +0000 Subject: [PATCH] fix(connection): improve connection handling and timeouts --- changelog.md | 7 ++++++ ts/00_commitinfo_data.ts | 2 +- ts/core/utils/log-deduplicator.ts | 6 ++--- ts/detection/protocol-detector.ts | 24 ++++++++++++------- .../smart-proxy/socket-handler-server.ts | 19 +++++++++++++++ 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/changelog.md b/changelog.md index 5323a24..1d15677 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-02-26 - 25.8.2 - fix(connection) +improve connection handling and timeouts + +- Flush logs on process beforeExit and avoid calling process.exit in SIGINT/SIGTERM handlers to preserve host graceful shutdown +- Store protocol entries with a createdAt timestamp in ProtocolDetector and remove stale entries older than 30s to prevent leaked state from abandoned handshakes or port scanners +- Add backend connect timeout (30s) and idle timeouts (5 minutes) for dynamic forwards; destroy sockets on timeout and emit logs for timeout events + ## 2026-02-25 - 25.8.1 - fix(allocator) switch global allocator from tikv-jemallocator to mimalloc diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index cbfc58e..19126cc 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '25.8.1', + version: '25.8.2', description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' } diff --git a/ts/core/utils/log-deduplicator.ts b/ts/core/utils/log-deduplicator.ts index 3a33995..dafa965 100644 --- a/ts/core/utils/log-deduplicator.ts +++ b/ts/core/utils/log-deduplicator.ts @@ -354,17 +354,17 @@ export class LogDeduplicator { // Global instance for connection-related log deduplication export const connectionLogDeduplicator = new LogDeduplicator(5000); // 5 second batches -// Ensure logs are flushed on process exit +// Ensure logs are flushed on process exit. +// Only use beforeExit — do NOT call process.exit() from SIGINT/SIGTERM handlers +// as that kills the host process's graceful shutdown (e.g., dcrouter connection draining). process.on('beforeExit', () => { connectionLogDeduplicator.flushAll(); }); process.on('SIGINT', () => { connectionLogDeduplicator.cleanup(); - process.exit(0); }); process.on('SIGTERM', () => { connectionLogDeduplicator.cleanup(); - process.exit(0); }); \ No newline at end of file diff --git a/ts/detection/protocol-detector.ts b/ts/detection/protocol-detector.ts index 8376699..7708f93 100644 --- a/ts/detection/protocol-detector.ts +++ b/ts/detection/protocol-detector.ts @@ -18,8 +18,8 @@ export class ProtocolDetector { private fragmentManager: DetectionFragmentManager; private tlsDetector: TlsDetector; private httpDetector: HttpDetector; - private connectionProtocols: Map = new Map(); - + private connectionProtocols: Map = new Map(); + constructor() { this.fragmentManager = new DetectionFragmentManager(); this.tlsDetector = new TlsDetector(); @@ -124,8 +124,9 @@ export class ProtocolDetector { const connectionId = DetectionFragmentManager.createConnectionId(context); // Check if we already know the protocol for this connection - const knownProtocol = this.connectionProtocols.get(connectionId); - + const knownEntry = this.connectionProtocols.get(connectionId); + const knownProtocol = knownEntry?.protocol; + if (knownProtocol === 'http') { const result = this.httpDetector.detectWithContext(buffer, context, options); if (result) { @@ -163,7 +164,7 @@ export class ProtocolDetector { if (!knownProtocol) { // First peek to determine protocol type if (this.tlsDetector.canHandle(buffer)) { - this.connectionProtocols.set(connectionId, 'tls'); + this.connectionProtocols.set(connectionId, { protocol: 'tls', createdAt: Date.now() }); // Handle TLS with fragment accumulation const handler = this.fragmentManager.getHandler('tls'); const fragmentResult = handler.addFragment(connectionId, buffer); @@ -189,7 +190,7 @@ export class ProtocolDetector { } if (this.httpDetector.canHandle(buffer)) { - this.connectionProtocols.set(connectionId, 'http'); + this.connectionProtocols.set(connectionId, { protocol: 'http', createdAt: Date.now() }); const result = this.httpDetector.detectWithContext(buffer, context, options); if (result) { if (result.isComplete) { @@ -221,6 +222,14 @@ export class ProtocolDetector { private cleanupInstance(): void { this.fragmentManager.cleanup(); + // Remove stale connectionProtocols entries (abandoned handshakes, port scanners) + const maxAge = 30_000; // 30 seconds + const now = Date.now(); + for (const [id, entry] of this.connectionProtocols) { + if (now - entry.createdAt > maxAge) { + this.connectionProtocols.delete(id); + } + } } /** @@ -242,8 +251,7 @@ export class ProtocolDetector { * @param _maxAge Maximum age in milliseconds (default: 30 seconds) */ static cleanupConnections(_maxAge: number = 30000): void { - // Cleanup is now handled internally by the fragment manager - this.getInstance().fragmentManager.cleanup(); + this.getInstance().cleanupInstance(); } /** diff --git a/ts/proxies/smart-proxy/socket-handler-server.ts b/ts/proxies/smart-proxy/socket-handler-server.ts index 457725d..be0bd9e 100644 --- a/ts/proxies/smart-proxy/socket-handler-server.ts +++ b/ts/proxies/smart-proxy/socket-handler-server.ts @@ -254,11 +254,30 @@ export class SocketHandlerServer { // Connect to the resolved target const backend = plugins.net.connect(port, host, () => { + // Connection established — set idle timeout on both sides (5 min) + socket.setTimeout(300_000); + backend.setTimeout(300_000); + // Pipe bidirectionally socket.pipe(backend); backend.pipe(socket); }); + // Connect timeout: if backend doesn't connect within 30s, destroy both + backend.setTimeout(30_000); + + backend.on('timeout', () => { + logger.log('warn', `Dynamic forward timeout to ${host}:${port}`, { component: 'socket-handler-server' }); + backend.destroy(); + socket.destroy(); + }); + + socket.on('timeout', () => { + logger.log('debug', `Dynamic forward client idle timeout`, { component: 'socket-handler-server' }); + socket.destroy(); + backend.destroy(); + }); + backend.on('error', (err) => { logger.log('error', `Dynamic forward backend error: ${err.message}`, { component: 'socket-handler-server' }); socket.destroy();