@push.rocks/smartproxy 3.25.2 → 3.25.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,14 +7,10 @@ export interface IDomainConfig {
7
7
  blockedIPs?: string[]; // Glob patterns for blocked IPs
8
8
  targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
9
9
  portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
10
- // Protocol-specific timeout overrides
11
- httpTimeout?: number; // HTTP connection timeout override (ms)
12
- wsTimeout?: number; // WebSocket connection timeout override (ms)
10
+ // Allow domain-specific timeout override
11
+ connectionTimeout?: number; // Connection timeout override (ms)
13
12
  }
14
13
 
15
- /** Connection protocol types for timeout management */
16
- export type ProtocolType = 'http' | 'websocket' | 'https' | 'tls' | 'unknown';
17
-
18
14
  /** Port proxy settings including global allowed port ranges */
19
15
  export interface IPortProxySettings extends plugins.tls.TlsOptions {
20
16
  fromPort: number;
@@ -26,40 +22,37 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
26
22
  defaultBlockedIPs?: string[];
27
23
  preserveSourceIP?: boolean;
28
24
 
29
- // Updated timeout settings with better defaults
30
- initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 15000 (15s)
31
- socketTimeout?: number; // Socket inactivity timeout (ms), default: 300000 (5m)
32
- inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 30000 (30s)
33
-
34
- // Protocol-specific timeouts
35
- maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h)
36
- httpConnectionTimeout?: number; // HTTP specific timeout (ms), default: 1800000 (30m)
37
- wsConnectionTimeout?: number; // WebSocket specific timeout (ms), default: 14400000 (4h)
38
- httpKeepAliveTimeout?: number; // HTTP keep-alive header timeout (ms), default: 1200000 (20m)
25
+ // Timeout settings
26
+ initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
27
+ socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
28
+ inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
29
+ maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h)
30
+ inactivityTimeout?: number; // Inactivity timeout (ms), default: 3600000 (1h)
39
31
 
40
- gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
32
+ gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
41
33
  globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
42
- forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
34
+ forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
43
35
 
44
36
  // Socket optimization settings
45
- noDelay?: boolean; // Disable Nagle's algorithm (default: true)
46
- keepAlive?: boolean; // Enable TCP keepalive (default: true)
47
- keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
48
- maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
37
+ noDelay?: boolean; // Disable Nagle's algorithm (default: true)
38
+ keepAlive?: boolean; // Enable TCP keepalive (default: true)
39
+ keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
40
+ maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
49
41
 
50
- // Enable enhanced features
51
- disableInactivityCheck?: boolean; // Disable inactivity checking entirely
52
- enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
53
- enableProtocolDetection?: boolean; // Enable HTTP/WebSocket protocol detection
54
- enableDetailedLogging?: boolean; // Enable detailed connection logging
42
+ // Enhanced features
43
+ disableInactivityCheck?: boolean; // Disable inactivity checking entirely
44
+ enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
45
+ enableDetailedLogging?: boolean; // Enable detailed connection logging
46
+ enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
47
+ enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
55
48
 
56
49
  // Rate limiting and security
57
- maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
50
+ maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
58
51
  connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
59
52
  }
60
53
 
61
54
  /**
62
- * Enhanced connection record with protocol-specific handling
55
+ * Enhanced connection record
63
56
  */
