@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.
@@ -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
- // First try the standard SNI extraction
487
- const standardSni = this.extractSNI(buffer, enableLogging);
488
- if (standardSni) {
692
+ const log = (message: string) => {
489
693
  if (enableLogging) {
490
- console.log(`[SNI Extraction] Found standard SNI: ${standardSni}`);
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(buffer)) {
498
- if (enableLogging) {
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(buffer, enableLogging);
738
+ const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
504
739
  if (pskSni) {
505
- if (enableLogging) {
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
- // Could add more browser-specific heuristics here if needed
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('[SNI Extraction] Failed to extract SNI from resumption mechanisms');
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;