@push.rocks/smartproxy 3.32.2 → 3.33.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/ts/classes.portproxy.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface IDomainConfig {
|
|
|
10
10
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
11
11
|
// Allow domain-specific timeout override
|
|
12
12
|
connectionTimeout?: number; // Connection timeout override (ms)
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
// New properties for NetworkProxy integration
|
|
15
15
|
useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
|
|
16
16
|
networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0)
|
|
@@ -54,14 +54,19 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
54
54
|
// Rate limiting and security
|
|
55
55
|
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
|
|
56
56
|
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
// Enhanced keep-alive settings
|
|
59
59
|
keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
|
|
60
60
|
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
|
|
61
61
|
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
// New property for NetworkProxy integration
|
|
64
64
|
networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
|
|
65
|
+
|
|
66
|
+
// Browser optimization settings
|
|
67
|
+
browserFriendlyMode?: boolean; // Optimizes handling for browser connections
|
|
68
|
+
allowRenegotiationWithDifferentSNI?: boolean; // Allows SNI changes during renegotiation
|
|
69
|
+
relatedDomainPatterns?: string[][]; // Patterns for domains that should be allowed to share connections
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/**
|
|
@@ -90,16 +95,23 @@ interface IConnectionRecord {
|
|
|
90
95
|
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
|
|
91
96
|
hasReceivedInitialData: boolean; // Whether initial data has been received
|
|
92
97
|
domainConfig?: IDomainConfig; // Associated domain config for this connection
|
|
93
|
-
|
|
98
|
+
|
|
94
99
|
// Keep-alive tracking
|
|
95
100
|
hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
|
|
96
101
|
inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
|
|
97
102
|
incomingTerminationReason?: string | null; // Reason for incoming termination
|
|
98
103
|
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
|
99
|
-
|
|
104
|
+
|
|
100
105
|
// New field for NetworkProxy tracking
|
|
101
106
|
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
|
102
107
|
networkProxyIndex?: number; // Which NetworkProxy instance is being used
|
|
108
|
+
|
|
109
|
+
// New field for renegotiation handler
|
|
110
|
+
renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
|
|
111
|
+
|
|
112
|
+
// Browser connection tracking
|
|
113
|
+
isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
|
|
114
|
+
domainSwitches?: number; // Number of times the domain has been switched on this connection
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
/**
|
|
@@ -266,6 +278,58 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
266
278
|
}
|
|
267
279
|
}
|
|
268
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Checks if a TLS record is a proper ClientHello message (more accurate than just checking record type)
|
|
283
|
+
* @param buffer - Buffer containing the TLS record
|
|
284
|
+
* @returns true if the buffer contains a proper ClientHello message
|
|
285
|
+
*/
|
|
286
|
+
function isClientHello(buffer: Buffer): boolean {
|
|
287
|
+
try {
|
|
288
|
+
if (buffer.length < 9) return false; // Too small for a proper ClientHello
|
|
289
|
+
|
|
290
|
+
// Check record type (has to be handshake - 22)
|
|
291
|
+
if (buffer.readUInt8(0) !== 22) return false;
|
|
292
|
+
|
|
293
|
+
// After the TLS record header (5 bytes), check the handshake type (1 for ClientHello)
|
|
294
|
+
if (buffer.readUInt8(5) !== 1) return false;
|
|
295
|
+
|
|
296
|
+
// Basic checks passed, this appears to be a ClientHello
|
|
297
|
+
return true;
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.log(`Error checking for ClientHello: ${err}`);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Checks if two domains are related based on configured patterns
|
|
306
|
+
* @param domain1 - First domain name
|
|
307
|
+
* @param domain2 - Second domain name
|
|
308
|
+
* @param relatedPatterns - Array of domain pattern groups where domains in the same group are considered related
|
|
309
|
+
* @returns true if domains are related, false otherwise
|
|
310
|
+
*/
|
|
311
|
+
function areDomainsRelated(
|
|
312
|
+
domain1: string,
|
|
313
|
+
domain2: string,
|
|
314
|
+
relatedPatterns?: string[][]
|
|
315
|
+
): boolean {
|
|
316
|
+
// Only exact same domains or empty domains are automatically related
|
|
317
|
+
if (!domain1 || !domain2 || domain1 === domain2) return true;
|
|
318
|
+
|
|
319
|
+
// Check against configured related domain patterns - the ONLY source of truth
|
|
320
|
+
if (relatedPatterns && relatedPatterns.length > 0) {
|
|
321
|
+
for (const patternGroup of relatedPatterns) {
|
|
322
|
+
const domain1Matches = patternGroup.some((pattern) => plugins.minimatch(domain1, pattern));
|
|
323
|
+
const domain2Matches = patternGroup.some((pattern) => plugins.minimatch(domain2, pattern));
|
|
324
|
+
|
|
325
|
+
if (domain1Matches && domain2Matches) return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// If no patterns match, domains are not related
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
269
333
|
// Helper: Check if a port falls within any of the given port ranges
|
|
270
334
|
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
|
271
335
|
return ranges.some((range) => port >= range.from && port <= range.to);
|
|
@@ -348,7 +412,7 @@ export class PortProxy {
|
|
|
348
412
|
// Connection tracking by IP for rate limiting
|
|
349
413
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
350
414
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
351
|
-
|
|
415
|
+
|
|
352
416
|
// New property to store NetworkProxy instances
|
|
353
417
|
private networkProxies: NetworkProxy[] = [];
|
|
354
418
|
|
|
@@ -375,8 +439,8 @@ export class PortProxy {
|
|
|
375
439
|
|
|
376
440
|
// Feature flags
|
|
377
441
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
378
|
-
enableKeepAliveProbes:
|
|
379
|
-
? settingsArg.enableKeepAliveProbes : true, // Enable by default
|
|
442
|
+
enableKeepAliveProbes:
|
|
443
|
+
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, // Enable by default
|
|
380
444
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
381
445
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
382
446
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
|
|
@@ -384,13 +448,18 @@ export class PortProxy {
|
|
|
384
448
|
// Rate limiting defaults
|
|
385
449
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
|
|
386
450
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
|
|
387
|
-
|
|
451
|
+
|
|
388
452
|
// Enhanced keep-alive settings
|
|
389
453
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
|
|
390
454
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
|
|
391
455
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
456
|
+
|
|
457
|
+
// Browser optimization settings (new)
|
|
458
|
+
browserFriendlyMode: settingsArg.browserFriendlyMode || true, // On by default
|
|
459
|
+
allowRenegotiationWithDifferentSNI: settingsArg.allowRenegotiationWithDifferentSNI || false, // Off by default
|
|
460
|
+
relatedDomainPatterns: settingsArg.relatedDomainPatterns || [], // Empty by default
|
|
392
461
|
};
|
|
393
|
-
|
|
462
|
+
|
|
394
463
|
// Store NetworkProxy instances if provided
|
|
395
464
|
this.networkProxies = settingsArg.networkProxies || [];
|
|
396
465
|
}
|
|
@@ -413,58 +482,66 @@ export class PortProxy {
|
|
|
413
482
|
serverName?: string
|
|
414
483
|
): void {
|
|
415
484
|
// Determine which NetworkProxy to use
|
|
416
|
-
const proxyIndex =
|
|
417
|
-
? domainConfig.networkProxyIndex
|
|
418
|
-
|
|
419
|
-
|
|
485
|
+
const proxyIndex =
|
|
486
|
+
domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0;
|
|
487
|
+
|
|
420
488
|
// Validate the NetworkProxy index
|
|
421
489
|
if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
|
|
422
|
-
console.log(
|
|
490
|
+
console.log(
|
|
491
|
+
`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`
|
|
492
|
+
);
|
|
423
493
|
// Fall back to direct connection
|
|
424
|
-
return this.setupDirectConnection(
|
|
494
|
+
return this.setupDirectConnection(
|
|
495
|
+
connectionId,
|
|
496
|
+
socket,
|
|
497
|
+
record,
|
|
498
|
+
domainConfig,
|
|
499
|
+
serverName,
|
|
500
|
+
initialData
|
|
501
|
+
);
|
|
425
502
|
}
|
|
426
|
-
|
|
503
|
+
|
|
427
504
|
const networkProxy = this.networkProxies[proxyIndex];
|
|
428
505
|
const proxyPort = networkProxy.getListeningPort();
|
|
429
506
|
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
|
430
|
-
|
|
507
|
+
|
|
431
508
|
if (this.settings.enableDetailedLogging) {
|
|
432
509
|
console.log(
|
|
433
510
|
`[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
|
|
434
511
|
);
|
|
435
512
|
}
|
|
436
|
-
|
|
513
|
+
|
|
437
514
|
// Create a connection to the NetworkProxy
|
|
438
515
|
const proxySocket = plugins.net.connect({
|
|
439
516
|
host: proxyHost,
|
|
440
|
-
port: proxyPort
|
|
517
|
+
port: proxyPort,
|
|
441
518
|
});
|
|
442
|
-
|
|
519
|
+
|
|
443
520
|
// Store the outgoing socket in the record
|
|
444
521
|
record.outgoing = proxySocket;
|
|
445
522
|
record.outgoingStartTime = Date.now();
|
|
446
523
|
record.usingNetworkProxy = true;
|
|
447
524
|
record.networkProxyIndex = proxyIndex;
|
|
448
|
-
|
|
525
|
+
|
|
449
526
|
// Set up error handlers
|
|
450
527
|
proxySocket.on('error', (err) => {
|
|
451
528
|
console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
|
|
452
529
|
this.cleanupConnection(record, 'network_proxy_connect_error');
|
|
453
530
|
});
|
|
454
|
-
|
|
531
|
+
|
|
455
532
|
// Handle connection to NetworkProxy
|
|
456
533
|
proxySocket.on('connect', () => {
|
|
457
534
|
if (this.settings.enableDetailedLogging) {
|
|
458
535
|
console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
|
|
459
536
|
}
|
|
460
|
-
|
|
537
|
+
|
|
461
538
|
// First send the initial data that contains the TLS ClientHello
|
|
462
539
|
proxySocket.write(initialData);
|
|
463
|
-
|
|
540
|
+
|
|
464
541
|
// Now set up bidirectional piping between client and NetworkProxy
|
|
465
542
|
socket.pipe(proxySocket);
|
|
466
543
|
proxySocket.pipe(socket);
|
|
467
|
-
|
|
544
|
+
|
|
468
545
|
// Setup cleanup handlers
|
|
469
546
|
proxySocket.on('close', () => {
|
|
470
547
|
if (this.settings.enableDetailedLogging) {
|
|
@@ -472,18 +549,20 @@ export class PortProxy {
|
|
|
472
549
|
}
|
|
473
550
|
this.cleanupConnection(record, 'network_proxy_closed');
|
|
474
551
|
});
|
|
475
|
-
|
|
552
|
+
|
|
476
553
|
socket.on('close', () => {
|
|
477
554
|
if (this.settings.enableDetailedLogging) {
|
|
478
|
-
console.log(
|
|
555
|
+
console.log(
|
|
556
|
+
`[${connectionId}] Client connection closed after forwarding to NetworkProxy`
|
|
557
|
+
);
|
|
479
558
|
}
|
|
480
559
|
this.cleanupConnection(record, 'client_closed');
|
|
481
560
|
});
|
|
482
|
-
|
|
561
|
+
|
|
483
562
|
// Update activity on data transfer
|
|
484
563
|
socket.on('data', () => this.updateActivity(record));
|
|
485
564
|
proxySocket.on('data', () => this.updateActivity(record));
|
|
486
|
-
|
|
565
|
+
|
|
487
566
|
if (this.settings.enableDetailedLogging) {
|
|
488
567
|
console.log(
|
|
489
568
|
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
|
|
@@ -491,7 +570,7 @@ export class PortProxy {
|
|
|
491
570
|
}
|
|
492
571
|
});
|
|
493
572
|
}
|
|
494
|
-
|
|
573
|
+
|
|
495
574
|
/**
|
|
496
575
|
* Sets up a direct connection to the target (original behavior)
|
|
497
576
|
* This is used when NetworkProxy isn't configured or as a fallback
|
|
@@ -568,11 +647,11 @@ export class PortProxy {
|
|
|
568
647
|
|
|
569
648
|
// Apply socket optimizations
|
|
570
649
|
targetSocket.setNoDelay(this.settings.noDelay);
|
|
571
|
-
|
|
650
|
+
|
|
572
651
|
// Apply keep-alive settings to the outgoing connection as well
|
|
573
652
|
if (this.settings.keepAlive) {
|
|
574
653
|
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
575
|
-
|
|
654
|
+
|
|
576
655
|
// Apply enhanced TCP keep-alive options if enabled
|
|
577
656
|
if (this.settings.enableKeepAliveProbes) {
|
|
578
657
|
try {
|
|
@@ -585,7 +664,9 @@ export class PortProxy {
|
|
|
585
664
|
} catch (err) {
|
|
586
665
|
// Ignore errors - these are optional enhancements
|
|
587
666
|
if (this.settings.enableDetailedLogging) {
|
|
588
|
-
console.log(
|
|
667
|
+
console.log(
|
|
668
|
+
`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
|
|
669
|
+
);
|
|
589
670
|
}
|
|
590
671
|
}
|
|
591
672
|
}
|
|
@@ -642,19 +723,21 @@ export class PortProxy {
|
|
|
642
723
|
// For keep-alive connections, just log a warning instead of closing
|
|
643
724
|
if (record.hasKeepAlive) {
|
|
644
725
|
console.log(
|
|
645
|
-
`[${connectionId}] Timeout event on incoming keep-alive connection from ${
|
|
726
|
+
`[${connectionId}] Timeout event on incoming keep-alive connection from ${
|
|
727
|
+
record.remoteIP
|
|
728
|
+
} after ${plugins.prettyMs(
|
|
646
729
|
this.settings.socketTimeout || 3600000
|
|
647
730
|
)}. Connection preserved.`
|
|
648
731
|
);
|
|
649
732
|
// Don't close the connection - just log
|
|
650
733
|
return;
|
|
651
734
|
}
|
|
652
|
-
|
|
735
|
+
|
|
653
736
|
// For non-keep-alive connections, proceed with normal cleanup
|
|
654
737
|
console.log(
|
|
655
|
-
`[${connectionId}] Timeout on incoming side from ${
|
|
656
|
-
|
|
657
|
-
)}`
|
|
738
|
+
`[${connectionId}] Timeout on incoming side from ${
|
|
739
|
+
record.remoteIP
|
|
740
|
+
} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
|
|
658
741
|
);
|
|
659
742
|
if (record.incomingTerminationReason === null) {
|
|
660
743
|
record.incomingTerminationReason = 'timeout';
|
|
@@ -667,19 +750,21 @@ export class PortProxy {
|
|
|
667
750
|
// For keep-alive connections, just log a warning instead of closing
|
|
668
751
|
if (record.hasKeepAlive) {
|
|
669
752
|
console.log(
|
|
670
|
-
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${
|
|
753
|
+
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${
|
|
754
|
+
record.remoteIP
|
|
755
|
+
} after ${plugins.prettyMs(
|
|
671
756
|
this.settings.socketTimeout || 3600000
|
|
672
757
|
)}. Connection preserved.`
|
|
673
758
|
);
|
|
674
759
|
// Don't close the connection - just log
|
|
675
760
|
return;
|
|
676
761
|
}
|
|
677
|
-
|
|
762
|
+
|
|
678
763
|
// For non-keep-alive connections, proceed with normal cleanup
|
|
679
764
|
console.log(
|
|
680
|
-
`[${connectionId}] Timeout on outgoing side from ${
|
|
681
|
-
|
|
682
|
-
)}`
|
|
765
|
+
`[${connectionId}] Timeout on outgoing side from ${
|
|
766
|
+
record.remoteIP
|
|
767
|
+
} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
|
|
683
768
|
);
|
|
684
769
|
if (record.outgoingTerminationReason === null) {
|
|
685
770
|
record.outgoingTerminationReason = 'timeout';
|
|
@@ -693,9 +778,11 @@ export class PortProxy {
|
|
|
693
778
|
// Disable timeouts completely for immortal connections
|
|
694
779
|
socket.setTimeout(0);
|
|
695
780
|
targetSocket.setTimeout(0);
|
|
696
|
-
|
|
781
|
+
|
|
697
782
|
if (this.settings.enableDetailedLogging) {
|
|
698
|
-
console.log(
|
|
783
|
+
console.log(
|
|
784
|
+
`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`
|
|
785
|
+
);
|
|
699
786
|
}
|
|
700
787
|
} else {
|
|
701
788
|
// Set normal timeouts for other connections
|
|
@@ -725,9 +812,7 @@ export class PortProxy {
|
|
|
725
812
|
const combinedData = Buffer.concat(record.pendingData);
|
|
726
813
|
targetSocket.write(combinedData, (err) => {
|
|
727
814
|
if (err) {
|
|
728
|
-
console.log(
|
|
729
|
-
`[${connectionId}] Error writing pending data to target: ${err.message}`
|
|
730
|
-
);
|
|
815
|
+
console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
|
|
731
816
|
return this.initiateCleanupOnce(record, 'write_error');
|
|
732
817
|
}
|
|
733
818
|
|
|
@@ -746,7 +831,9 @@ export class PortProxy {
|
|
|
746
831
|
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
747
832
|
: ''
|
|
748
833
|
}` +
|
|
749
|
-
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
834
|
+
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
835
|
+
record.hasKeepAlive ? 'Yes' : 'No'
|
|
836
|
+
}`
|
|
750
837
|
);
|
|
751
838
|
} else {
|
|
752
839
|
console.log(
|
|
@@ -777,7 +864,9 @@ export class PortProxy {
|
|
|
777
864
|
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
778
865
|
: ''
|
|
779
866
|
}` +
|
|
780
|
-
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
867
|
+
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
868
|
+
record.hasKeepAlive ? 'Yes' : 'No'
|
|
869
|
+
}`
|
|
781
870
|
);
|
|
782
871
|
} else {
|
|
783
872
|
console.log(
|
|
@@ -797,82 +886,134 @@ export class PortProxy {
|
|
|
797
886
|
record.pendingData = [];
|
|
798
887
|
record.pendingDataSize = 0;
|
|
799
888
|
|
|
800
|
-
// Add the renegotiation
|
|
889
|
+
// Add the renegotiation handler for SNI validation, with browser-friendly improvements
|
|
801
890
|
if (serverName) {
|
|
802
|
-
|
|
803
|
-
|
|
891
|
+
// Define a handler for checking renegotiation with improved detection
|
|
892
|
+
const renegotiationHandler = (renegChunk: Buffer) => {
|
|
893
|
+
// Only process if this looks like a TLS ClientHello (more precise than just checking for type 22)
|
|
894
|
+
if (isClientHello(renegChunk)) {
|
|
804
895
|
try {
|
|
805
|
-
//
|
|
896
|
+
// Extract SNI from ClientHello
|
|
806
897
|
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
898
|
+
|
|
899
|
+
// Skip if no SNI was found
|
|
900
|
+
if (!newSNI) return;
|
|
901
|
+
|
|
902
|
+
// Handle SNI change during renegotiation
|
|
903
|
+
if (newSNI !== record.lockedDomain) {
|
|
904
|
+
// Track domain switches for browser connections
|
|
905
|
+
if (!record.domainSwitches) record.domainSwitches = 0;
|
|
906
|
+
record.domainSwitches++;
|
|
907
|
+
|
|
908
|
+
// Check if this is a normal behavior of browser connection reuse
|
|
909
|
+
const isRelatedDomain = areDomainsRelated(
|
|
910
|
+
newSNI,
|
|
911
|
+
record.lockedDomain || '',
|
|
912
|
+
this.settings.relatedDomainPatterns
|
|
810
913
|
);
|
|
811
|
-
|
|
812
|
-
|
|
914
|
+
|
|
915
|
+
// Decide how to handle the SNI change based on settings
|
|
916
|
+
if (this.settings.browserFriendlyMode && isRelatedDomain) {
|
|
917
|
+
console.log(
|
|
918
|
+
`[${connectionId}] Browser domain switch detected: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
919
|
+
`Domains are related, allowing connection to continue (domain switch #${record.domainSwitches}).`
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
// Update the locked domain to the new one
|
|
923
|
+
record.lockedDomain = newSNI;
|
|
924
|
+
} else if (this.settings.allowRenegotiationWithDifferentSNI) {
|
|
925
|
+
console.log(
|
|
926
|
+
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
927
|
+
`Allowing due to allowRenegotiationWithDifferentSNI setting.`
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
// Update the locked domain to the new one
|
|
931
|
+
record.lockedDomain = newSNI;
|
|
932
|
+
} else {
|
|
933
|
+
// Standard strict behavior - terminate connection on SNI mismatch
|
|
934
|
+
console.log(
|
|
935
|
+
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
936
|
+
`Terminating connection. Enable browserFriendlyMode to allow this.`
|
|
937
|
+
);
|
|
938
|
+
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
939
|
+
}
|
|
940
|
+
} else if (this.settings.enableDetailedLogging) {
|
|
813
941
|
console.log(
|
|
814
|
-
`[${connectionId}]
|
|
942
|
+
`[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
|
|
815
943
|
);
|
|
816
944
|
}
|
|
817
945
|
} catch (err) {
|
|
818
946
|
console.log(
|
|
819
|
-
`[${connectionId}] Error processing
|
|
947
|
+
`[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.`
|
|
820
948
|
);
|
|
821
949
|
}
|
|
822
950
|
}
|
|
823
|
-
}
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
// Store the handler in the connection record so we can remove it during cleanup
|
|
954
|
+
record.renegotiationHandler = renegotiationHandler;
|
|
955
|
+
|
|
956
|
+
// Add the listener
|
|
957
|
+
socket.on('data', renegotiationHandler);
|
|
824
958
|
}
|
|
825
959
|
|
|
826
960
|
// Set connection timeout with simpler logic
|
|
827
961
|
if (record.cleanupTimer) {
|
|
828
962
|
clearTimeout(record.cleanupTimer);
|
|
829
963
|
}
|
|
830
|
-
|
|
964
|
+
|
|
831
965
|
// For immortal keep-alive connections, skip setting a timeout completely
|
|
832
966
|
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
|
833
967
|
if (this.settings.enableDetailedLogging) {
|
|
834
|
-
console.log(
|
|
968
|
+
console.log(
|
|
969
|
+
`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`
|
|
970
|
+
);
|
|
835
971
|
}
|
|
836
972
|
// No cleanup timer for immortal connections
|
|
837
|
-
}
|
|
973
|
+
}
|
|
838
974
|
// For extended keep-alive connections, use extended timeout
|
|
839
975
|
else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
|
840
976
|
const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
841
977
|
const safeTimeout = ensureSafeTimeout(extendedTimeout);
|
|
842
|
-
|
|
978
|
+
|
|
843
979
|
record.cleanupTimer = setTimeout(() => {
|
|
844
980
|
console.log(
|
|
845
|
-
`[${connectionId}] Keep-alive connection from ${
|
|
846
|
-
|
|
847
|
-
)}), forcing cleanup.`
|
|
981
|
+
`[${connectionId}] Keep-alive connection from ${
|
|
982
|
+
record.remoteIP
|
|
983
|
+
} exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.`
|
|
848
984
|
);
|
|
849
985
|
this.initiateCleanupOnce(record, 'extended_lifetime');
|
|
850
986
|
}, safeTimeout);
|
|
851
|
-
|
|
987
|
+
|
|
852
988
|
// Make sure timeout doesn't keep the process alive
|
|
853
989
|
if (record.cleanupTimer.unref) {
|
|
854
990
|
record.cleanupTimer.unref();
|
|
855
991
|
}
|
|
856
|
-
|
|
992
|
+
|
|
857
993
|
if (this.settings.enableDetailedLogging) {
|
|
858
|
-
console.log(
|
|
994
|
+
console.log(
|
|
995
|
+
`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(
|
|
996
|
+
extendedTimeout
|
|
997
|
+
)}`
|
|
998
|
+
);
|
|
859
999
|
}
|
|
860
1000
|
}
|
|
861
1001
|
// For standard connections, use normal timeout
|
|
862
1002
|
else {
|
|
863
1003
|
// Use domain-specific timeout if available, otherwise use default
|
|
864
|
-
const connectionTimeout =
|
|
1004
|
+
const connectionTimeout =
|
|
1005
|
+
record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
|
|
865
1006
|
const safeTimeout = ensureSafeTimeout(connectionTimeout);
|
|
866
|
-
|
|
1007
|
+
|
|
867
1008
|
record.cleanupTimer = setTimeout(() => {
|
|
868
1009
|
console.log(
|
|
869
|
-
`[${connectionId}] Connection from ${
|
|
870
|
-
|
|
871
|
-
)}), forcing cleanup.`
|
|
1010
|
+
`[${connectionId}] Connection from ${
|
|
1011
|
+
record.remoteIP
|
|
1012
|
+
} exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`
|
|
872
1013
|
);
|
|
873
1014
|
this.initiateCleanupOnce(record, 'connection_timeout');
|
|
874
1015
|
}, safeTimeout);
|
|
875
|
-
|
|
1016
|
+
|
|
876
1017
|
// Make sure timeout doesn't keep the process alive
|
|
877
1018
|
if (record.cleanupTimer.unref) {
|
|
878
1019
|
record.cleanupTimer.unref();
|
|
@@ -973,6 +1114,16 @@ export class PortProxy {
|
|
|
973
1114
|
const bytesReceived = record.bytesReceived;
|
|
974
1115
|
const bytesSent = record.bytesSent;
|
|
975
1116
|
|
|
1117
|
+
// Remove the renegotiation handler if present
|
|
1118
|
+
if (record.renegotiationHandler && record.incoming) {
|
|
1119
|
+
try {
|
|
1120
|
+
record.incoming.removeListener('data', record.renegotiationHandler);
|
|
1121
|
+
record.renegotiationHandler = undefined;
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
console.log(`[${record.id}] Error removing renegotiation handler: ${err}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
976
1127
|
try {
|
|
977
1128
|
if (!record.incoming.destroyed) {
|
|
978
1129
|
// Try graceful shutdown first, then force destroy after a short timeout
|
|
@@ -1047,8 +1198,11 @@ export class PortProxy {
|
|
|
1047
1198
|
` Duration: ${plugins.prettyMs(
|
|
1048
1199
|
duration
|
|
1049
1200
|
)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
|
|
1050
|
-
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
1051
|
-
|
|
1201
|
+
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
1202
|
+
record.hasKeepAlive ? 'Yes' : 'No'
|
|
1203
|
+
}` +
|
|
1204
|
+
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}` +
|
|
1205
|
+
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
|
|
1052
1206
|
);
|
|
1053
1207
|
} else {
|
|
1054
1208
|
console.log(
|
|
@@ -1063,7 +1217,7 @@ export class PortProxy {
|
|
|
1063
1217
|
*/
|
|
1064
1218
|
private updateActivity(record: IConnectionRecord): void {
|
|
1065
1219
|
record.lastActivity = Date.now();
|
|
1066
|
-
|
|
1220
|
+
|
|
1067
1221
|
// Clear any inactivity warning
|
|
1068
1222
|
if (record.inactivityWarningIssued) {
|
|
1069
1223
|
record.inactivityWarningIssued = false;
|
|
@@ -1082,7 +1236,7 @@ export class PortProxy {
|
|
|
1082
1236
|
}
|
|
1083
1237
|
return this.settings.targetIP!;
|
|
1084
1238
|
}
|
|
1085
|
-
|
|
1239
|
+
|
|
1086
1240
|
/**
|
|
1087
1241
|
* Initiates cleanup once for a connection
|
|
1088
1242
|
*/
|
|
@@ -1090,12 +1244,15 @@ export class PortProxy {
|
|
|
1090
1244
|
if (this.settings.enableDetailedLogging) {
|
|
1091
1245
|
console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
|
|
1092
1246
|
}
|
|
1093
|
-
|
|
1094
|
-
if (
|
|
1247
|
+
|
|
1248
|
+
if (
|
|
1249
|
+
record.incomingTerminationReason === null ||
|
|
1250
|
+
record.incomingTerminationReason === undefined
|
|
1251
|
+
) {
|
|
1095
1252
|
record.incomingTerminationReason = reason;
|
|
1096
1253
|
this.incrementTerminationStat('incoming', reason);
|
|
1097
1254
|
}
|
|
1098
|
-
|
|
1255
|
+
|
|
1099
1256
|
this.cleanupConnection(record, reason);
|
|
1100
1257
|
}
|
|
1101
1258
|
|
|
@@ -1219,7 +1376,7 @@ export class PortProxy {
|
|
|
1219
1376
|
|
|
1220
1377
|
// Apply socket optimizations
|
|
1221
1378
|
socket.setNoDelay(this.settings.noDelay);
|
|
1222
|
-
|
|
1379
|
+
|
|
1223
1380
|
// Create a unique connection ID and record
|
|
1224
1381
|
const connectionId = generateConnectionId();
|
|
1225
1382
|
const connectionRecord: IConnectionRecord = {
|
|
@@ -1243,16 +1400,20 @@ export class PortProxy {
|
|
|
1243
1400
|
hasKeepAlive: false, // Will set to true if keep-alive is applied
|
|
1244
1401
|
incomingTerminationReason: null,
|
|
1245
1402
|
outgoingTerminationReason: null,
|
|
1246
|
-
|
|
1403
|
+
|
|
1247
1404
|
// Initialize NetworkProxy tracking fields
|
|
1248
|
-
usingNetworkProxy: false
|
|
1405
|
+
usingNetworkProxy: false,
|
|
1406
|
+
|
|
1407
|
+
// Initialize browser connection tracking
|
|
1408
|
+
isBrowserConnection: this.settings.browserFriendlyMode, // Assume browser if browserFriendlyMode is enabled
|
|
1409
|
+
domainSwitches: 0, // Track domain switches
|
|
1249
1410
|
};
|
|
1250
|
-
|
|
1411
|
+
|
|
1251
1412
|
// Apply keep-alive settings if enabled
|
|
1252
1413
|
if (this.settings.keepAlive) {
|
|
1253
1414
|
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
1254
1415
|
connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
|
|
1255
|
-
|
|
1416
|
+
|
|
1256
1417
|
// Apply enhanced TCP keep-alive options if enabled
|
|
1257
1418
|
if (this.settings.enableKeepAliveProbes) {
|
|
1258
1419
|
try {
|
|
@@ -1266,7 +1427,9 @@ export class PortProxy {
|
|
|
1266
1427
|
} catch (err) {
|
|
1267
1428
|
// Ignore errors - these are optional enhancements
|
|
1268
1429
|
if (this.settings.enableDetailedLogging) {
|
|
1269
|
-
console.log(
|
|
1430
|
+
console.log(
|
|
1431
|
+
`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`
|
|
1432
|
+
);
|
|
1270
1433
|
}
|
|
1271
1434
|
}
|
|
1272
1435
|
}
|
|
@@ -1279,8 +1442,9 @@ export class PortProxy {
|
|
|
1279
1442
|
if (this.settings.enableDetailedLogging) {
|
|
1280
1443
|
console.log(
|
|
1281
1444
|
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
|
|
1282
|
-
|
|
1283
|
-
|
|
1445
|
+
`Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
|
1446
|
+
`Mode: ${this.settings.browserFriendlyMode ? 'Browser-friendly' : 'Standard'}. ` +
|
|
1447
|
+
`Active connections: ${this.connectionRecords.size}`
|
|
1284
1448
|
);
|
|
1285
1449
|
} else {
|
|
1286
1450
|
console.log(
|
|
@@ -1418,12 +1582,12 @@ export class PortProxy {
|
|
|
1418
1582
|
)}`
|
|
1419
1583
|
);
|
|
1420
1584
|
}
|
|
1421
|
-
|
|
1585
|
+
|
|
1422
1586
|
// Check if we should forward this to a NetworkProxy
|
|
1423
1587
|
if (
|
|
1424
|
-
isTlsHandshakeDetected &&
|
|
1425
|
-
domainConfig.useNetworkProxy === true &&
|
|
1426
|
-
initialChunk &&
|
|
1588
|
+
isTlsHandshakeDetected &&
|
|
1589
|
+
domainConfig.useNetworkProxy === true &&
|
|
1590
|
+
initialChunk &&
|
|
1427
1591
|
this.networkProxies.length > 0
|
|
1428
1592
|
) {
|
|
1429
1593
|
return this.forwardToNetworkProxy(
|
|
@@ -1450,6 +1614,11 @@ export class PortProxy {
|
|
|
1450
1614
|
}
|
|
1451
1615
|
}
|
|
1452
1616
|
|
|
1617
|
+
// Save the initial SNI for browser connection management
|
|
1618
|
+
if (serverName) {
|
|
1619
|
+
connectionRecord.lockedDomain = serverName;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1453
1622
|
// If we didn't forward to NetworkProxy, proceed with direct connection
|
|
1454
1623
|
return this.setupDirectConnection(
|
|
1455
1624
|
connectionId,
|
|
@@ -1622,7 +1791,9 @@ export class PortProxy {
|
|
|
1622
1791
|
console.log(
|
|
1623
1792
|
`PortProxy -> OK: Now listening on port ${port}${
|
|
1624
1793
|
this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
|
|
1625
|
-
}${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}
|
|
1794
|
+
}${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}${
|
|
1795
|
+
this.settings.browserFriendlyMode ? ' (Browser-friendly mode enabled)' : ''
|
|
1796
|
+
}`
|
|
1626
1797
|
);
|
|
1627
1798
|
});
|
|
1628
1799
|
this.netServers.push(server);
|
|
@@ -1642,6 +1813,7 @@ export class PortProxy {
|
|
|
1642
1813
|
let pendingTlsHandshakes = 0;
|
|
1643
1814
|
let keepAliveConnections = 0;
|
|
1644
1815
|
let networkProxyConnections = 0;
|
|
1816
|
+
let domainSwitchedConnections = 0;
|
|
1645
1817
|
|
|
1646
1818
|
// Create a copy of the keys to avoid modification during iteration
|
|
1647
1819
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
@@ -1661,20 +1833,23 @@ export class PortProxy {
|
|
|
1661
1833
|
} else {
|
|
1662
1834
|
nonTlsConnections++;
|
|
1663
1835
|
}
|
|
1664
|
-
|
|
1836
|
+
|
|
1665
1837
|
if (record.hasKeepAlive) {
|
|
1666
1838
|
keepAliveConnections++;
|
|
1667
1839
|
}
|
|
1668
|
-
|
|
1840
|
+
|
|
1669
1841
|
if (record.usingNetworkProxy) {
|
|
1670
1842
|
networkProxyConnections++;
|
|
1671
1843
|
}
|
|
1672
1844
|
|
|
1845
|
+
if (record.domainSwitches && record.domainSwitches > 0) {
|
|
1846
|
+
domainSwitchedConnections++;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1673
1849
|
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
|
1674
1850
|
if (record.outgoingStartTime) {
|
|
1675
1851
|
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
|
1676
1852
|
}
|
|
1677
|
-
|
|
1678
1853
|
// Parity check: if outgoing socket closed and incoming remains active
|
|
1679
1854
|
if (
|
|
1680
1855
|
record.outgoingClosedTime &&
|
|
@@ -1706,35 +1881,38 @@ export class PortProxy {
|
|
|
1706
1881
|
}
|
|
1707
1882
|
|
|
1708
1883
|
// Skip inactivity check if disabled or for immortal keep-alive connections
|
|
1709
|
-
if (
|
|
1710
|
-
|
|
1711
|
-
|
|
1884
|
+
if (
|
|
1885
|
+
!this.settings.disableInactivityCheck &&
|
|
1886
|
+
!(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
|
|
1887
|
+
) {
|
|
1712
1888
|
const inactivityTime = now - record.lastActivity;
|
|
1713
|
-
|
|
1889
|
+
|
|
1714
1890
|
// Use extended timeout for extended-treatment keep-alive connections
|
|
1715
1891
|
let effectiveTimeout = this.settings.inactivityTimeout!;
|
|
1716
1892
|
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
|
1717
1893
|
const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
|
|
1718
1894
|
effectiveTimeout = effectiveTimeout * multiplier;
|
|
1719
1895
|
}
|
|
1720
|
-
|
|
1896
|
+
|
|
1721
1897
|
if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
|
|
1722
1898
|
// For keep-alive connections, issue a warning first
|
|
1723
1899
|
if (record.hasKeepAlive && !record.inactivityWarningIssued) {
|
|
1724
1900
|
console.log(
|
|
1725
|
-
`[${id}] Warning: Keep-alive connection from ${
|
|
1726
|
-
|
|
1901
|
+
`[${id}] Warning: Keep-alive connection from ${
|
|
1902
|
+
record.remoteIP
|
|
1903
|
+
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
1904
|
+
`Will close in 10 minutes if no activity.`
|
|
1727
1905
|
);
|
|
1728
|
-
|
|
1906
|
+
|
|
1729
1907
|
// Set warning flag and add grace period
|
|
1730
1908
|
record.inactivityWarningIssued = true;
|
|
1731
1909
|
record.lastActivity = now - (effectiveTimeout - 600000);
|
|
1732
|
-
|
|
1910
|
+
|
|
1733
1911
|
// Try to stimulate activity with a probe packet
|
|
1734
1912
|
if (record.outgoing && !record.outgoing.destroyed) {
|
|
1735
1913
|
try {
|
|
1736
1914
|
record.outgoing.write(Buffer.alloc(0));
|
|
1737
|
-
|
|
1915
|
+
|
|
1738
1916
|
if (this.settings.enableDetailedLogging) {
|
|
1739
1917
|
console.log(`[${id}] Sent probe packet to test keep-alive connection`);
|
|
1740
1918
|
}
|
|
@@ -1746,15 +1924,17 @@ export class PortProxy {
|
|
|
1746
1924
|
// For non-keep-alive or after warning, close the connection
|
|
1747
1925
|
console.log(
|
|
1748
1926
|
`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
|
|
1749
|
-
|
|
1750
|
-
|
|
1927
|
+
`for ${plugins.prettyMs(inactivityTime)}.` +
|
|
1928
|
+
(record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
|
|
1751
1929
|
);
|
|
1752
1930
|
this.cleanupConnection(record, 'inactivity');
|
|
1753
1931
|
}
|
|
1754
1932
|
} else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
|
|
1755
1933
|
// If activity detected after warning, clear the warning
|
|
1756
1934
|
if (this.settings.enableDetailedLogging) {
|
|
1757
|
-
console.log(
|
|
1935
|
+
console.log(
|
|
1936
|
+
`[${id}] Connection activity detected after inactivity warning, resetting warning`
|
|
1937
|
+
);
|
|
1758
1938
|
}
|
|
1759
1939
|
record.inactivityWarningIssued = false;
|
|
1760
1940
|
}
|
|
@@ -1765,7 +1945,8 @@ export class PortProxy {
|
|
|
1765
1945
|
console.log(
|
|
1766
1946
|
`Active connections: ${this.connectionRecords.size}. ` +
|
|
1767
1947
|
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
|
|
1768
|
-
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}
|
|
1948
|
+
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}, ` +
|
|
1949
|
+
`DomainSwitched=${domainSwitchedConnections}. ` +
|
|
1769
1950
|
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
|
|
1770
1951
|
maxOutgoing
|
|
1771
1952
|
)}. ` +
|
|
@@ -1903,4 +2084,4 @@ export class PortProxy {
|
|
|
1903
2084
|
|
|
1904
2085
|
console.log('PortProxy shutdown complete.');
|
|
1905
2086
|
}
|
|
1906
|
-
}
|
|
2087
|
+
}
|