@push.rocks/smartproxy 3.37.3 → 3.38.2
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.portproxy.js +27 -4
- package/dist_ts/classes.snihandler.d.ts +74 -2
- package/dist_ts/classes.snihandler.js +261 -17
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +33 -3
- package/ts/classes.snihandler.ts +333 -16
package/ts/classes.snihandler.ts
CHANGED
|
@@ -2,17 +2,25 @@ import { Buffer } from 'buffer';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* SNI (Server Name Indication) handler for TLS connections.
|
|
5
|
-
* Provides robust extraction of SNI values from TLS ClientHello messages
|
|
5
|
+
* Provides robust extraction of SNI values from TLS ClientHello messages
|
|
6
|
+
* with support for fragmented packets, TLS 1.3 resumption, and Chrome-specific
|
|
7
|
+
* connection behaviors.
|
|
6
8
|
*/
|
|
7
9
|
export class SniHandler {
|
|
8
10
|
// TLS record types and constants
|
|
9
11
|
private static readonly TLS_HANDSHAKE_RECORD_TYPE = 22;
|
|
12
|
+
private static readonly TLS_APPLICATION_DATA_TYPE = 23; // TLS Application Data record type
|
|
10
13
|
private static readonly TLS_CLIENT_HELLO_HANDSHAKE_TYPE = 1;
|
|
11
14
|
private static readonly TLS_SNI_EXTENSION_TYPE = 0x0000;
|
|
12
15
|
private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023;
|
|
13
16
|
private static readonly TLS_SNI_HOST_NAME_TYPE = 0;
|
|
14
17
|
private static readonly TLS_PSK_EXTENSION_TYPE = 0x0029; // Pre-Shared Key extension type for TLS 1.3
|
|
15
18
|
private static readonly TLS_PSK_KE_MODES_EXTENSION_TYPE = 0x002D; // PSK Key Exchange Modes
|
|
19
|
+
private static readonly TLS_EARLY_DATA_EXTENSION_TYPE = 0x002A; // Early Data (0-RTT) extension
|
|
20
|
+
|
|
21
|
+
// Buffer for handling fragmented ClientHello messages
|
|
22
|
+
private static fragmentedBuffers: Map<string, Buffer> = new Map();
|
|
23
|
+
private static fragmentTimeout: number = 1000; // ms to wait for fragments before cleanup
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
26
|
* Checks if a buffer contains a TLS handshake message (record type 22)
|
|
@@ -22,6 +30,107 @@ export class SniHandler {
|
|
|
22
30
|
public static isTlsHandshake(buffer: Buffer): boolean {
|
|
23
31
|
return buffer.length > 0 && buffer[0] === this.TLS_HANDSHAKE_RECORD_TYPE;
|
|
24
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Checks if a buffer contains TLS application data (record type 23)
|
|
36
|
+
* @param buffer - The buffer to check
|
|
37
|
+
* @returns true if the buffer starts with a TLS application data record type
|
|
38
|
+
*/
|
|
39
|
+
public static isTlsApplicationData(buffer: Buffer): boolean {
|
|
40
|
+
return buffer.length > 0 && buffer[0] === this.TLS_APPLICATION_DATA_TYPE;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a connection ID based on source/destination information
|
|
45
|
+
* Used to track fragmented ClientHello messages across multiple packets
|
|
46
|
+
*
|
|
47
|
+
* @param connectionInfo - Object containing connection identifiers (IP/port)
|
|
48
|
+
* @returns A string ID for the connection
|
|
49
|
+
*/
|
|
50
|
+
public static createConnectionId(connectionInfo: {
|
|
51
|
+
sourceIp?: string;
|
|
52
|
+
sourcePort?: number;
|
|
53
|
+
destIp?: string;
|
|
54
|
+
destPort?: number;
|
|
55
|
+
}): string {
|
|
56
|
+
const { sourceIp, sourcePort, destIp, destPort } = connectionInfo;
|
|
57
|
+
return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Handles potential fragmented ClientHello messages by buffering and reassembling
|
|
62
|
+
* TLS record fragments that might span multiple TCP packets.
|
|
63
|
+
*
|
|
64
|
+
* @param buffer - The current buffer fragment
|
|
65
|
+
* @param connectionId - Unique identifier for the connection
|
|
66
|
+
* @param enableLogging - Whether to enable logging
|
|
67
|
+
* @returns A complete buffer if reassembly is successful, or undefined if more fragments are needed
|
|
68
|
+
*/
|
|
69
|
+
public static handleFragmentedClientHello(
|
|
70
|
+
buffer: Buffer,
|
|
71
|
+
connectionId: string,
|
|
72
|
+
enableLogging: boolean = false
|
|
73
|
+
): Buffer | undefined {
|
|
74
|
+
const log = (message: string) => {
|
|
75
|
+
if (enableLogging) {
|
|
76
|
+
console.log(`[SNI Fragment] ${message}`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Check if we've seen this connection before
|
|
81
|
+
if (!this.fragmentedBuffers.has(connectionId)) {
|
|
82
|
+
// New connection, start with this buffer
|
|
83
|
+
this.fragmentedBuffers.set(connectionId, buffer);
|
|
84
|
+
|
|
85
|
+
// Set timeout to clean up if we don't get a complete ClientHello
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
if (this.fragmentedBuffers.has(connectionId)) {
|
|
88
|
+
this.fragmentedBuffers.delete(connectionId);
|
|
89
|
+
log(`Connection ${connectionId} timed out waiting for complete ClientHello`);
|
|
90
|
+
}
|
|
91
|
+
}, this.fragmentTimeout);
|
|
92
|
+
|
|
93
|
+
// Evaluate if this buffer already contains a complete ClientHello
|
|
94
|
+
try {
|
|
95
|
+
if (buffer.length >= 5) {
|
|
96
|
+
const recordLength = (buffer[3] << 8) + buffer[4];
|
|
97
|
+
if (buffer.length >= recordLength + 5) {
|
|
98
|
+
log(`Initial buffer contains complete ClientHello, length: ${buffer.length}`);
|
|
99
|
+
return buffer;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
log(`Error checking initial buffer completeness: ${e}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
log(`Started buffering connection ${connectionId}, initial size: ${buffer.length}`);
|
|
107
|
+
return undefined; // Need more fragments
|
|
108
|
+
} else {
|
|
109
|
+
// Existing connection, append this buffer
|
|
110
|
+
const existingBuffer = this.fragmentedBuffers.get(connectionId)!;
|
|
111
|
+
const newBuffer = Buffer.concat([existingBuffer, buffer]);
|
|
112
|
+
this.fragmentedBuffers.set(connectionId, newBuffer);
|
|
113
|
+
|
|
114
|
+
log(`Appended to buffer for ${connectionId}, new size: ${newBuffer.length}`);
|
|
115
|
+
|
|
116
|
+
// Check if we now have a complete ClientHello
|
|
117
|
+
try {
|
|
118
|
+
if (newBuffer.length >= 5) {
|
|
119
|
+
const recordLength = (newBuffer[3] << 8) + newBuffer[4];
|
|
120
|
+
if (newBuffer.length >= recordLength + 5) {
|
|
121
|
+
log(`Assembled complete ClientHello, length: ${newBuffer.length}`);
|
|
122
|
+
// Complete message received, remove from tracking
|
|
123
|
+
this.fragmentedBuffers.delete(connectionId);
|
|
124
|
+
return newBuffer;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
log(`Error checking reassembled buffer completeness: ${e}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return undefined; // Still need more fragments
|
|
132
|
+
}
|
|
133
|
+
}
|
|
25
134
|
|
|
26
135
|
/**
|
|
27
136
|
* Checks if a buffer contains a TLS ClientHello message
|
|
@@ -466,6 +575,93 @@ export class SniHandler {
|
|
|
466
575
|
}
|
|
467
576
|
}
|
|
468
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Checks if the buffer contains TLS 1.3 early data (0-RTT)
|
|
580
|
+
* @param buffer - The buffer to check
|
|
581
|
+
* @param enableLogging - Whether to enable logging
|
|
582
|
+
* @returns true if early data is detected
|
|
583
|
+
*/
|
|
584
|
+
public static hasEarlyData(
|
|
585
|
+
buffer: Buffer,
|
|
586
|
+
enableLogging: boolean = false
|
|
587
|
+
): boolean {
|
|
588
|
+
const log = (message: string) => {
|
|
589
|
+
if (enableLogging) {
|
|
590
|
+
console.log(`[Early Data] ${message}`);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
// Check if this is a valid ClientHello first
|
|
596
|
+
if (!this.isClientHello(buffer)) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Find the extensions section
|
|
601
|
+
let pos = 5; // Start after record header
|
|
602
|
+
|
|
603
|
+
// Skip handshake type (1 byte)
|
|
604
|
+
pos += 1;
|
|
605
|
+
|
|
606
|
+
// Skip handshake length (3 bytes)
|
|
607
|
+
pos += 3;
|
|
608
|
+
|
|
609
|
+
// Skip client version (2 bytes)
|
|
610
|
+
pos += 2;
|
|
611
|
+
|
|
612
|
+
// Skip client random (32 bytes)
|
|
613
|
+
pos += 32;
|
|
614
|
+
|
|
615
|
+
// Skip session ID
|
|
616
|
+
if (pos + 1 > buffer.length) return false;
|
|
617
|
+
const sessionIdLength = buffer[pos];
|
|
618
|
+
pos += 1 + sessionIdLength;
|
|
619
|
+
|
|
620
|
+
// Skip cipher suites
|
|
621
|
+
if (pos + 2 > buffer.length) return false;
|
|
622
|
+
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
623
|
+
pos += 2 + cipherSuitesLength;
|
|
624
|
+
|
|
625
|
+
// Skip compression methods
|
|
626
|
+
if (pos + 1 > buffer.length) return false;
|
|
627
|
+
const compressionMethodsLength = buffer[pos];
|
|
628
|
+
pos += 1 + compressionMethodsLength;
|
|
629
|
+
|
|
630
|
+
// Check if we have extensions
|
|
631
|
+
if (pos + 2 > buffer.length) return false;
|
|
632
|
+
|
|
633
|
+
// Get extensions length
|
|
634
|
+
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
635
|
+
pos += 2;
|
|
636
|
+
|
|
637
|
+
// Extensions end position
|
|
638
|
+
const extensionsEnd = pos + extensionsLength;
|
|
639
|
+
if (extensionsEnd > buffer.length) return false;
|
|
640
|
+
|
|
641
|
+
// Look for early data extension
|
|
642
|
+
while (pos + 4 <= extensionsEnd) {
|
|
643
|
+
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
644
|
+
pos += 2;
|
|
645
|
+
|
|
646
|
+
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
647
|
+
pos += 2;
|
|
648
|
+
|
|
649
|
+
if (extensionType === this.TLS_EARLY_DATA_EXTENSION_TYPE) {
|
|
650
|
+
log('Early Data (0-RTT) extension detected');
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Skip to next extension
|
|
655
|
+
pos += extensionLength;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return false;
|
|
659
|
+
} catch (error) {
|
|
660
|
+
log(`Error checking for early data: ${error}`);
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
469
665
|
/**
|
|
470
666
|
* Attempts to extract SNI from an initial ClientHello packet and handles
|
|
471
667
|
* session resumption edge cases more robustly than the standard extraction.
|
|
@@ -474,44 +670,165 @@ export class SniHandler {
|
|
|
474
670
|
* 1. Standard SNI extraction
|
|
475
671
|
* 2. TLS 1.3 PSK-based resumption (Chrome, Firefox, etc.)
|
|
476
672
|
* 3. Session ticket-based resumption
|
|
673
|
+
* 4. Fragmented ClientHello messages
|
|
674
|
+
* 5. TLS 1.3 Early Data (0-RTT)
|
|
675
|
+
* 6. Chrome's connection racing behaviors
|
|
477
676
|
*
|
|
478
677
|
* @param buffer - The buffer containing the TLS ClientHello message
|
|
678
|
+
* @param connectionInfo - Optional connection information for fragment handling
|
|
479
679
|
* @param enableLogging - Whether to enable detailed debug logging
|
|
480
680
|
* @returns The extracted server name or undefined if not found
|
|
481
681
|
*/
|
|
482
682
|
public static extractSNIWithResumptionSupport(
|
|
483
|
-
buffer: Buffer,
|
|
683
|
+
buffer: Buffer,
|
|
684
|
+
connectionInfo?: {
|
|
685
|
+
sourceIp?: string;
|
|
686
|
+
sourcePort?: number;
|
|
687
|
+
destIp?: string;
|
|
688
|
+
destPort?: number;
|
|
689
|
+
},
|
|
484
690
|
enableLogging: boolean = false
|
|
485
691
|
): string | undefined {
|
|
486
|
-
|
|
487
|
-
const standardSni = this.extractSNI(buffer, enableLogging);
|
|
488
|
-
if (standardSni) {
|
|
692
|
+
const log = (message: string) => {
|
|
489
693
|
if (enableLogging) {
|
|
490
|
-
console.log(`[SNI Extraction]
|
|
694
|
+
console.log(`[SNI Extraction] ${message}`);
|
|
491
695
|
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// Check if we need to handle fragmented packets
|
|
699
|
+
let processBuffer = buffer;
|
|
700
|
+
if (connectionInfo) {
|
|
701
|
+
const connectionId = this.createConnectionId(connectionInfo);
|
|
702
|
+
const reassembledBuffer = this.handleFragmentedClientHello(
|
|
703
|
+
buffer,
|
|
704
|
+
connectionId,
|
|
705
|
+
enableLogging
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
if (!reassembledBuffer) {
|
|
709
|
+
log(`Waiting for more fragments on connection ${connectionId}`);
|
|
710
|
+
return undefined; // Need more fragments to complete ClientHello
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
processBuffer = reassembledBuffer;
|
|
714
|
+
log(`Using reassembled buffer of length ${processBuffer.length}`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// First try the standard SNI extraction
|
|
718
|
+
const standardSni = this.extractSNI(processBuffer, enableLogging);
|
|
719
|
+
if (standardSni) {
|
|
720
|
+
log(`Found standard SNI: ${standardSni}`);
|
|
492
721
|
return standardSni;
|
|
493
722
|
}
|
|
494
723
|
|
|
724
|
+
// Check for TLS 1.3 early data (0-RTT)
|
|
725
|
+
const hasEarly = this.hasEarlyData(processBuffer, enableLogging);
|
|
726
|
+
if (hasEarly) {
|
|
727
|
+
log('TLS 1.3 Early Data detected, using special handling');
|
|
728
|
+
// In 0-RTT, Chrome often relies on server remembering the SNI from previous sessions
|
|
729
|
+
// We could implement session tracking here if necessary
|
|
730
|
+
}
|
|
731
|
+
|
|
495
732
|
// If standard extraction failed and we have a valid ClientHello,
|
|
496
733
|
// this might be a session resumption with non-standard format
|
|
497
|
-
if (this.isClientHello(
|
|
498
|
-
|
|
499
|
-
console.log('[SNI Extraction] Detected ClientHello without standard SNI, possible session resumption');
|
|
500
|
-
}
|
|
734
|
+
if (this.isClientHello(processBuffer)) {
|
|
735
|
+
log('Detected ClientHello without standard SNI, possible session resumption');
|
|
501
736
|
|
|
502
737
|
// Try to extract from PSK extension (TLS 1.3 resumption)
|
|
503
|
-
const pskSni = this.extractSNIFromPSKExtension(
|
|
738
|
+
const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
|
|
504
739
|
if (pskSni) {
|
|
505
|
-
|
|
506
|
-
console.log(`[SNI Extraction] Extracted SNI from PSK extension: ${pskSni}`);
|
|
507
|
-
}
|
|
740
|
+
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
|
508
741
|
return pskSni;
|
|
509
742
|
}
|
|
510
743
|
|
|
511
|
-
//
|
|
744
|
+
// Special handling for Chrome connection racing
|
|
745
|
+
// Chrome often opens multiple connections in parallel with different
|
|
746
|
+
// characteristics to improve performance
|
|
747
|
+
// Here we would look for specific patterns in ClientHello that indicate
|
|
748
|
+
// it's part of a connection race
|
|
749
|
+
|
|
750
|
+
// Detect if this is likely a secondary connection in a race
|
|
751
|
+
// by examining the cipher suites and extensions
|
|
752
|
+
// This would require session state tracking across connections
|
|
753
|
+
|
|
754
|
+
log('Failed to extract SNI from resumption mechanisms');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Main entry point for SNI extraction that handles all edge cases.
|
|
762
|
+
* This should be called for each TLS packet received from a client.
|
|
763
|
+
*
|
|
764
|
+
* The method uses connection tracking to handle fragmented ClientHello
|
|
765
|
+
* messages and various TLS 1.3 behaviors, including Chrome's connection
|
|
766
|
+
* racing patterns.
|
|
767
|
+
*
|
|
768
|
+
* @param buffer - The buffer containing TLS data
|
|
769
|
+
* @param connectionInfo - Connection metadata (IPs and ports)
|
|
770
|
+
* @param enableLogging - Whether to enable detailed debug logging
|
|
771
|
+
* @param cachedSni - Optional cached SNI from previous connections (for racing detection)
|
|
772
|
+
* @returns The extracted server name or undefined if not found or more data needed
|
|
773
|
+
*/
|
|
774
|
+
public static processTlsPacket(
|
|
775
|
+
buffer: Buffer,
|
|
776
|
+
connectionInfo: {
|
|
777
|
+
sourceIp: string;
|
|
778
|
+
sourcePort: number;
|
|
779
|
+
destIp: string;
|
|
780
|
+
destPort: number;
|
|
781
|
+
timestamp?: number;
|
|
782
|
+
},
|
|
783
|
+
enableLogging: boolean = false,
|
|
784
|
+
cachedSni?: string
|
|
785
|
+
): string | undefined {
|
|
786
|
+
const log = (message: string) => {
|
|
512
787
|
if (enableLogging) {
|
|
513
|
-
console.log(
|
|
788
|
+
console.log(`[TLS Packet] ${message}`);
|
|
514
789
|
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// Add timestamp if not provided
|
|
793
|
+
if (!connectionInfo.timestamp) {
|
|
794
|
+
connectionInfo.timestamp = Date.now();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Check if this is a TLS handshake
|
|
798
|
+
if (!this.isTlsHandshake(buffer) && !this.isTlsApplicationData(buffer)) {
|
|
799
|
+
log('Not a TLS handshake or application data packet');
|
|
800
|
+
return undefined;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Create connection ID for tracking
|
|
804
|
+
const connectionId = this.createConnectionId(connectionInfo);
|
|
805
|
+
log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`);
|
|
806
|
+
|
|
807
|
+
// Handle special case: if we already have a cached SNI from a previous
|
|
808
|
+
// connection from the same client IP within a short time window,
|
|
809
|
+
// this might be a connection racing situation
|
|
810
|
+
if (cachedSni && this.isTlsApplicationData(buffer)) {
|
|
811
|
+
log(`Using cached SNI from connection racing: ${cachedSni}`);
|
|
812
|
+
return cachedSni;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Try to extract SNI with full resumption support and fragment handling
|
|
816
|
+
const sni = this.extractSNIWithResumptionSupport(
|
|
817
|
+
buffer,
|
|
818
|
+
connectionInfo,
|
|
819
|
+
enableLogging
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
if (sni) {
|
|
823
|
+
log(`Successfully extracted SNI: ${sni}`);
|
|
824
|
+
return sni;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// If we couldn't extract an SNI, check if this is a valid ClientHello
|
|
828
|
+
// If it is, but we couldn't get an SNI, it might be a fragment or
|
|
829
|
+
// a connection race situation
|
|
830
|
+
if (this.isClientHello(buffer)) {
|
|
831
|
+
log('Valid ClientHello detected, but no SNI extracted - might need more data');
|
|
515
832
|
}
|
|
516
833
|
|
|
517
834
|
return undefined;
|