64
57
  interface IConnectionRecord {
65
58
  id: string; // Unique connection identifier
@@ -76,78 +69,161 @@ interface IConnectionRecord {
76
69
  pendingDataSize: number; // Track total size of pending data
77
70
 
78
71
  // Enhanced tracking fields
79
- protocolType: ProtocolType; // Connection protocol type
80
- isPooledConnection: boolean; // Whether this is likely a browser pooled connection
81
- lastHttpRequest?: number; // Timestamp of last HTTP request (for keep-alive tracking)
82
- httpKeepAliveTimeout?: number; // HTTP keep-alive timeout from headers
83
72
  bytesReceived: number; // Total bytes received
84
73
  bytesSent: number; // Total bytes sent
85
74
  remoteIP: string; // Remote IP (cached for logging after socket close)
86
75
  localPort: number; // Local port (cached for logging)
87
- httpRequests: number; // Count of HTTP requests on this connection
76
+ isTLS: boolean; // Whether this connection is a TLS connection
77
+ tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
78
+ hasReceivedInitialData: boolean; // Whether initial data has been received
79
+ domainConfig?: IDomainConfig; // Associated domain config for this connection
88
80
  }
89
81
 
90
82
  /**
91
83
  * Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
84
+ * Enhanced for robustness and detailed logging.
92
85
  * @param buffer - Buffer containing the TLS ClientHello.
86
+ * @param enableLogging - Whether to enable detailed logging.
93
87
  * @returns The server name if found, otherwise undefined.
94
88
  */
95
- function extractSNI(buffer: Buffer): string | undefined {
96
- let offset = 0;
97
- if (buffer.length < 5) return undefined;
98
-
99
- const recordType = buffer.readUInt8(0);
100
- if (recordType !== 22) return undefined; // 22 = handshake
101
-
102
- const recordLength = buffer.readUInt16BE(3);
103
- if (buffer.length < 5 + recordLength) return undefined;
89
+ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
90
+ try {
91
+ // Check if buffer is too small for TLS
92
+ if (buffer.length < 5) {
93
+ if (enableLogging) console.log("Buffer too small for TLS header");
94
+ return undefined;
95
+ }
104
96
 
105
- offset = 5;
106
- const handshakeType = buffer.readUInt8(offset);
107
- if (handshakeType !== 1) return undefined; // 1 = ClientHello
97
+ // Check record type (has to be handshake - 22)
98
+ const recordType = buffer.readUInt8(0);
99
+ if (recordType !== 22) {
100
+ if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
101
+ return undefined;
102
+ }
108
103
 
109
- offset += 4; // Skip handshake header (type + length)
110
- offset += 2 + 32; // Skip client version and random
104
+ // Check TLS version (has to be 3.1 or higher)
105
+ const majorVersion = buffer.readUInt8(1);
106
+ const minorVersion = buffer.readUInt8(2);
107
+ if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
108
+
109
+ // Check record length
110
+ const recordLength = buffer.readUInt16BE(3);
111
+ if (buffer.length < 5 + recordLength) {
112
+ if (enableLogging) console.log(`Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`);
113
+ return undefined;
114
+ }
111
115
 
112
- const sessionIDLength = buffer.readUInt8(offset);
113
- offset += 1 + sessionIDLength; // Skip session ID
116
+ let offset = 5;
117
+ const handshakeType = buffer.readUInt8(offset);
118
+ if (handshakeType !== 1) {
119
+ if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
120
+ return undefined;
121
+ }
114
122
 
115
- const cipherSuitesLength = buffer.readUInt16BE(offset);
116
- offset += 2 + cipherSuitesLength; // Skip cipher suites
123
+ offset += 4; // Skip handshake header (type + length)
124
+
125
+ // Client version
126
+ const clientMajorVersion = buffer.readUInt8(offset);
127
+ const clientMinorVersion = buffer.readUInt8(offset + 1);
128
+ if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
129
+
130
+ offset += 2 + 32; // Skip client version and random
117
131
 
118
- const compressionMethodsLength = buffer.readUInt8(offset);
119
- offset += 1 + compressionMethodsLength; // Skip compression methods
132
+ // Session ID
133
+ const sessionIDLength = buffer.readUInt8(offset);
134
+ if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
135
+ offset += 1 + sessionIDLength; // Skip session ID
120
136
 
121
- if (offset + 2 > buffer.length) return undefined;
122
- const extensionsLength = buffer.readUInt16BE(offset);
123
- offset += 2;
124
- const extensionsEnd = offset + extensionsLength;
137
+ // Cipher suites
138
+ if (offset + 2 > buffer.length) {
139
+ if (enableLogging) console.log("Buffer too small for cipher suites length");
140
+ return undefined;
141
+ }
142
+ const cipherSuitesLength = buffer.readUInt16BE(offset);
143
+ if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
144
+ offset += 2 + cipherSuitesLength; // Skip cipher suites
145
+
146
+ // Compression methods
147
+ if (offset + 1 > buffer.length) {
148
+ if (enableLogging) console.log("Buffer too small for compression methods length");
149
+ return undefined;
150
+ }
151
+ const compressionMethodsLength = buffer.readUInt8(offset);
152
+ if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
153
+ offset += 1 + compressionMethodsLength; // Skip compression methods
154
+
155
+ // Extensions
156
+ if (offset + 2 > buffer.length) {
157
+ if (enableLogging) console.log("Buffer too small for extensions length");
158
+ return undefined;
159
+ }
160
+ const extensionsLength = buffer.readUInt16BE(offset);
161
+ if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
162
+ offset += 2;
163
+ const extensionsEnd = offset + extensionsLength;
164
+
165
+ if (extensionsEnd > buffer.length) {
166
+ if (enableLogging) console.log(`Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`);
167
+ return undefined;
168
+ }
125
169
 
126
- while (offset + 4 <= extensionsEnd) {
127
- const extensionType = buffer.readUInt16BE(offset);
128
- const extensionLength = buffer.readUInt16BE(offset + 2);
129
- offset += 4;
130
- if (extensionType === 0x0000) { // SNI extension
131
- if (offset + 2 > buffer.length) return undefined;
132
- const sniListLength = buffer.readUInt16BE(offset);
133
- offset += 2;
134
- const sniListEnd = offset + sniListLength;
135
- while (offset + 3 < sniListEnd) {
136
- const nameType = buffer.readUInt8(offset++);
137
- const nameLen = buffer.readUInt16BE(offset);
170
+ // Parse extensions
171
+ while (offset + 4 <= extensionsEnd) {
172
+ const extensionType = buffer.readUInt16BE(offset);
173
+ const extensionLength = buffer.readUInt16BE(offset + 2);
174
+
175
+ if (enableLogging) console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
176
+
177
+ offset += 4;
178
+
179
+ if (extensionType === 0x0000) { // SNI extension
180
+ if (offset + 2 > buffer.length) {
181
+ if (enableLogging) console.log("Buffer too small for SNI list length");
182
+ return undefined;
183
+ }
184
+
185
+ const sniListLength = buffer.readUInt16BE(offset);
186
+ if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
138
187
  offset += 2;
139
- if (nameType === 0) { // host_name
140
- if (offset + nameLen > buffer.length) return undefined;
141
- return buffer.toString('utf8', offset, offset + nameLen);
188
+ const sniListEnd = offset + sniListLength;
189
+
190
+ if (sniListEnd > buffer.length) {
191
+ if (enableLogging) console.log(`Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`);
192
+ return undefined;
193
+ }
194
+
195
+ while (offset + 3 < sniListEnd) {
196
+ const nameType = buffer.readUInt8(offset++);
197
+ const nameLen = buffer.readUInt16BE(offset);
198
+ offset += 2;
199
+
200
+ if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
201
+
202
+ if (nameType === 0) { // host_name
203
+ if (offset + nameLen > buffer.length) {
204
+ if (enableLogging) console.log(`Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${buffer.length}`);
205
+ return undefined;
206
+ }
207
+
208
+ const serverName = buffer.toString('utf8', offset, offset + nameLen);
209
+ if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
210
+ return serverName;
211
+ }
212
+
213
+ offset += nameLen;
142
214
  }
143
- offset += nameLen;
215
+ break;
216
+ } else {
217
+ offset += extensionLength;
144
218
  }
145
- break;
146
- } else {
147
- offset += extensionLength;
148
219
  }
220
+
221
+ if (enableLogging) console.log("No SNI extension found");
222
+ return undefined;
223
+ } catch (err) {
224
+ console.log(`Error extracting SNI: ${err}`);
225
+ return undefined;
149
226
  }
150
- return undefined;
151
227
  }
152
228
 
153
229
  // Helper: Check if a port falls within any of the given port ranges
@@ -157,7 +233,10 @@ const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }
157
233
 
158
234
  // Helper: Check if a given IP matches any of the glob patterns
159
235
  const isAllowed = (ip: string, patterns: string[]): boolean => {
236
+ if (!ip || !patterns || patterns.length === 0) return false;
237
+
160
238
  const normalizeIP = (ip: string): string[] => {
239
+ if (!ip) return [];
161
240
  if (ip.startsWith('::ffff:')) {
162
241
  const ipv4 = ip.slice(7);
163
242
  return [ip, ipv4];
@@ -167,7 +246,10 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
167
246
  }
168
247
  return [ip];
169
248
  };
249
+
170
250
  const normalizedIPVariants = normalizeIP(ip);
251
+ if (normalizedIPVariants.length === 0) return false;
252
+
171
253
  const expandedPatterns = patterns.flatMap(normalizeIP);
172
254
  return normalizedIPVariants.some(ipVariant =>
173
255
  expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
@@ -176,6 +258,7 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
176
258
 
177
259
  // Helper: Check if an IP is allowed considering allowed and blocked glob patterns
178
260
  const isGlobIPAllowed = (ip: string, allowed: string[], blocked: string[] = []): boolean => {
261
+ if (!ip) return false;
179
262
  if (blocked.length > 0 && isAllowed(ip, blocked)) return false;
180
263
  return isAllowed(ip, allowed);
181
264
  };
@@ -185,34 +268,17 @@ const generateConnectionId = (): string => {
185
268
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
186
269
  };
187
270
 
188
- // Protocol detection helpers
189
- const isHttpRequest = (buffer: Buffer): boolean => {
190
- if (buffer.length < 4) return false;
191
- const start = buffer.toString('ascii', 0, 4).toUpperCase();
192
- return (
193
- start.startsWith('GET ') ||
194
- start.startsWith('POST') ||
195
- start.startsWith('PUT ') ||
196
- start.startsWith('HEAD') ||
197
- start.startsWith('DELE') ||
198
- start.startsWith('PATC') ||
199
- start.startsWith('OPTI')
200
- );
201
- };
202
-
203
- const isWebSocketUpgrade = (buffer: Buffer): boolean => {
204
- if (buffer.length < 20) return false;
205
- const data = buffer.toString('ascii', 0, Math.min(buffer.length, 200));
206
- return (
207
- data.includes('Upgrade: websocket') ||
208
- data.includes('Upgrade: WebSocket')
209
- );
210
- };
211
-
271
+ // Helper: Check if a buffer contains a TLS handshake
212
272
  const isTlsHandshake = (buffer: Buffer): boolean => {
213
273
  return buffer.length > 0 && buffer[0] === 22; // ContentType.handshake
214
274
  };
215
275
 
276
+ // Helper: Generate a slightly randomized timeout to prevent thundering herd
277
+ const randomizeTimeout = (baseTimeout: number, variationPercent: number = 5): number => {
278
+ const variation = baseTimeout * (variationPercent / 100);
279
+ return baseTimeout + Math.floor(Math.random() * variation * 2) - variation;
280
+ };
281
+
216
282
  export class PortProxy {
217
283
  private netServers: plugins.net.Server[] = [];
218
284
  settings: IPortProxySettings;
@@ -242,16 +308,12 @@ export class PortProxy {
242
308
  ...settingsArg,
243
309
  targetIP: settingsArg.targetIP || 'localhost',
244
310
 
245
- // Timeout settings with browser-friendly defaults
246
- initialDataTimeout: settingsArg.initialDataTimeout || 30000, // 30 seconds
247
- socketTimeout: settingsArg.socketTimeout || 300000, // 5 minutes
248
- inactivityCheckInterval: settingsArg.inactivityCheckInterval || 30000, // 30 seconds
249
-
250
- // Protocol-specific timeouts
251
- maxConnectionLifetime: settingsArg.maxConnectionLifetime || 3600000, // 1 hour default
252
- httpConnectionTimeout: settingsArg.httpConnectionTimeout || 1800000, // 30 minutes
253
- wsConnectionTimeout: settingsArg.wsConnectionTimeout || 14400000, // 4 hours
254
- httpKeepAliveTimeout: settingsArg.httpKeepAliveTimeout || 1200000, // 20 minutes
311
+ // Timeout settings with our enhanced defaults
312
+ initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial data
313
+ socketTimeout: settingsArg.socketTimeout || 3600000, // 1 hour socket timeout
314
+ inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
315
+ maxConnectionLifetime: settingsArg.maxConnectionLifetime || 3600000, // 1 hour default lifetime
316
+ inactivityTimeout: settingsArg.inactivityTimeout || 3600000, // 1 hour inactivity timeout
255
317
 
256
318
  gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
257
319
 
@@ -259,13 +321,14 @@ export class PortProxy {
259
321
  noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
260
322
  keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
261
323
  keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds
262
- maxPendingDataSize: settingsArg.maxPendingDataSize || 1024 * 1024, // 1MB
324
+ maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
263
325
 
264
326
  // Feature flags
265
327
  disableInactivityCheck: settingsArg.disableInactivityCheck || false,
266
328
  enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false,
267
- enableProtocolDetection: settingsArg.enableProtocolDetection !== undefined ? settingsArg.enableProtocolDetection : true,
268
329
  enableDetailedLogging: settingsArg.enableDetailedLogging || false,
330
+ enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
331
+ enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || true,
269
332
 
270
333
  // Rate limiting defaults
271
334
  maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
@@ -332,115 +395,22 @@ export class PortProxy {
332
395
  }
333
396
 
334
397
  /**
335
- * Get protocol-specific timeout based on connection type
398
+ * Get connection timeout based on domain config or default settings
336
399
  */
337
- private getProtocolTimeout(record: IConnectionRecord, domainConfig?: IDomainConfig): number {
338
- // If the protocol has a domain-specific timeout, use that
339
- if (domainConfig) {
340
- if (record.protocolType === 'http' && domainConfig.httpTimeout) {
341
- return domainConfig.httpTimeout;
342
- }
343
- if (record.protocolType === 'websocket' && domainConfig.wsTimeout) {
344
- return domainConfig.wsTimeout;
345
- }
346
- }
347
-
348
- // Use HTTP keep-alive timeout from headers if available
349
- if (record.httpKeepAliveTimeout) {
350
- return record.httpKeepAliveTimeout;
400
+ private getConnectionTimeout(record: IConnectionRecord): number {
401
+ // If the connection has a domain-specific timeout, use that
402
+ if (record.domainConfig?.connectionTimeout) {
403
+ return record.domainConfig.connectionTimeout;
351
404
  }
352
405
 
353
- // Otherwise use default protocol-specific timeout
354
- switch (record.protocolType) {
355
- case 'http':
356
- return this.settings.httpConnectionTimeout!;
357
- case 'websocket':
358
- return this.settings.wsConnectionTimeout!;
359
- case 'https':
360
- case 'tls':
361
- return this.settings.httpConnectionTimeout!; // Use HTTP timeout for HTTPS by default
362
- default:
363
- return this.settings.maxConnectionLifetime!;
364
- }
365
- }
366
-
367
- /**
368
- * Detect protocol and update connection record
369
- */
370
- private detectProtocol(data: Buffer, record: IConnectionRecord): void {
371
- if (!this.settings.enableProtocolDetection || record.protocolType !== 'unknown') {
372
- return;
373
- }
374
-
375
- try {
376
- // Detect TLS/HTTPS
377
- if (isTlsHandshake(data)) {
378
- record.protocolType = 'tls';
379
- if (this.settings.enableDetailedLogging) {
380
- console.log(`[${record.id}] Protocol detected: TLS`);
381
- }
382
- return;
383
- }
384
-
385
- // Detect HTTP including WebSocket upgrades
386
- if (isHttpRequest(data)) {
387
- record.httpRequests++;
388
- record.lastHttpRequest = Date.now();
389
-
390
- // Check for WebSocket upgrade
391
- if (isWebSocketUpgrade(data)) {
392
- record.protocolType = 'websocket';
393
- if (this.settings.enableDetailedLogging) {
394
- console.log(`[${record.id}] Protocol detected: WebSocket Upgrade`);
395
- }
396
- } else {
397
- record.protocolType = 'http';
398
-
399
- // Parse HTTP keep-alive headers
400
- this.parseHttpHeaders(data, record);
401
-
402
- if (this.settings.enableDetailedLogging) {
403
- console.log(`[${record.id}] Protocol detected: HTTP${record.isPooledConnection ? ' (KeepAlive)' : ''}`);
404
- }
405
- }
406
- }
407
- } catch (err) {
408
- console.log(`[${record.id}] Error detecting protocol: ${err}`);
409
- }
410
- }
411
-
412
- /**
413
- * Parse HTTP headers for keep-alive and other connection info
414
- */
415
- private parseHttpHeaders(data: Buffer, record: IConnectionRecord): void {
416
- try {
417
- const headerStr = data.toString('utf8', 0, Math.min(data.length, 1024));
418
-
419
- // Check for HTTP keep-alive
420
- const connectionHeader = headerStr.match(/\r\nConnection:\s*([^\r\n]+)/i);
421
- if (connectionHeader && connectionHeader[1].toLowerCase().includes('keep-alive')) {
422
- record.isPooledConnection = true;
423
-
424
- // Check for Keep-Alive timeout value
425
- const keepAliveHeader = headerStr.match(/\r\nKeep-Alive:\s*([^\r\n]+)/i);
426
- if (keepAliveHeader) {
427
- const timeoutMatch = keepAliveHeader[1].match(/timeout=(\d+)/i);
428
- if (timeoutMatch && timeoutMatch[1]) {
429
- const timeoutSec = parseInt(timeoutMatch[1], 10);
430
- if (!isNaN(timeoutSec) && timeoutSec > 0) {
431
- // Convert seconds to milliseconds and add some buffer
432
- record.httpKeepAliveTimeout = (timeoutSec * 1000) + 5000;
433
-
434
- if (this.settings.enableDetailedLogging) {
435
- console.log(`[${record.id}] HTTP Keep-Alive timeout set to ${timeoutSec} seconds`);
436
- }
437
- }
438
- }
439
- }
440
- }
441
- } catch (err) {
442
- console.log(`[${record.id}] Error parsing HTTP headers: ${err}`);
406
+ // Use default timeout, potentially randomized
407
+ const baseTimeout = this.settings.maxConnectionLifetime!;
408
+
409
+ if (this.settings.enableRandomizedTimeouts) {
410
+ return randomizeTimeout(baseTimeout);
443
411
  }
412
+
413
+ return baseTimeout;
444
414
  }
445
415
 
446
416
  /**
@@ -465,7 +435,6 @@ export class PortProxy {
465
435
  const duration = Date.now() - record.incomingStartTime;
466
436
  const bytesReceived = record.bytesReceived;
467
437
  const bytesSent = record.bytesSent;
468
- const httpRequests = record.httpRequests;
469
438
 
470
439
  try {
471
440
  if (!record.incoming.destroyed) {
@@ -538,7 +507,7 @@ export class PortProxy {
538
507
  if (this.settings.enableDetailedLogging) {
539
508
  console.log(`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
540
509
  ` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
541
- `HTTP Requests: ${httpRequests}, Protocol: ${record.protocolType}, Pooled: ${record.isPooledConnection}`);
510
+ `TLS: ${record.isTLS ? 'Yes' : 'No'}`);
542
511
  } else {
543
512
  console.log(`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`);
544
513
  }
@@ -608,6 +577,21 @@ export class PortProxy {
608
577
  socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
609
578
  }
610
579
 
580
+ // Apply enhanced TCP options if available
581
+ if (this.settings.enableKeepAliveProbes) {
582
+ try {
583
+ // These are platform-specific and may not be available
584
+ if ('setKeepAliveProbes' in socket) {
585
+ (socket as any).setKeepAliveProbes(10);
586
+ }
587
+ if ('setKeepAliveInterval' in socket) {
588
+ (socket as any).setKeepAliveInterval(1000);
589
+ }
590
+ } catch (err) {
591
+ // Ignore errors - these are optional enhancements
592
+ }
593
+ }
594
+
611
595
  // Create a unique connection ID and record
612
596
  const connectionId = generateConnectionId();
613
597
  const connectionRecord: IConnectionRecord = {
@@ -621,13 +605,13 @@ export class PortProxy {
621
605
  pendingDataSize: 0,
622
606
 
623
607
  // Initialize enhanced tracking fields
624
- protocolType: 'unknown',
625
- isPooledConnection: false,
626
608
  bytesReceived: 0,
627
609
  bytesSent: 0,
628
610
  remoteIP: remoteIP,
629
611
  localPort: localPort,
630
- httpRequests: 0
612
+ isTLS: false,
613
+ tlsHandshakeComplete: false,
614
+ hasReceivedInitialData: false
631
615
  };
632
616
 
633
617
  // Track connection by IP
@@ -685,9 +669,15 @@ export class PortProxy {
685
669
  socket.end();
686
670
  cleanupOnce();
687
671
  }
688
- }, this.settings.initialDataTimeout);
672
+ }, this.settings.initialDataTimeout!);
673
+
674
+ // Make sure timeout doesn't keep the process alive
675
+ if (initialTimeout.unref) {
676
+ initialTimeout.unref();
677
+ }
689
678
  } else {
690
679
  initialDataReceived = true;
680
+ connectionRecord.hasReceivedInitialData = true;
691
681
  }
692
682
 
693
683
  socket.on('error', (err: Error) => {
@@ -699,39 +689,14 @@ export class PortProxy {
699
689
  connectionRecord.bytesReceived += chunk.length;
700
690
  this.updateActivity(connectionRecord);
701
691
 
702
- // Detect protocol on first data chunk
703
- if (connectionRecord.protocolType === 'unknown') {
704
- this.detectProtocol(chunk, connectionRecord);
692
+ // Check for TLS handshake if this is the first chunk
693
+ if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
694
+ connectionRecord.isTLS = true;
705
695
 
706
- // Update timeout based on protocol
707
- if (connectionRecord.cleanupTimer) {
708
- clearTimeout(connectionRecord.cleanupTimer);
709
-
710
- // Set new timeout based on protocol
711
- const protocolTimeout = this.getProtocolTimeout(connectionRecord);
712
- connectionRecord.cleanupTimer = setTimeout(() => {
713
- console.log(`[${connectionId}] ${connectionRecord.protocolType} connection timeout after ${plugins.prettyMs(protocolTimeout)}`);
714
- initiateCleanupOnce(`${connectionRecord.protocolType}_timeout`);
715
- }, protocolTimeout);
716
- }
717
- } else if (connectionRecord.protocolType === 'http' && isHttpRequest(chunk)) {
718
- // Additional HTTP request on the same connection
719
- connectionRecord.httpRequests++;
720
- connectionRecord.lastHttpRequest = Date.now();
721
-
722
- // Parse HTTP headers again for keep-alive changes
723
- this.parseHttpHeaders(chunk, connectionRecord);
724
-
725
- // Update timeout based on new HTTP headers
726
- if (connectionRecord.cleanupTimer) {
727
- clearTimeout(connectionRecord.cleanupTimer);
728
-
729
- // Set new timeout based on updated HTTP info
730
- const protocolTimeout = this.getProtocolTimeout(connectionRecord);
731
- connectionRecord.cleanupTimer = setTimeout(() => {
732
- console.log(`[${connectionId}] HTTP connection timeout after ${plugins.prettyMs(protocolTimeout)}`);
733
- initiateCleanupOnce('http_timeout');
734
- }, protocolTimeout);
696
+ if (this.settings.enableTlsDebugLogging) {
697
+ console.log(`[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`);
698
+ // Try to extract SNI and log detailed debug info
699
+ extractSNI(chunk, true);
735
700
  }
736
701
  }
737
702
  });
@@ -797,9 +762,17 @@ export class PortProxy {
797
762
  initialTimeout = null;
798
763
  }
799
764
 
800
- // Detect protocol if initial chunk is available
801
- if (initialChunk && this.settings.enableProtocolDetection) {
802
- this.detectProtocol(initialChunk, connectionRecord);
765
+ // Mark that we've received initial data
766
+ initialDataReceived = true;
767
+ connectionRecord.hasReceivedInitialData = true;
768
+
769
+ // Check if this looks like a TLS handshake
770
+ if (initialChunk && isTlsHandshake(initialChunk)) {
771
+ connectionRecord.isTLS = true;
772
+
773
+ if (this.settings.enableTlsDebugLogging) {
774
+ console.log(`[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`);
775
+ }
803
776
  }
804
777
 
805
778
  // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
@@ -809,6 +782,9 @@ export class PortProxy {
809
782
  config.domains.some(d => plugins.minimatch(serverName, d))
810
783
  ) : undefined);
811
784
 
785
+ // Save domain config in connection record
786
+ connectionRecord.domainConfig = domainConfig;
787
+
812
788
  // IP validation is skipped if allowedIPs is empty
813
789
  if (domainConfig) {
814
790
  const effectiveAllowedIPs: string[] = [
@@ -847,9 +823,13 @@ export class PortProxy {
847
823
  // Track bytes received
848
824
  connectionRecord.bytesReceived += chunk.length;
849
825
 
850
- // Detect protocol even during connection setup
851
- if (this.settings.enableProtocolDetection && connectionRecord.protocolType === 'unknown') {
852
- this.detectProtocol(chunk, connectionRecord);
826
+ // Check for TLS handshake
827
+ if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
828
+ connectionRecord.isTLS = true;
829
+
830
+ if (this.settings.enableTlsDebugLogging) {
831
+ console.log(`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`);
832
+ }
853
833
  }
854
834
 
855
835
  // Check if adding this chunk would exceed the buffer limit
@@ -888,6 +868,20 @@ export class PortProxy {
888
868
  targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
889
869
  }
890
870
 
871
+ // Apply enhanced TCP options if available
872
+ if (this.settings.enableKeepAliveProbes) {
873
+ try {
874
+ if ('setKeepAliveProbes' in targetSocket) {
875
+ (targetSocket as any).setKeepAliveProbes(10);
876
+ }
877
+ if ('setKeepAliveInterval' in targetSocket) {
878
+ (targetSocket as any).setKeepAliveInterval(1000);
879
+ }
880
+ } catch (err) {
881
+ // Ignore errors - these are optional enhancements
882
+ }
883
+ }
884
+
891
885
  // Setup specific error handler for connection phase
892
886
  targetSocket.once('error', (err) => {
893
887
  // This handler runs only once during the initial connection phase
@@ -928,7 +922,7 @@ export class PortProxy {
928
922
 
929
923
  // Handle timeouts
930
924
  socket.on('timeout', () => {
931
- console.log(`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 300000)}`);
925
+ console.log(`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`);
932
926
  if (incomingTerminationReason === null) {
933
927
  incomingTerminationReason = 'timeout';
934
928
  this.incrementTerminationStat('incoming', 'timeout');
@@ -937,7 +931,7 @@ export class PortProxy {
937
931
  });
938
932
 
939
933
  targetSocket.on('timeout', () => {
940
- console.log(`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 300000)}`);
934
+ console.log(`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`);
941
935
  if (outgoingTerminationReason === null) {
942
936
  outgoingTerminationReason = 'timeout';
943
937
  this.incrementTerminationStat('outgoing', 'timeout');
@@ -946,8 +940,8 @@ export class PortProxy {
946
940
  });
947
941
 
948
942
  // Set appropriate timeouts using the configured value
949
- socket.setTimeout(this.settings.socketTimeout || 300000);
950
- targetSocket.setTimeout(this.settings.socketTimeout || 300000);
943
+ socket.setTimeout(this.settings.socketTimeout || 3600000);
944
+ targetSocket.setTimeout(this.settings.socketTimeout || 3600000);
951
945
 
952
946
  // Track outgoing data for bytes counting
953
947
  targetSocket.on('data', (chunk: Buffer) => {
@@ -984,7 +978,7 @@ export class PortProxy {
984
978
  console.log(
985
979
  `[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
986
980
  `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
987
- ` Protocol: ${connectionRecord.protocolType}`
981
+ ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
988
982
  );
989
983
  } else {
990
984
  console.log(
@@ -1003,7 +997,7 @@ export class PortProxy {
1003
997
  console.log(
1004
998
  `[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
1005
999
  `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
1006
- ` Protocol: ${connectionRecord.protocolType}`
1000
+ ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
1007
1001
  );
1008
1002
  } else {
1009
1003
  console.log(
@@ -1023,7 +1017,7 @@ export class PortProxy {
1023
1017
  if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
1024
1018
  try {
1025
1019
  // Try to extract SNI from potential renegotiation
1026
- const newSNI = extractSNI(renegChunk);
1020
+ const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
1027
1021
  if (newSNI && newSNI !== connectionRecord.lockedDomain) {
1028
1022
  console.log(`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`);
1029
1023
  initiateCleanupOnce('sni_mismatch');
@@ -1037,17 +1031,31 @@ export class PortProxy {
1037
1031
  });
1038
1032
  }
1039
1033
 
1040
- // Set protocol-specific timeout based on detected protocol
1034
+ // Set connection timeout
1041
1035
  if (connectionRecord.cleanupTimer) {
1042
1036
  clearTimeout(connectionRecord.cleanupTimer);
1043
1037
  }
1044
1038
 
1045
- // Set timeout based on protocol
1046
- const protocolTimeout = this.getProtocolTimeout(connectionRecord, domainConfig);
1039
+ // Set timeout based on domain config or default
1040
+ const connectionTimeout = this.getConnectionTimeout(connectionRecord);
1047
1041
  connectionRecord.cleanupTimer = setTimeout(() => {
1048
- console.log(`[${connectionId}] ${connectionRecord.protocolType} connection exceeded max lifetime (${plugins.prettyMs(protocolTimeout)}), forcing cleanup.`);
1049
- initiateCleanupOnce(`${connectionRecord.protocolType}_max_lifetime`);
1050
- }, protocolTimeout);
1042
+ console.log(`[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`);
1043
+ initiateCleanupOnce('connection_timeout');
1044
+ }, connectionTimeout);
1045
+
1046
+ // Make sure timeout doesn't keep the process alive
1047
+ if (connectionRecord.cleanupTimer.unref) {
1048
+ connectionRecord.cleanupTimer.unref();
1049
+ }
1050
+
1051
+ // Mark TLS handshake as complete for TLS connections
1052
+ if (connectionRecord.isTLS) {
1053
+ connectionRecord.tlsHandshakeComplete = true;
1054
+
1055
+ if (this.settings.enableTlsDebugLogging) {
1056
+ console.log(`[${connectionId}] TLS handshake complete for connection from ${remoteIP}`);
1057
+ }
1058
+ }
1051
1059
  });
1052
1060
  };
1053
1061
 
@@ -1055,7 +1063,7 @@ export class PortProxy {
1055
1063
  // Only apply port-based rules if the incoming port is within one of the global port ranges.
1056
1064
  if (this.settings.globalPortRanges && isPortInRanges(localPort, this.settings.globalPortRanges)) {
1057
1065
  if (this.settings.forwardAllGlobalRanges) {
1058
- if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
1066
+ if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
1059
1067
  console.log(`[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
1060
1068
  socket.end();
1061
1069
  return;
@@ -1111,7 +1119,20 @@ export class PortProxy {
1111
1119
  }
1112
1120
 
1113
1121
  initialDataReceived = true;
1114
- const serverName = extractSNI(chunk) || '';
1122
+
1123
+ // Try to extract SNI
1124
+ let serverName = '';
1125
+
1126
+ if (isTlsHandshake(chunk)) {
1127
+ connectionRecord.isTLS = true;
1128
+
1129
+ if (this.settings.enableTlsDebugLogging) {
1130
+ console.log(`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`);
1131
+ }
1132
+
1133
+ serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
1134
+ }
1135
+
1115
1136
  // Lock the connection to the negotiated SNI.
1116
1137
  connectionRecord.lockedDomain = serverName;
1117
1138
 
@@ -1123,9 +1144,12 @@ export class PortProxy {
1123
1144
  });
1124
1145
  } else {
1125
1146
  initialDataReceived = true;
1126
- if (!this.settings.defaultAllowedIPs || this.settings.defaultAllowedIPs.length === 0 || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
1147
+ connectionRecord.hasReceivedInitialData = true;
1148
+
1149
+ if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
1127
1150
  return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
1128
1151
  }
1152
+
1129
1153
  setupConnection('');
1130
1154
  }
1131
1155
  };
@@ -1167,11 +1191,10 @@ export class PortProxy {
1167
1191
  const now = Date.now();
1168
1192
  let maxIncoming = 0;
1169
1193
  let maxOutgoing = 0;
1170
- let httpConnections = 0;
1171
- let wsConnections = 0;
1172
1194
  let tlsConnections = 0;
1173
- let unknownConnections = 0;
1174
- let pooledConnections = 0;
1195
+ let nonTlsConnections = 0;
1196
+ let completedTlsHandshakes = 0;
1197
+ let pendingTlsHandshakes = 0;
1175
1198
 
1176
1199
  // Create a copy of the keys to avoid modification during iteration
1177
1200
  const connectionIds = [...this.connectionRecords.keys()];
@@ -1180,17 +1203,16 @@ export class PortProxy {
1180
1203
  const record = this.connectionRecords.get(id);
1181
1204
  if (!record) continue;
1182
1205
 
1183
- // Track connection stats by protocol
1184
- switch (record.protocolType) {
1185
- case 'http': httpConnections++; break;
1186
- case 'websocket': wsConnections++; break;
1187
- case 'tls':
1188
- case 'https': tlsConnections++; break;
1189
- default: unknownConnections++; break;
1190
- }
1191
-
1192
- if (record.isPooledConnection) {
1193
- pooledConnections++;
1206
+ // Track connection stats
1207
+ if (record.isTLS) {
1208
+ tlsConnections++;
1209
+ if (record.tlsHandshakeComplete) {
1210
+ completedTlsHandshakes++;
1211
+ } else {
1212
+ pendingTlsHandshakes++;
1213
+ }
1214
+ } else {
1215
+ nonTlsConnections++;
1194
1216
  }
1195
1217
 
1196
1218
  maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
@@ -1208,23 +1230,20 @@ export class PortProxy {
1208
1230
  this.cleanupConnection(record, 'parity_check');
1209
1231
  }
1210
1232
 
1233
+ // Check for stalled connections waiting for initial data
1234
+ if (!record.hasReceivedInitialData &&
1235
+ (now - record.incomingStartTime > this.settings.initialDataTimeout! / 2)) {
1236
+ console.log(`[${id}] Warning: Connection from ${record.remoteIP} has not received initial data after ${plugins.prettyMs(now - record.incomingStartTime)}`);
1237
+ }
1238
+
1211
1239
  // Skip inactivity check if disabled
1212
1240
  if (!this.settings.disableInactivityCheck) {
1213
- // Inactivity check - use protocol-specific values
1214
- let inactivityThreshold = Math.floor(Math.random() * (1800000 - 1200000 + 1)) + 1200000; // random between 20 and 30 minutes
1215
-
1216
- // Set protocol-specific inactivity thresholds
1217
- if (record.protocolType === 'http' && record.isPooledConnection) {
1218
- inactivityThreshold = this.settings.httpKeepAliveTimeout || 1200000; // 20 minutes for pooled HTTP
1219
- } else if (record.protocolType === 'websocket') {
1220
- inactivityThreshold = this.settings.wsConnectionTimeout || 14400000; // 4 hours for WebSocket
1221
- } else if (record.protocolType === 'http') {
1222
- inactivityThreshold = this.settings.httpConnectionTimeout || 1800000; // 30 minutes for HTTP
1223
- }
1241
+ // Inactivity check with configurable timeout
1242
+ const inactivityThreshold = this.settings.inactivityTimeout!;
1224
1243
 
1225
1244
  const inactivityTime = now - record.lastActivity;
1226
1245
  if (inactivityTime > inactivityThreshold && !record.connectionClosed) {
1227
- console.log(`[${id}] Inactivity check: No activity on ${record.protocolType} connection from ${record.remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
1246
+ console.log(`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
1228
1247
  this.cleanupConnection(record, 'inactivity');
1229
1248
  }
1230
1249
  }
@@ -1233,11 +1252,11 @@ export class PortProxy {
1233
1252
  // Log detailed stats periodically
1234
1253
  console.log(
1235
1254
  `Active connections: ${this.connectionRecords.size}. ` +
1236
- `Types: HTTP=${httpConnections}, WS=${wsConnections}, TLS=${tlsConnections}, Unknown=${unknownConnections}, Pooled=${pooledConnections}. ` +
1255
+ `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), Non-TLS=${nonTlsConnections}. ` +
1237
1256
  `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` +
1238
1257
  `Termination stats: ${JSON.stringify({IN: this.terminationStats.incoming, OUT: this.terminationStats.outgoing})}`
1239
1258
  );
1240
- }, this.settings.inactivityCheckInterval || 30000);
1259
+ }, this.settings.inactivityCheckInterval || 60000);
1241
1260
 
1242
1261
  // Make sure the interval doesn't keep the process alive
1243
1262
  if (this.connectionLogger.unref) {