@push.rocks/smartproxy 3.28.5 → 3.29.0
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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.iptablesproxy.d.ts +79 -7
- package/dist_ts/classes.iptablesproxy.js +662 -67
- package/dist_ts/classes.networkproxy.d.ts +46 -1
- package/dist_ts/classes.networkproxy.js +347 -8
- package/dist_ts/classes.portproxy.d.ts +43 -4
- package/dist_ts/classes.portproxy.js +573 -354
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.iptablesproxy.ts +786 -68
- package/ts/classes.networkproxy.ts +417 -7
- package/ts/classes.portproxy.ts +789 -469
package/ts/classes.portproxy.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { NetworkProxy } from './classes.networkproxy.js';
|
|
2
3
|
|
|
3
4
|
/** Domain configuration with per-domain allowed port ranges */
|
|
4
5
|
export interface IDomainConfig {
|
|
@@ -9,6 +10,10 @@ export interface IDomainConfig {
|
|
|
9
10
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
10
11
|
// Allow domain-specific timeout override
|
|
11
12
|
connectionTimeout?: number; // Connection timeout override (ms)
|
|
13
|
+
|
|
14
|
+
// New properties for NetworkProxy integration
|
|
15
|
+
useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
|
|
16
|
+
networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0)
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
/** Port proxy settings including global allowed port ranges */
|
|
@@ -26,8 +31,8 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
26
31
|
initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
|
|
27
32
|
socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
|
|
28
33
|
inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
|
|
29
|
-
maxConnectionLifetime?: number; // Default max connection lifetime (ms), default:
|
|
30
|
-
inactivityTimeout?: number; // Inactivity timeout (ms), default:
|
|
34
|
+
maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h)
|
|
35
|
+
inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h)
|
|
31
36
|
|
|
32
37
|
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
|
|
33
38
|
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
|
@@ -49,6 +54,14 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
49
54
|
// Rate limiting and security
|
|
50
55
|
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
|
|
51
56
|
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
|
|
57
|
+
|
|
58
|
+
// Enhanced keep-alive settings
|
|
59
|
+
keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
|
|
60
|
+
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
|
|
61
|
+
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
|
62
|
+
|
|
63
|
+
// New property for NetworkProxy integration
|
|
64
|
+
networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
/**
|
|
@@ -77,6 +90,16 @@ interface IConnectionRecord {
|
|
|
77
90
|
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
|
|
78
91
|
hasReceivedInitialData: boolean; // Whether initial data has been received
|
|
79
92
|
domainConfig?: IDomainConfig; // Associated domain config for this connection
|
|
93
|
+
|
|
94
|
+
// Keep-alive tracking
|
|
95
|
+
hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
|
|
96
|
+
inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
|
|
97
|
+
incomingTerminationReason?: string | null; // Reason for incoming termination
|
|
98
|
+
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
|
99
|
+
|
|
100
|
+
// New field for NetworkProxy tracking
|
|
101
|
+
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
|
102
|
+
networkProxyIndex?: number; // Which NetworkProxy instance is being used
|
|
80
103
|
}
|
|
81
104
|
|
|
82
105
|
/**
|
|
@@ -325,6 +348,9 @@ export class PortProxy {
|
|
|
325
348
|
// Connection tracking by IP for rate limiting
|
|
326
349
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
327
350
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
351
|
+
|
|
352
|
+
// New property to store NetworkProxy instances
|
|
353
|
+
private networkProxies: NetworkProxy[] = [];
|
|
328
354
|
|
|
329
355
|
constructor(settingsArg: IPortProxySettings) {
|
|
330
356
|
// Set reasonable defaults for all settings
|
|
@@ -332,11 +358,11 @@ export class PortProxy {
|
|
|
332
358
|
...settingsArg,
|
|
333
359
|
targetIP: settingsArg.targetIP || 'localhost',
|
|
334
360
|
|
|
335
|
-
// Timeout settings with
|
|
361
|
+
// Timeout settings with reasonable defaults
|
|
336
362
|
initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake
|
|
337
|
-
socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout ||
|
|
363
|
+
socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 3600000), // 1 hour socket timeout
|
|
338
364
|
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
|
|
339
|
-
maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime ||
|
|
365
|
+
maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 86400000), // 24 hours default
|
|
340
366
|
inactivityTimeout: ensureSafeTimeout(settingsArg.inactivityTimeout || 14400000), // 4 hours inactivity timeout
|
|
341
367
|
|
|
342
368
|
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
|
|
@@ -344,20 +370,526 @@ export class PortProxy {
|
|
|
344
370
|
// Socket optimization settings
|
|
345
371
|
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
346
372
|
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
347
|
-
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay ||
|
|
373
|
+
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness)
|
|
348
374
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
|
|
349
375
|
|
|
350
376
|
// Feature flags
|
|
351
377
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
352
|
-
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes
|
|
378
|
+
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
|
|
379
|
+
? settingsArg.enableKeepAliveProbes : true, // Enable by default
|
|
353
380
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
354
381
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
355
|
-
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts ||
|
|
382
|
+
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
|
|
356
383
|
|
|
357
384
|
// Rate limiting defaults
|
|
358
385
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
|
|
359
386
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
|
|
387
|
+
|
|
388
|
+
// Enhanced keep-alive settings
|
|
389
|
+
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
|
|
390
|
+
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
|
|
391
|
+
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
360
392
|
};
|
|
393
|
+
|
|
394
|
+
// Store NetworkProxy instances if provided
|
|
395
|
+
this.networkProxies = settingsArg.networkProxies || [];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Forwards a TLS connection to a NetworkProxy for handling
|
|
400
|
+
* @param connectionId - Unique connection identifier
|
|
401
|
+
* @param socket - The incoming client socket
|
|
402
|
+
* @param record - The connection record
|
|
403
|
+
* @param domainConfig - The domain configuration
|
|
404
|
+
* @param initialData - Initial data chunk (TLS ClientHello)
|
|
405
|
+
* @param serverName - SNI hostname (if available)
|
|
406
|
+
*/
|
|
407
|
+
private forwardToNetworkProxy(
|
|
408
|
+
connectionId: string,
|
|
409
|
+
socket: plugins.net.Socket,
|
|
410
|
+
record: IConnectionRecord,
|
|
411
|
+
domainConfig: IDomainConfig,
|
|
412
|
+
initialData: Buffer,
|
|
413
|
+
serverName?: string
|
|
414
|
+
): void {
|
|
415
|
+
// Determine which NetworkProxy to use
|
|
416
|
+
const proxyIndex = domainConfig.networkProxyIndex !== undefined
|
|
417
|
+
? domainConfig.networkProxyIndex
|
|
418
|
+
: 0;
|
|
419
|
+
|
|
420
|
+
// Validate the NetworkProxy index
|
|
421
|
+
if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
|
|
422
|
+
console.log(`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`);
|
|
423
|
+
// Fall back to direct connection
|
|
424
|
+
return this.setupDirectConnection(connectionId, socket, record, domainConfig, serverName, initialData);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const networkProxy = this.networkProxies[proxyIndex];
|
|
428
|
+
const proxyPort = networkProxy.getListeningPort();
|
|
429
|
+
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
|
430
|
+
|
|
431
|
+
if (this.settings.enableDetailedLogging) {
|
|
432
|
+
console.log(
|
|
433
|
+
`[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Create a connection to the NetworkProxy
|
|
438
|
+
const proxySocket = plugins.net.connect({
|
|
439
|
+
host: proxyHost,
|
|
440
|
+
port: proxyPort
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Store the outgoing socket in the record
|
|
444
|
+
record.outgoing = proxySocket;
|
|
445
|
+
record.outgoingStartTime = Date.now();
|
|
446
|
+
record.usingNetworkProxy = true;
|
|
447
|
+
record.networkProxyIndex = proxyIndex;
|
|
448
|
+
|
|
449
|
+
// Set up error handlers
|
|
450
|
+
proxySocket.on('error', (err) => {
|
|
451
|
+
console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
|
|
452
|
+
this.cleanupConnection(record, 'network_proxy_connect_error');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Handle connection to NetworkProxy
|
|
456
|
+
proxySocket.on('connect', () => {
|
|
457
|
+
if (this.settings.enableDetailedLogging) {
|
|
458
|
+
console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// First send the initial data that contains the TLS ClientHello
|
|
462
|
+
proxySocket.write(initialData);
|
|
463
|
+
|
|
464
|
+
// Now set up bidirectional piping between client and NetworkProxy
|
|
465
|
+
socket.pipe(proxySocket);
|
|
466
|
+
proxySocket.pipe(socket);
|
|
467
|
+
|
|
468
|
+
// Setup cleanup handlers
|
|
469
|
+
proxySocket.on('close', () => {
|
|
470
|
+
if (this.settings.enableDetailedLogging) {
|
|
471
|
+
console.log(`[${connectionId}] NetworkProxy connection closed`);
|
|
472
|
+
}
|
|
473
|
+
this.cleanupConnection(record, 'network_proxy_closed');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
socket.on('close', () => {
|
|
477
|
+
if (this.settings.enableDetailedLogging) {
|
|
478
|
+
console.log(`[${connectionId}] Client connection closed after forwarding to NetworkProxy`);
|
|
479
|
+
}
|
|
480
|
+
this.cleanupConnection(record, 'client_closed');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Update activity on data transfer
|
|
484
|
+
socket.on('data', () => this.updateActivity(record));
|
|
485
|
+
proxySocket.on('data', () => this.updateActivity(record));
|
|
486
|
+
|
|
487
|
+
if (this.settings.enableDetailedLogging) {
|
|
488
|
+
console.log(
|
|
489
|
+
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Sets up a direct connection to the target (original behavior)
|
|
497
|
+
* This is used when NetworkProxy isn't configured or as a fallback
|
|
498
|
+
*/
|
|
499
|
+
private setupDirectConnection(
|
|
500
|
+
connectionId: string,
|
|
501
|
+
socket: plugins.net.Socket,
|
|
502
|
+
record: IConnectionRecord,
|
|
503
|
+
domainConfig: IDomainConfig | undefined,
|
|
504
|
+
serverName?: string,
|
|
505
|
+
initialChunk?: Buffer,
|
|
506
|
+
overridePort?: number
|
|
507
|
+
): void {
|
|
508
|
+
// Existing connection setup logic
|
|
509
|
+
const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
|
|
510
|
+
const connectionOptions: plugins.net.NetConnectOpts = {
|
|
511
|
+
host: targetHost,
|
|
512
|
+
port: overridePort !== undefined ? overridePort : this.settings.toPort,
|
|
513
|
+
};
|
|
514
|
+
if (this.settings.preserveSourceIP) {
|
|
515
|
+
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Pause the incoming socket to prevent buffer overflows
|
|
519
|
+
socket.pause();
|
|
520
|
+
|
|
521
|
+
// Temporary handler to collect data during connection setup
|
|
522
|
+
const tempDataHandler = (chunk: Buffer) => {
|
|
523
|
+
// Track bytes received
|
|
524
|
+
record.bytesReceived += chunk.length;
|
|
525
|
+
|
|
526
|
+
// Check for TLS handshake
|
|
527
|
+
if (!record.isTLS && isTlsHandshake(chunk)) {
|
|
528
|
+
record.isTLS = true;
|
|
529
|
+
|
|
530
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
531
|
+
console.log(
|
|
532
|
+
`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Check if adding this chunk would exceed the buffer limit
|
|
538
|
+
const newSize = record.pendingDataSize + chunk.length;
|
|
539
|
+
|
|
540
|
+
if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
|
|
541
|
+
console.log(
|
|
542
|
+
`[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
|
|
543
|
+
);
|
|
544
|
+
socket.end(); // Gracefully close the socket
|
|
545
|
+
return this.initiateCleanupOnce(record, 'buffer_limit_exceeded');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Buffer the chunk and update the size counter
|
|
549
|
+
record.pendingData.push(Buffer.from(chunk));
|
|
550
|
+
record.pendingDataSize = newSize;
|
|
551
|
+
this.updateActivity(record);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// Add the temp handler to capture all incoming data during connection setup
|
|
555
|
+
socket.on('data', tempDataHandler);
|
|
556
|
+
|
|
557
|
+
// Add initial chunk to pending data if present
|
|
558
|
+
if (initialChunk) {
|
|
559
|
+
record.bytesReceived += initialChunk.length;
|
|
560
|
+
record.pendingData.push(Buffer.from(initialChunk));
|
|
561
|
+
record.pendingDataSize = initialChunk.length;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Create the target socket but don't set up piping immediately
|
|
565
|
+
const targetSocket = plugins.net.connect(connectionOptions);
|
|
566
|
+
record.outgoing = targetSocket;
|
|
567
|
+
record.outgoingStartTime = Date.now();
|
|
568
|
+
|
|
569
|
+
// Apply socket optimizations
|
|
570
|
+
targetSocket.setNoDelay(this.settings.noDelay);
|
|
571
|
+
|
|
572
|
+
// Apply keep-alive settings to the outgoing connection as well
|
|
573
|
+
if (this.settings.keepAlive) {
|
|
574
|
+
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
575
|
+
|
|
576
|
+
// Apply enhanced TCP keep-alive options if enabled
|
|
577
|
+
if (this.settings.enableKeepAliveProbes) {
|
|
578
|
+
try {
|
|
579
|
+
if ('setKeepAliveProbes' in targetSocket) {
|
|
580
|
+
(targetSocket as any).setKeepAliveProbes(10);
|
|
581
|
+
}
|
|
582
|
+
if ('setKeepAliveInterval' in targetSocket) {
|
|
583
|
+
(targetSocket as any).setKeepAliveInterval(1000);
|
|
584
|
+
}
|
|
585
|
+
} catch (err) {
|
|
586
|
+
// Ignore errors - these are optional enhancements
|
|
587
|
+
if (this.settings.enableDetailedLogging) {
|
|
588
|
+
console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Setup specific error handler for connection phase
|
|
595
|
+
targetSocket.once('error', (err) => {
|
|
596
|
+
// This handler runs only once during the initial connection phase
|
|
597
|
+
const code = (err as any).code;
|
|
598
|
+
console.log(
|
|
599
|
+
`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
// Resume the incoming socket to prevent it from hanging
|
|
603
|
+
socket.resume();
|
|
604
|
+
|
|
605
|
+
if (code === 'ECONNREFUSED') {
|
|
606
|
+
console.log(
|
|
607
|
+
`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
|
|
608
|
+
);
|
|
609
|
+
} else if (code === 'ETIMEDOUT') {
|
|
610
|
+
console.log(
|
|
611
|
+
`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`
|
|
612
|
+
);
|
|
613
|
+
} else if (code === 'ECONNRESET') {
|
|
614
|
+
console.log(
|
|
615
|
+
`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`
|
|
616
|
+
);
|
|
617
|
+
} else if (code === 'EHOSTUNREACH') {
|
|
618
|
+
console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Clear any existing error handler after connection phase
|
|
622
|
+
targetSocket.removeAllListeners('error');
|
|
623
|
+
|
|
624
|
+
// Re-add the normal error handler for established connections
|
|
625
|
+
targetSocket.on('error', this.handleError('outgoing', record));
|
|
626
|
+
|
|
627
|
+
if (record.outgoingTerminationReason === null) {
|
|
628
|
+
record.outgoingTerminationReason = 'connection_failed';
|
|
629
|
+
this.incrementTerminationStat('outgoing', 'connection_failed');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Clean up the connection
|
|
633
|
+
this.initiateCleanupOnce(record, `connection_failed_${code}`);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Setup close handler
|
|
637
|
+
targetSocket.on('close', this.handleClose('outgoing', record));
|
|
638
|
+
socket.on('close', this.handleClose('incoming', record));
|
|
639
|
+
|
|
640
|
+
// Handle timeouts with keep-alive awareness
|
|
641
|
+
socket.on('timeout', () => {
|
|
642
|
+
// For keep-alive connections, just log a warning instead of closing
|
|
643
|
+
if (record.hasKeepAlive) {
|
|
644
|
+
console.log(
|
|
645
|
+
`[${connectionId}] Timeout event on incoming keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
|
|
646
|
+
this.settings.socketTimeout || 3600000
|
|
647
|
+
)}. Connection preserved.`
|
|
648
|
+
);
|
|
649
|
+
// Don't close the connection - just log
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// For non-keep-alive connections, proceed with normal cleanup
|
|
654
|
+
console.log(
|
|
655
|
+
`[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs(
|
|
656
|
+
this.settings.socketTimeout || 3600000
|
|
657
|
+
)}`
|
|
658
|
+
);
|
|
659
|
+
if (record.incomingTerminationReason === null) {
|
|
660
|
+
record.incomingTerminationReason = 'timeout';
|
|
661
|
+
this.incrementTerminationStat('incoming', 'timeout');
|
|
662
|
+
}
|
|
663
|
+
this.initiateCleanupOnce(record, 'timeout_incoming');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
targetSocket.on('timeout', () => {
|
|
667
|
+
// For keep-alive connections, just log a warning instead of closing
|
|
668
|
+
if (record.hasKeepAlive) {
|
|
669
|
+
console.log(
|
|
670
|
+
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
|
|
671
|
+
this.settings.socketTimeout || 3600000
|
|
672
|
+
)}. Connection preserved.`
|
|
673
|
+
);
|
|
674
|
+
// Don't close the connection - just log
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// For non-keep-alive connections, proceed with normal cleanup
|
|
679
|
+
console.log(
|
|
680
|
+
`[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs(
|
|
681
|
+
this.settings.socketTimeout || 3600000
|
|
682
|
+
)}`
|
|
683
|
+
);
|
|
684
|
+
if (record.outgoingTerminationReason === null) {
|
|
685
|
+
record.outgoingTerminationReason = 'timeout';
|
|
686
|
+
this.incrementTerminationStat('outgoing', 'timeout');
|
|
687
|
+
}
|
|
688
|
+
this.initiateCleanupOnce(record, 'timeout_outgoing');
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Set appropriate timeouts, or disable for immortal keep-alive connections
|
|
692
|
+
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
|
693
|
+
// Disable timeouts completely for immortal connections
|
|
694
|
+
socket.setTimeout(0);
|
|
695
|
+
targetSocket.setTimeout(0);
|
|
696
|
+
|
|
697
|
+
if (this.settings.enableDetailedLogging) {
|
|
698
|
+
console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`);
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
// Set normal timeouts for other connections
|
|
702
|
+
socket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
|
|
703
|
+
targetSocket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Track outgoing data for bytes counting
|
|
707
|
+
targetSocket.on('data', (chunk: Buffer) => {
|
|
708
|
+
record.bytesSent += chunk.length;
|
|
709
|
+
this.updateActivity(record);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Wait for the outgoing connection to be ready before setting up piping
|
|
713
|
+
targetSocket.once('connect', () => {
|
|
714
|
+
// Clear the initial connection error handler
|
|
715
|
+
targetSocket.removeAllListeners('error');
|
|
716
|
+
|
|
717
|
+
// Add the normal error handler for established connections
|
|
718
|
+
targetSocket.on('error', this.handleError('outgoing', record));
|
|
719
|
+
|
|
720
|
+
// Remove temporary data handler
|
|
721
|
+
socket.removeListener('data', tempDataHandler);
|
|
722
|
+
|
|
723
|
+
// Flush all pending data to target
|
|
724
|
+
if (record.pendingData.length > 0) {
|
|
725
|
+
const combinedData = Buffer.concat(record.pendingData);
|
|
726
|
+
targetSocket.write(combinedData, (err) => {
|
|
727
|
+
if (err) {
|
|
728
|
+
console.log(
|
|
729
|
+
`[${connectionId}] Error writing pending data to target: ${err.message}`
|
|
730
|
+
);
|
|
731
|
+
return this.initiateCleanupOnce(record, 'write_error');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Now set up piping for future data and resume the socket
|
|
735
|
+
socket.pipe(targetSocket);
|
|
736
|
+
targetSocket.pipe(socket);
|
|
737
|
+
socket.resume(); // Resume the socket after piping is established
|
|
738
|
+
|
|
739
|
+
if (this.settings.enableDetailedLogging) {
|
|
740
|
+
console.log(
|
|
741
|
+
`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
742
|
+
`${
|
|
743
|
+
serverName
|
|
744
|
+
? ` (SNI: ${serverName})`
|
|
745
|
+
: domainConfig
|
|
746
|
+
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
747
|
+
: ''
|
|
748
|
+
}` +
|
|
749
|
+
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
|
|
750
|
+
);
|
|
751
|
+
} else {
|
|
752
|
+
console.log(
|
|
753
|
+
`Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
754
|
+
`${
|
|
755
|
+
serverName
|
|
756
|
+
? ` (SNI: ${serverName})`
|
|
757
|
+
: domainConfig
|
|
758
|
+
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
759
|
+
: ''
|
|
760
|
+
}`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
} else {
|
|
765
|
+
// No pending data, so just set up piping
|
|
766
|
+
socket.pipe(targetSocket);
|
|
767
|
+
targetSocket.pipe(socket);
|
|
768
|
+
socket.resume(); // Resume the socket after piping is established
|
|
769
|
+
|
|
770
|
+
if (this.settings.enableDetailedLogging) {
|
|
771
|
+
console.log(
|
|
772
|
+
`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
773
|
+
`${
|
|
774
|
+
serverName
|
|
775
|
+
? ` (SNI: ${serverName})`
|
|
776
|
+
: domainConfig
|
|
777
|
+
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
778
|
+
: ''
|
|
779
|
+
}` +
|
|
780
|
+
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
|
|
781
|
+
);
|
|
782
|
+
} else {
|
|
783
|
+
console.log(
|
|
784
|
+
`Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
785
|
+
`${
|
|
786
|
+
serverName
|
|
787
|
+
? ` (SNI: ${serverName})`
|
|
788
|
+
: domainConfig
|
|
789
|
+
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
790
|
+
: ''
|
|
791
|
+
}`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Clear the buffer now that we've processed it
|
|
797
|
+
record.pendingData = [];
|
|
798
|
+
record.pendingDataSize = 0;
|
|
799
|
+
|
|
800
|
+
// Add the renegotiation listener for SNI validation
|
|
801
|
+
if (serverName) {
|
|
802
|
+
socket.on('data', (renegChunk: Buffer) => {
|
|
803
|
+
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
804
|
+
try {
|
|
805
|
+
// Try to extract SNI from potential renegotiation
|
|
806
|
+
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
|
807
|
+
if (newSNI && newSNI !== record.lockedDomain) {
|
|
808
|
+
console.log(
|
|
809
|
+
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.`
|
|
810
|
+
);
|
|
811
|
+
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
812
|
+
} else if (newSNI && this.settings.enableDetailedLogging) {
|
|
813
|
+
console.log(
|
|
814
|
+
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
} catch (err) {
|
|
818
|
+
console.log(
|
|
819
|
+
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Set connection timeout with simpler logic
|
|
827
|
+
if (record.cleanupTimer) {
|
|
828
|
+
clearTimeout(record.cleanupTimer);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// For immortal keep-alive connections, skip setting a timeout completely
|
|
832
|
+
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
|
833
|
+
if (this.settings.enableDetailedLogging) {
|
|
834
|
+
console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`);
|
|
835
|
+
}
|
|
836
|
+
// No cleanup timer for immortal connections
|
|
837
|
+
}
|
|
838
|
+
// For extended keep-alive connections, use extended timeout
|
|
839
|
+
else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
|
840
|
+
const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
841
|
+
const safeTimeout = ensureSafeTimeout(extendedTimeout);
|
|
842
|
+
|
|
843
|
+
record.cleanupTimer = setTimeout(() => {
|
|
844
|
+
console.log(
|
|
845
|
+
`[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs(
|
|
846
|
+
extendedTimeout
|
|
847
|
+
)}), forcing cleanup.`
|
|
848
|
+
);
|
|
849
|
+
this.initiateCleanupOnce(record, 'extended_lifetime');
|
|
850
|
+
}, safeTimeout);
|
|
851
|
+
|
|
852
|
+
// Make sure timeout doesn't keep the process alive
|
|
853
|
+
if (record.cleanupTimer.unref) {
|
|
854
|
+
record.cleanupTimer.unref();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (this.settings.enableDetailedLogging) {
|
|
858
|
+
console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// For standard connections, use normal timeout
|
|
862
|
+
else {
|
|
863
|
+
// Use domain-specific timeout if available, otherwise use default
|
|
864
|
+
const connectionTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
|
|
865
|
+
const safeTimeout = ensureSafeTimeout(connectionTimeout);
|
|
866
|
+
|
|
867
|
+
record.cleanupTimer = setTimeout(() => {
|
|
868
|
+
console.log(
|
|
869
|
+
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs(
|
|
870
|
+
connectionTimeout
|
|
871
|
+
)}), forcing cleanup.`
|
|
872
|
+
);
|
|
873
|
+
this.initiateCleanupOnce(record, 'connection_timeout');
|
|
874
|
+
}, safeTimeout);
|
|
875
|
+
|
|
876
|
+
// Make sure timeout doesn't keep the process alive
|
|
877
|
+
if (record.cleanupTimer.unref) {
|
|
878
|
+
record.cleanupTimer.unref();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Mark TLS handshake as complete for TLS connections
|
|
883
|
+
if (record.isTLS) {
|
|
884
|
+
record.tlsHandshakeComplete = true;
|
|
885
|
+
|
|
886
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
887
|
+
console.log(
|
|
888
|
+
`[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
});
|
|
361
893
|
}
|
|
362
894
|
|
|
363
895
|
/**
|
|
@@ -418,25 +950,6 @@ export class PortProxy {
|
|
|
418
950
|
this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
|
|
419
951
|
}
|
|
420
952
|
|
|
421
|
-
/**
|
|
422
|
-
* Get connection timeout based on domain config or default settings
|
|
423
|
-
*/
|
|
424
|
-
private getConnectionTimeout(record: IConnectionRecord): number {
|
|
425
|
-
// If the connection has a domain-specific timeout, use that with safety check
|
|
426
|
-
if (record.domainConfig?.connectionTimeout) {
|
|
427
|
-
return ensureSafeTimeout(record.domainConfig.connectionTimeout);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Use default timeout, potentially randomized with safety check
|
|
431
|
-
const baseTimeout = this.settings.maxConnectionLifetime!;
|
|
432
|
-
|
|
433
|
-
if (this.settings.enableRandomizedTimeouts) {
|
|
434
|
-
return randomizeTimeout(baseTimeout);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
return ensureSafeTimeout(baseTimeout);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
953
|
/**
|
|
441
954
|
* Cleans up a connection record.
|
|
442
955
|
* Destroys both incoming and outgoing sockets, clears timers, and removes the record.
|
|
@@ -534,7 +1047,8 @@ export class PortProxy {
|
|
|
534
1047
|
` Duration: ${plugins.prettyMs(
|
|
535
1048
|
duration
|
|
536
1049
|
)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
|
|
537
|
-
`TLS: ${record.isTLS ? 'Yes' : 'No'}`
|
|
1050
|
+
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
|
|
1051
|
+
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}`
|
|
538
1052
|
);
|
|
539
1053
|
} else {
|
|
540
1054
|
console.log(
|
|
@@ -549,6 +1063,11 @@ export class PortProxy {
|
|
|
549
1063
|
*/
|
|
550
1064
|
private updateActivity(record: IConnectionRecord): void {
|
|
551
1065
|
record.lastActivity = Date.now();
|
|
1066
|
+
|
|
1067
|
+
// Clear any inactivity warning
|
|
1068
|
+
if (record.inactivityWarningIssued) {
|
|
1069
|
+
record.inactivityWarningIssued = false;
|
|
1070
|
+
}
|
|
552
1071
|
}
|
|
553
1072
|
|
|
554
1073
|
/**
|
|
@@ -563,6 +1082,97 @@ export class PortProxy {
|
|
|
563
1082
|
}
|
|
564
1083
|
return this.settings.targetIP!;
|
|
565
1084
|
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Initiates cleanup once for a connection
|
|
1088
|
+
*/
|
|
1089
|
+
private initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
|
|
1090
|
+
if (this.settings.enableDetailedLogging) {
|
|
1091
|
+
console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) {
|
|
1095
|
+
record.incomingTerminationReason = reason;
|
|
1096
|
+
this.incrementTerminationStat('incoming', reason);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
this.cleanupConnection(record, reason);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Creates a generic error handler for incoming or outgoing sockets
|
|
1104
|
+
*/
|
|
1105
|
+
private handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
|
1106
|
+
return (err: Error) => {
|
|
1107
|
+
const code = (err as any).code;
|
|
1108
|
+
let reason = 'error';
|
|
1109
|
+
|
|
1110
|
+
const now = Date.now();
|
|
1111
|
+
const connectionDuration = now - record.incomingStartTime;
|
|
1112
|
+
const lastActivityAge = now - record.lastActivity;
|
|
1113
|
+
|
|
1114
|
+
if (code === 'ECONNRESET') {
|
|
1115
|
+
reason = 'econnreset';
|
|
1116
|
+
console.log(
|
|
1117
|
+
`[${record.id}] ECONNRESET on ${side} side from ${record.remoteIP}: ${
|
|
1118
|
+
err.message
|
|
1119
|
+
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
1120
|
+
lastActivityAge
|
|
1121
|
+
)} ago`
|
|
1122
|
+
);
|
|
1123
|
+
} else if (code === 'ETIMEDOUT') {
|
|
1124
|
+
reason = 'etimedout';
|
|
1125
|
+
console.log(
|
|
1126
|
+
`[${record.id}] ETIMEDOUT on ${side} side from ${record.remoteIP}: ${
|
|
1127
|
+
err.message
|
|
1128
|
+
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
1129
|
+
lastActivityAge
|
|
1130
|
+
)} ago`
|
|
1131
|
+
);
|
|
1132
|
+
} else {
|
|
1133
|
+
console.log(
|
|
1134
|
+
`[${record.id}] Error on ${side} side from ${record.remoteIP}: ${
|
|
1135
|
+
err.message
|
|
1136
|
+
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
1137
|
+
lastActivityAge
|
|
1138
|
+
)} ago`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (side === 'incoming' && record.incomingTerminationReason === null) {
|
|
1143
|
+
record.incomingTerminationReason = reason;
|
|
1144
|
+
this.incrementTerminationStat('incoming', reason);
|
|
1145
|
+
} else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
|
|
1146
|
+
record.outgoingTerminationReason = reason;
|
|
1147
|
+
this.incrementTerminationStat('outgoing', reason);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
this.initiateCleanupOnce(record, reason);
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Creates a generic close handler for incoming or outgoing sockets
|
|
1156
|
+
*/
|
|
1157
|
+
private handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
|
1158
|
+
return () => {
|
|
1159
|
+
if (this.settings.enableDetailedLogging) {
|
|
1160
|
+
console.log(`[${record.id}] Connection closed on ${side} side from ${record.remoteIP}`);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (side === 'incoming' && record.incomingTerminationReason === null) {
|
|
1164
|
+
record.incomingTerminationReason = 'normal';
|
|
1165
|
+
this.incrementTerminationStat('incoming', 'normal');
|
|
1166
|
+
} else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
|
|
1167
|
+
record.outgoingTerminationReason = 'normal';
|
|
1168
|
+
this.incrementTerminationStat('outgoing', 'normal');
|
|
1169
|
+
// Record the time when outgoing socket closed.
|
|
1170
|
+
record.outgoingClosedTime = Date.now();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
this.initiateCleanupOnce(record, 'closed_' + side);
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
566
1176
|
|
|
567
1177
|
/**
|
|
568
1178
|
* Main method to start the proxy
|
|
@@ -609,25 +1219,7 @@ export class PortProxy {
|
|
|
609
1219
|
|
|
610
1220
|
// Apply socket optimizations
|
|
611
1221
|
socket.setNoDelay(this.settings.noDelay);
|
|
612
|
-
|
|
613
|
-
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// Apply enhanced TCP options if available
|
|
617
|
-
if (this.settings.enableKeepAliveProbes) {
|
|
618
|
-
try {
|
|
619
|
-
// These are platform-specific and may not be available
|
|
620
|
-
if ('setKeepAliveProbes' in socket) {
|
|
621
|
-
(socket as any).setKeepAliveProbes(10);
|
|
622
|
-
}
|
|
623
|
-
if ('setKeepAliveInterval' in socket) {
|
|
624
|
-
(socket as any).setKeepAliveInterval(1000);
|
|
625
|
-
}
|
|
626
|
-
} catch (err) {
|
|
627
|
-
// Ignore errors - these are optional enhancements
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
1222
|
+
|
|
631
1223
|
// Create a unique connection ID and record
|
|
632
1224
|
const connectionId = generateConnectionId();
|
|
633
1225
|
const connectionRecord: IConnectionRecord = {
|
|
@@ -648,7 +1240,37 @@ export class PortProxy {
|
|
|
648
1240
|
isTLS: false,
|
|
649
1241
|
tlsHandshakeComplete: false,
|
|
650
1242
|
hasReceivedInitialData: false,
|
|
1243
|
+
hasKeepAlive: false, // Will set to true if keep-alive is applied
|
|
1244
|
+
incomingTerminationReason: null,
|
|
1245
|
+
outgoingTerminationReason: null,
|
|
1246
|
+
|
|
1247
|
+
// Initialize NetworkProxy tracking fields
|
|
1248
|
+
usingNetworkProxy: false
|
|
651
1249
|
};
|
|
1250
|
+
|
|
1251
|
+
// Apply keep-alive settings if enabled
|
|
1252
|
+
if (this.settings.keepAlive) {
|
|
1253
|
+
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
1254
|
+
connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
|
|
1255
|
+
|
|
1256
|
+
// Apply enhanced TCP keep-alive options if enabled
|
|
1257
|
+
if (this.settings.enableKeepAliveProbes) {
|
|
1258
|
+
try {
|
|
1259
|
+
// These are platform-specific and may not be available
|
|
1260
|
+
if ('setKeepAliveProbes' in socket) {
|
|
1261
|
+
(socket as any).setKeepAliveProbes(10); // More aggressive probing
|
|
1262
|
+
}
|
|
1263
|
+
if ('setKeepAliveInterval' in socket) {
|
|
1264
|
+
(socket as any).setKeepAliveInterval(1000); // 1 second interval between probes
|
|
1265
|
+
}
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
// Ignore errors - these are optional enhancements
|
|
1268
|
+
if (this.settings.enableDetailedLogging) {
|
|
1269
|
+
console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
652
1274
|
|
|
653
1275
|
// Track connection by IP
|
|
654
1276
|
this.trackConnectionByIP(remoteIP, connectionId);
|
|
@@ -656,7 +1278,9 @@ export class PortProxy {
|
|
|
656
1278
|
|
|
657
1279
|
if (this.settings.enableDetailedLogging) {
|
|
658
1280
|
console.log(
|
|
659
|
-
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}.
|
|
1281
|
+
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
|
|
1282
|
+
`Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
|
1283
|
+
`Active connections: ${this.connectionRecords.size}`
|
|
660
1284
|
);
|
|
661
1285
|
} else {
|
|
662
1286
|
console.log(
|
|
@@ -665,35 +1289,16 @@ export class PortProxy {
|
|
|
665
1289
|
}
|
|
666
1290
|
|
|
667
1291
|
let initialDataReceived = false;
|
|
668
|
-
let incomingTerminationReason: string | null = null;
|
|
669
|
-
let outgoingTerminationReason: string | null = null;
|
|
670
|
-
|
|
671
|
-
// Local function for cleanupOnce
|
|
672
|
-
const cleanupOnce = () => {
|
|
673
|
-
this.cleanupConnection(connectionRecord);
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
// Define initiateCleanupOnce for compatibility
|
|
677
|
-
const initiateCleanupOnce = (reason: string = 'normal') => {
|
|
678
|
-
if (this.settings.enableDetailedLogging) {
|
|
679
|
-
console.log(`[${connectionId}] Connection cleanup initiated for ${remoteIP} (${reason})`);
|
|
680
|
-
}
|
|
681
|
-
if (incomingTerminationReason === null) {
|
|
682
|
-
incomingTerminationReason = reason;
|
|
683
|
-
this.incrementTerminationStat('incoming', reason);
|
|
684
|
-
}
|
|
685
|
-
cleanupOnce();
|
|
686
|
-
};
|
|
687
1292
|
|
|
688
|
-
//
|
|
1293
|
+
// Define helpers for rejecting connections
|
|
689
1294
|
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
|
690
1295
|
console.log(`[${connectionId}] ${logMessage}`);
|
|
691
1296
|
socket.end();
|
|
692
|
-
if (incomingTerminationReason === null) {
|
|
693
|
-
incomingTerminationReason = reason;
|
|
1297
|
+
if (connectionRecord.incomingTerminationReason === null) {
|
|
1298
|
+
connectionRecord.incomingTerminationReason = reason;
|
|
694
1299
|
this.incrementTerminationStat('incoming', reason);
|
|
695
1300
|
}
|
|
696
|
-
|
|
1301
|
+
this.cleanupConnection(connectionRecord, reason);
|
|
697
1302
|
};
|
|
698
1303
|
|
|
699
1304
|
// Set an initial timeout for SNI data if needed
|
|
@@ -704,12 +1309,12 @@ export class PortProxy {
|
|
|
704
1309
|
console.log(
|
|
705
1310
|
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
|
|
706
1311
|
);
|
|
707
|
-
if (incomingTerminationReason === null) {
|
|
708
|
-
incomingTerminationReason = 'initial_timeout';
|
|
1312
|
+
if (connectionRecord.incomingTerminationReason === null) {
|
|
1313
|
+
connectionRecord.incomingTerminationReason = 'initial_timeout';
|
|
709
1314
|
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
710
1315
|
}
|
|
711
1316
|
socket.end();
|
|
712
|
-
|
|
1317
|
+
this.cleanupConnection(connectionRecord, 'initial_timeout');
|
|
713
1318
|
}
|
|
714
1319
|
}, this.settings.initialDataTimeout!);
|
|
715
1320
|
|
|
@@ -722,9 +1327,7 @@ export class PortProxy {
|
|
|
722
1327
|
connectionRecord.hasReceivedInitialData = true;
|
|
723
1328
|
}
|
|
724
1329
|
|
|
725
|
-
socket.on('error', (
|
|
726
|
-
console.log(`[${connectionId}] Incoming socket error from ${remoteIP}: ${err.message}`);
|
|
727
|
-
});
|
|
1330
|
+
socket.on('error', this.handleError('incoming', connectionRecord));
|
|
728
1331
|
|
|
729
1332
|
// Track data for bytes counting
|
|
730
1333
|
socket.on('data', (chunk: Buffer) => {
|
|
@@ -745,73 +1348,8 @@ export class PortProxy {
|
|
|
745
1348
|
}
|
|
746
1349
|
});
|
|
747
1350
|
|
|
748
|
-
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
|
|
749
|
-
const code = (err as any).code;
|
|
750
|
-
let reason = 'error';
|
|
751
|
-
|
|
752
|
-
const now = Date.now();
|
|
753
|
-
const connectionDuration = now - connectionRecord.incomingStartTime;
|
|
754
|
-
const lastActivityAge = now - connectionRecord.lastActivity;
|
|
755
|
-
|
|
756
|
-
if (code === 'ECONNRESET') {
|
|
757
|
-
reason = 'econnreset';
|
|
758
|
-
console.log(
|
|
759
|
-
`[${connectionId}] ECONNRESET on ${side} side from ${remoteIP}: ${
|
|
760
|
-
err.message
|
|
761
|
-
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
762
|
-
lastActivityAge
|
|
763
|
-
)} ago`
|
|
764
|
-
);
|
|
765
|
-
} else if (code === 'ETIMEDOUT') {
|
|
766
|
-
reason = 'etimedout';
|
|
767
|
-
console.log(
|
|
768
|
-
`[${connectionId}] ETIMEDOUT on ${side} side from ${remoteIP}: ${
|
|
769
|
-
err.message
|
|
770
|
-
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
771
|
-
lastActivityAge
|
|
772
|
-
)} ago`
|
|
773
|
-
);
|
|
774
|
-
} else {
|
|
775
|
-
console.log(
|
|
776
|
-
`[${connectionId}] Error on ${side} side from ${remoteIP}: ${
|
|
777
|
-
err.message
|
|
778
|
-
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
779
|
-
lastActivityAge
|
|
780
|
-
)} ago`
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
785
|
-
incomingTerminationReason = reason;
|
|
786
|
-
this.incrementTerminationStat('incoming', reason);
|
|
787
|
-
} else if (side === 'outgoing' && outgoingTerminationReason === null) {
|
|
788
|
-
outgoingTerminationReason = reason;
|
|
789
|
-
this.incrementTerminationStat('outgoing', reason);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
initiateCleanupOnce(reason);
|
|
793
|
-
};
|
|
794
|
-
|
|
795
|
-
const handleClose = (side: 'incoming' | 'outgoing') => () => {
|
|
796
|
-
if (this.settings.enableDetailedLogging) {
|
|
797
|
-
console.log(`[${connectionId}] Connection closed on ${side} side from ${remoteIP}`);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
801
|
-
incomingTerminationReason = 'normal';
|
|
802
|
-
this.incrementTerminationStat('incoming', 'normal');
|
|
803
|
-
} else if (side === 'outgoing' && outgoingTerminationReason === null) {
|
|
804
|
-
outgoingTerminationReason = 'normal';
|
|
805
|
-
this.incrementTerminationStat('outgoing', 'normal');
|
|
806
|
-
// Record the time when outgoing socket closed.
|
|
807
|
-
connectionRecord.outgoingClosedTime = Date.now();
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
initiateCleanupOnce('closed_' + side);
|
|
811
|
-
};
|
|
812
|
-
|
|
813
1351
|
/**
|
|
814
|
-
* Sets up the connection to the target host.
|
|
1352
|
+
* Sets up the connection to the target host or NetworkProxy.
|
|
815
1353
|
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
|
|
816
1354
|
* @param initialChunk - Optional initial data chunk.
|
|
817
1355
|
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
|
|
@@ -834,7 +1372,8 @@ export class PortProxy {
|
|
|
834
1372
|
connectionRecord.hasReceivedInitialData = true;
|
|
835
1373
|
|
|
836
1374
|
// Check if this looks like a TLS handshake
|
|
837
|
-
|
|
1375
|
+
const isTlsHandshakeDetected = initialChunk && isTlsHandshake(initialChunk);
|
|
1376
|
+
if (isTlsHandshakeDetected) {
|
|
838
1377
|
connectionRecord.isTLS = true;
|
|
839
1378
|
|
|
840
1379
|
if (this.settings.enableTlsDebugLogging) {
|
|
@@ -879,6 +1418,23 @@ export class PortProxy {
|
|
|
879
1418
|
)}`
|
|
880
1419
|
);
|
|
881
1420
|
}
|
|
1421
|
+
|
|
1422
|
+
// Check if we should forward this to a NetworkProxy
|
|
1423
|
+
if (
|
|
1424
|
+
isTlsHandshakeDetected &&
|
|
1425
|
+
domainConfig.useNetworkProxy === true &&
|
|
1426
|
+
initialChunk &&
|
|
1427
|
+
this.networkProxies.length > 0
|
|
1428
|
+
) {
|
|
1429
|
+
return this.forwardToNetworkProxy(
|
|
1430
|
+
connectionId,
|
|
1431
|
+
socket,
|
|
1432
|
+
connectionRecord,
|
|
1433
|
+
domainConfig,
|
|
1434
|
+
initialChunk,
|
|
1435
|
+
serverName
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
882
1438
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
883
1439
|
if (
|
|
884
1440
|
!isGlobIPAllowed(
|
|
@@ -894,317 +1450,16 @@ export class PortProxy {
|
|
|
894
1450
|
}
|
|
895
1451
|
}
|
|
896
1452
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
socket.pause();
|
|
908
|
-
|
|
909
|
-
// Temporary handler to collect data during connection setup
|
|
910
|
-
const tempDataHandler = (chunk: Buffer) => {
|
|
911
|
-
// Track bytes received
|
|
912
|
-
connectionRecord.bytesReceived += chunk.length;
|
|
913
|
-
|
|
914
|
-
// Check for TLS handshake
|
|
915
|
-
if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
|
|
916
|
-
connectionRecord.isTLS = true;
|
|
917
|
-
|
|
918
|
-
if (this.settings.enableTlsDebugLogging) {
|
|
919
|
-
console.log(
|
|
920
|
-
`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
|
|
921
|
-
);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// Check if adding this chunk would exceed the buffer limit
|
|
926
|
-
const newSize = connectionRecord.pendingDataSize + chunk.length;
|
|
927
|
-
|
|
928
|
-
if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
|
|
929
|
-
console.log(
|
|
930
|
-
`[${connectionId}] Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
|
|
931
|
-
);
|
|
932
|
-
socket.end(); // Gracefully close the socket
|
|
933
|
-
return initiateCleanupOnce('buffer_limit_exceeded');
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// Buffer the chunk and update the size counter
|
|
937
|
-
connectionRecord.pendingData.push(Buffer.from(chunk));
|
|
938
|
-
connectionRecord.pendingDataSize = newSize;
|
|
939
|
-
this.updateActivity(connectionRecord);
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
// Add the temp handler to capture all incoming data during connection setup
|
|
943
|
-
socket.on('data', tempDataHandler);
|
|
944
|
-
|
|
945
|
-
// Add initial chunk to pending data if present
|
|
946
|
-
if (initialChunk) {
|
|
947
|
-
connectionRecord.bytesReceived += initialChunk.length;
|
|
948
|
-
connectionRecord.pendingData.push(Buffer.from(initialChunk));
|
|
949
|
-
connectionRecord.pendingDataSize = initialChunk.length;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Create the target socket but don't set up piping immediately
|
|
953
|
-
const targetSocket = plugins.net.connect(connectionOptions);
|
|
954
|
-
connectionRecord.outgoing = targetSocket;
|
|
955
|
-
connectionRecord.outgoingStartTime = Date.now();
|
|
956
|
-
|
|
957
|
-
// Apply socket optimizations
|
|
958
|
-
targetSocket.setNoDelay(this.settings.noDelay);
|
|
959
|
-
if (this.settings.keepAlive) {
|
|
960
|
-
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// Apply enhanced TCP options if available
|
|
964
|
-
if (this.settings.enableKeepAliveProbes) {
|
|
965
|
-
try {
|
|
966
|
-
if ('setKeepAliveProbes' in targetSocket) {
|
|
967
|
-
(targetSocket as any).setKeepAliveProbes(10);
|
|
968
|
-
}
|
|
969
|
-
if ('setKeepAliveInterval' in targetSocket) {
|
|
970
|
-
(targetSocket as any).setKeepAliveInterval(1000);
|
|
971
|
-
}
|
|
972
|
-
} catch (err) {
|
|
973
|
-
// Ignore errors - these are optional enhancements
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// Setup specific error handler for connection phase
|
|
978
|
-
targetSocket.once('error', (err) => {
|
|
979
|
-
// This handler runs only once during the initial connection phase
|
|
980
|
-
const code = (err as any).code;
|
|
981
|
-
console.log(
|
|
982
|
-
`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`
|
|
983
|
-
);
|
|
984
|
-
|
|
985
|
-
// Resume the incoming socket to prevent it from hanging
|
|
986
|
-
socket.resume();
|
|
987
|
-
|
|
988
|
-
if (code === 'ECONNREFUSED') {
|
|
989
|
-
console.log(
|
|
990
|
-
`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
|
|
991
|
-
);
|
|
992
|
-
} else if (code === 'ETIMEDOUT') {
|
|
993
|
-
console.log(
|
|
994
|
-
`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`
|
|
995
|
-
);
|
|
996
|
-
} else if (code === 'ECONNRESET') {
|
|
997
|
-
console.log(
|
|
998
|
-
`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`
|
|
999
|
-
);
|
|
1000
|
-
} else if (code === 'EHOSTUNREACH') {
|
|
1001
|
-
console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// Clear any existing error handler after connection phase
|
|
1005
|
-
targetSocket.removeAllListeners('error');
|
|
1006
|
-
|
|
1007
|
-
// Re-add the normal error handler for established connections
|
|
1008
|
-
targetSocket.on('error', handleError('outgoing'));
|
|
1009
|
-
|
|
1010
|
-
if (outgoingTerminationReason === null) {
|
|
1011
|
-
outgoingTerminationReason = 'connection_failed';
|
|
1012
|
-
this.incrementTerminationStat('outgoing', 'connection_failed');
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// Clean up the connection
|
|
1016
|
-
initiateCleanupOnce(`connection_failed_${code}`);
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
// Setup close handler
|
|
1020
|
-
targetSocket.on('close', handleClose('outgoing'));
|
|
1021
|
-
socket.on('close', handleClose('incoming'));
|
|
1022
|
-
|
|
1023
|
-
// Handle timeouts
|
|
1024
|
-
socket.on('timeout', () => {
|
|
1025
|
-
console.log(
|
|
1026
|
-
`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(
|
|
1027
|
-
this.settings.socketTimeout || 3600000
|
|
1028
|
-
)}`
|
|
1029
|
-
);
|
|
1030
|
-
if (incomingTerminationReason === null) {
|
|
1031
|
-
incomingTerminationReason = 'timeout';
|
|
1032
|
-
this.incrementTerminationStat('incoming', 'timeout');
|
|
1033
|
-
}
|
|
1034
|
-
initiateCleanupOnce('timeout_incoming');
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
targetSocket.on('timeout', () => {
|
|
1038
|
-
console.log(
|
|
1039
|
-
`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(
|
|
1040
|
-
this.settings.socketTimeout || 3600000
|
|
1041
|
-
)}`
|
|
1042
|
-
);
|
|
1043
|
-
if (outgoingTerminationReason === null) {
|
|
1044
|
-
outgoingTerminationReason = 'timeout';
|
|
1045
|
-
this.incrementTerminationStat('outgoing', 'timeout');
|
|
1046
|
-
}
|
|
1047
|
-
initiateCleanupOnce('timeout_outgoing');
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
// Set appropriate timeouts using the configured value with safety
|
|
1051
|
-
socket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
|
|
1052
|
-
targetSocket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
|
|
1053
|
-
|
|
1054
|
-
// Track outgoing data for bytes counting
|
|
1055
|
-
targetSocket.on('data', (chunk: Buffer) => {
|
|
1056
|
-
connectionRecord.bytesSent += chunk.length;
|
|
1057
|
-
this.updateActivity(connectionRecord);
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
// Wait for the outgoing connection to be ready before setting up piping
|
|
1061
|
-
targetSocket.once('connect', () => {
|
|
1062
|
-
// Clear the initial connection error handler
|
|
1063
|
-
targetSocket.removeAllListeners('error');
|
|
1064
|
-
|
|
1065
|
-
// Add the normal error handler for established connections
|
|
1066
|
-
targetSocket.on('error', handleError('outgoing'));
|
|
1067
|
-
|
|
1068
|
-
// Remove temporary data handler
|
|
1069
|
-
socket.removeListener('data', tempDataHandler);
|
|
1070
|
-
|
|
1071
|
-
// Flush all pending data to target
|
|
1072
|
-
if (connectionRecord.pendingData.length > 0) {
|
|
1073
|
-
const combinedData = Buffer.concat(connectionRecord.pendingData);
|
|
1074
|
-
targetSocket.write(combinedData, (err) => {
|
|
1075
|
-
if (err) {
|
|
1076
|
-
console.log(
|
|
1077
|
-
`[${connectionId}] Error writing pending data to target: ${err.message}`
|
|
1078
|
-
);
|
|
1079
|
-
return initiateCleanupOnce('write_error');
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// Now set up piping for future data and resume the socket
|
|
1083
|
-
socket.pipe(targetSocket);
|
|
1084
|
-
targetSocket.pipe(socket);
|
|
1085
|
-
socket.resume(); // Resume the socket after piping is established
|
|
1086
|
-
|
|
1087
|
-
if (this.settings.enableDetailedLogging) {
|
|
1088
|
-
console.log(
|
|
1089
|
-
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1090
|
-
`${
|
|
1091
|
-
serverName
|
|
1092
|
-
? ` (SNI: ${serverName})`
|
|
1093
|
-
: forcedDomain
|
|
1094
|
-
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1095
|
-
: ''
|
|
1096
|
-
}` +
|
|
1097
|
-
` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
|
|
1098
|
-
);
|
|
1099
|
-
} else {
|
|
1100
|
-
console.log(
|
|
1101
|
-
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1102
|
-
`${
|
|
1103
|
-
serverName
|
|
1104
|
-
? ` (SNI: ${serverName})`
|
|
1105
|
-
: forcedDomain
|
|
1106
|
-
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1107
|
-
: ''
|
|
1108
|
-
}`
|
|
1109
|
-
);
|
|
1110
|
-
}
|
|
1111
|
-
});
|
|
1112
|
-
} else {
|
|
1113
|
-
// No pending data, so just set up piping
|
|
1114
|
-
socket.pipe(targetSocket);
|
|
1115
|
-
targetSocket.pipe(socket);
|
|
1116
|
-
socket.resume(); // Resume the socket after piping is established
|
|
1117
|
-
|
|
1118
|
-
if (this.settings.enableDetailedLogging) {
|
|
1119
|
-
console.log(
|
|
1120
|
-
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1121
|
-
`${
|
|
1122
|
-
serverName
|
|
1123
|
-
? ` (SNI: ${serverName})`
|
|
1124
|
-
: forcedDomain
|
|
1125
|
-
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1126
|
-
: ''
|
|
1127
|
-
}` +
|
|
1128
|
-
` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
|
|
1129
|
-
);
|
|
1130
|
-
} else {
|
|
1131
|
-
console.log(
|
|
1132
|
-
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1133
|
-
`${
|
|
1134
|
-
serverName
|
|
1135
|
-
? ` (SNI: ${serverName})`
|
|
1136
|
-
: forcedDomain
|
|
1137
|
-
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1138
|
-
: ''
|
|
1139
|
-
}`
|
|
1140
|
-
);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
// Clear the buffer now that we've processed it
|
|
1145
|
-
connectionRecord.pendingData = [];
|
|
1146
|
-
connectionRecord.pendingDataSize = 0;
|
|
1147
|
-
|
|
1148
|
-
// Add the renegotiation listener for SNI validation
|
|
1149
|
-
if (serverName) {
|
|
1150
|
-
socket.on('data', (renegChunk: Buffer) => {
|
|
1151
|
-
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
1152
|
-
try {
|
|
1153
|
-
// Try to extract SNI from potential renegotiation
|
|
1154
|
-
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
|
1155
|
-
if (newSNI && newSNI !== connectionRecord.lockedDomain) {
|
|
1156
|
-
console.log(
|
|
1157
|
-
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`
|
|
1158
|
-
);
|
|
1159
|
-
initiateCleanupOnce('sni_mismatch');
|
|
1160
|
-
} else if (newSNI && this.settings.enableDetailedLogging) {
|
|
1161
|
-
console.log(
|
|
1162
|
-
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
|
|
1163
|
-
);
|
|
1164
|
-
}
|
|
1165
|
-
} catch (err) {
|
|
1166
|
-
console.log(
|
|
1167
|
-
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
|
|
1168
|
-
);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// Set connection timeout
|
|
1175
|
-
if (connectionRecord.cleanupTimer) {
|
|
1176
|
-
clearTimeout(connectionRecord.cleanupTimer);
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
// Set timeout based on domain config or default with safety check
|
|
1180
|
-
const connectionTimeout = this.getConnectionTimeout(connectionRecord);
|
|
1181
|
-
const safeTimeout = ensureSafeTimeout(connectionTimeout); // Ensure timeout is safe
|
|
1182
|
-
|
|
1183
|
-
connectionRecord.cleanupTimer = setTimeout(() => {
|
|
1184
|
-
console.log(
|
|
1185
|
-
`[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(
|
|
1186
|
-
connectionTimeout
|
|
1187
|
-
)}), forcing cleanup.`
|
|
1188
|
-
);
|
|
1189
|
-
initiateCleanupOnce('connection_timeout');
|
|
1190
|
-
}, safeTimeout);
|
|
1191
|
-
|
|
1192
|
-
// Make sure timeout doesn't keep the process alive
|
|
1193
|
-
if (connectionRecord.cleanupTimer.unref) {
|
|
1194
|
-
connectionRecord.cleanupTimer.unref();
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// Mark TLS handshake as complete for TLS connections
|
|
1198
|
-
if (connectionRecord.isTLS) {
|
|
1199
|
-
connectionRecord.tlsHandshakeComplete = true;
|
|
1200
|
-
|
|
1201
|
-
if (this.settings.enableTlsDebugLogging) {
|
|
1202
|
-
console.log(
|
|
1203
|
-
`[${connectionId}] TLS handshake complete for connection from ${remoteIP}`
|
|
1204
|
-
);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
});
|
|
1453
|
+
// If we didn't forward to NetworkProxy, proceed with direct connection
|
|
1454
|
+
return this.setupDirectConnection(
|
|
1455
|
+
connectionId,
|
|
1456
|
+
socket,
|
|
1457
|
+
connectionRecord,
|
|
1458
|
+
domainConfig,
|
|
1459
|
+
serverName,
|
|
1460
|
+
initialChunk,
|
|
1461
|
+
overridePort
|
|
1462
|
+
);
|
|
1208
1463
|
};
|
|
1209
1464
|
|
|
1210
1465
|
// --- PORT RANGE-BASED HANDLING ---
|
|
@@ -1367,7 +1622,7 @@ export class PortProxy {
|
|
|
1367
1622
|
console.log(
|
|
1368
1623
|
`PortProxy -> OK: Now listening on port ${port}${
|
|
1369
1624
|
this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
|
|
1370
|
-
}`
|
|
1625
|
+
}${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}`
|
|
1371
1626
|
);
|
|
1372
1627
|
});
|
|
1373
1628
|
this.netServers.push(server);
|
|
@@ -1385,6 +1640,8 @@ export class PortProxy {
|
|
|
1385
1640
|
let nonTlsConnections = 0;
|
|
1386
1641
|
let completedTlsHandshakes = 0;
|
|
1387
1642
|
let pendingTlsHandshakes = 0;
|
|
1643
|
+
let keepAliveConnections = 0;
|
|
1644
|
+
let networkProxyConnections = 0;
|
|
1388
1645
|
|
|
1389
1646
|
// Create a copy of the keys to avoid modification during iteration
|
|
1390
1647
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
@@ -1404,6 +1661,14 @@ export class PortProxy {
|
|
|
1404
1661
|
} else {
|
|
1405
1662
|
nonTlsConnections++;
|
|
1406
1663
|
}
|
|
1664
|
+
|
|
1665
|
+
if (record.hasKeepAlive) {
|
|
1666
|
+
keepAliveConnections++;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
if (record.usingNetworkProxy) {
|
|
1670
|
+
networkProxyConnections++;
|
|
1671
|
+
}
|
|
1407
1672
|
|
|
1408
1673
|
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
|
1409
1674
|
if (record.outgoingStartTime) {
|
|
@@ -1440,19 +1705,58 @@ export class PortProxy {
|
|
|
1440
1705
|
);
|
|
1441
1706
|
}
|
|
1442
1707
|
|
|
1443
|
-
// Skip inactivity check if disabled
|
|
1444
|
-
if (!this.settings.disableInactivityCheck
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1708
|
+
// Skip inactivity check if disabled or for immortal keep-alive connections
|
|
1709
|
+
if (!this.settings.disableInactivityCheck &&
|
|
1710
|
+
!(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) {
|
|
1711
|
+
|
|
1448
1712
|
const inactivityTime = now - record.lastActivity;
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1713
|
+
|
|
1714
|
+
// Use extended timeout for extended-treatment keep-alive connections
|
|
1715
|
+
let effectiveTimeout = this.settings.inactivityTimeout!;
|
|
1716
|
+
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
|
1717
|
+
const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
|
|
1718
|
+
effectiveTimeout = effectiveTimeout * multiplier;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
|
|
1722
|
+
// For keep-alive connections, issue a warning first
|
|
1723
|
+
if (record.hasKeepAlive && !record.inactivityWarningIssued) {
|
|
1724
|
+
console.log(
|
|
1725
|
+
`[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
1726
|
+
`Will close in 10 minutes if no activity.`
|
|
1727
|
+
);
|
|
1728
|
+
|
|
1729
|
+
// Set warning flag and add grace period
|
|
1730
|
+
record.inactivityWarningIssued = true;
|
|
1731
|
+
record.lastActivity = now - (effectiveTimeout - 600000);
|
|
1732
|
+
|
|
1733
|
+
// Try to stimulate activity with a probe packet
|
|
1734
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
1735
|
+
try {
|
|
1736
|
+
record.outgoing.write(Buffer.alloc(0));
|
|
1737
|
+
|
|
1738
|
+
if (this.settings.enableDetailedLogging) {
|
|
1739
|
+
console.log(`[${id}] Sent probe packet to test keep-alive connection`);
|
|
1740
|
+
}
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
console.log(`[${id}] Error sending probe packet: ${err}`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
} else {
|
|
1746
|
+
// For non-keep-alive or after warning, close the connection
|
|
1747
|
+
console.log(
|
|
1748
|
+
`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
|
|
1749
|
+
`for ${plugins.prettyMs(inactivityTime)}.` +
|
|
1750
|
+
(record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
|
|
1751
|
+
);
|
|
1752
|
+
this.cleanupConnection(record, 'inactivity');
|
|
1753
|
+
}
|
|
1754
|
+
} else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
|
|
1755
|
+
// If activity detected after warning, clear the warning
|
|
1756
|
+
if (this.settings.enableDetailedLogging) {
|
|
1757
|
+
console.log(`[${id}] Connection activity detected after inactivity warning, resetting warning`);
|
|
1758
|
+
}
|
|
1759
|
+
record.inactivityWarningIssued = false;
|
|
1456
1760
|
}
|
|
1457
1761
|
}
|
|
1458
1762
|
}
|
|
@@ -1460,7 +1764,8 @@ export class PortProxy {
|
|
|
1460
1764
|
// Log detailed stats periodically
|
|
1461
1765
|
console.log(
|
|
1462
1766
|
`Active connections: ${this.connectionRecords.size}. ` +
|
|
1463
|
-
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}),
|
|
1767
|
+
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
|
|
1768
|
+
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` +
|
|
1464
1769
|
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
|
|
1465
1770
|
maxOutgoing
|
|
1466
1771
|
)}. ` +
|
|
@@ -1477,6 +1782,21 @@ export class PortProxy {
|
|
|
1477
1782
|
}
|
|
1478
1783
|
}
|
|
1479
1784
|
|
|
1785
|
+
/**
|
|
1786
|
+
* Add or replace NetworkProxy instances
|
|
1787
|
+
*/
|
|
1788
|
+
public setNetworkProxies(networkProxies: NetworkProxy[]): void {
|
|
1789
|
+
this.networkProxies = networkProxies;
|
|
1790
|
+
console.log(`Updated NetworkProxy instances: ${this.networkProxies.length} proxies configured`);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Get a list of configured NetworkProxy instances
|
|
1795
|
+
*/
|
|
1796
|
+
public getNetworkProxies(): NetworkProxy[] {
|
|
1797
|
+
return this.networkProxies;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1480
1800
|
/**
|
|
1481
1801
|
* Gracefully shut down the proxy
|
|
1482
1802
|
*/
|