@push.rocks/smartproxy 3.38.2 → 3.40.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.
@@ -3,8 +3,8 @@ import { Buffer } from 'buffer';
3
3
  /**
4
4
  * SNI (Server Name Indication) handler for TLS connections.
5
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
+ * with support for fragmented packets, TLS 1.3 resumption, Chrome-specific
7
+ * connection behaviors, and tab hibernation/reactivation scenarios.
8
8
  */
9
9
  export class SniHandler {
10
10
  // TLS record types and constants
@@ -22,6 +22,132 @@ export class SniHandler {
22
22
  private static fragmentedBuffers: Map<string, Buffer> = new Map();
23
23
  private static fragmentTimeout: number = 1000; // ms to wait for fragments before cleanup
24
24
 
25
+ // Session tracking for tab reactivation scenarios
26
+ private static sessionCache: Map<string, {
27
+ sni: string;
28
+ timestamp: number;
29
+ clientRandom?: Buffer;
30
+ }> = new Map();
31
+
32
+ // Longer timeout for session cache (24 hours by default)
33
+ private static sessionCacheTimeout: number = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
34
+
35
+ // Cleanup interval for session cache (run every hour)
36
+ private static sessionCleanupInterval: NodeJS.Timeout | null = null;
37
+
38
+ /**
39
+ * Initialize the session cache cleanup mechanism.
40
+ * This should be called during application startup.
41
+ */
42
+ public static initSessionCacheCleanup(): void {
43
+ if (this.sessionCleanupInterval === null) {
44
+ this.sessionCleanupInterval = setInterval(() => {
45
+ this.cleanupSessionCache();
46
+ }, 60 * 60 * 1000); // Run every hour
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Clean up expired entries from the session cache
52
+ */
53
+ private static cleanupSessionCache(): void {
54
+ const now = Date.now();
55
+ const expiredKeys: string[] = [];
56
+
57
+ this.sessionCache.forEach((session, key) => {
58
+ if (now - session.timestamp > this.sessionCacheTimeout) {
59
+ expiredKeys.push(key);
60
+ }
61
+ });
62
+
63
+ expiredKeys.forEach(key => {
64
+ this.sessionCache.delete(key);
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Create a client identity key for session tracking
70
+ * Uses source IP and optional client random for uniqueness
71
+ *
72
+ * @param sourceIp - Client IP address
73
+ * @param clientRandom - Optional TLS client random value
74
+ * @returns A string key for the session cache
75
+ */
76
+ private static createClientKey(sourceIp: string, clientRandom?: Buffer): string {
77
+ if (clientRandom) {
78
+ // If we have the client random, use it for more precise tracking
79
+ return `${sourceIp}:${clientRandom.toString('hex')}`;
80
+ }
81
+ // Fall back to just IP-based tracking
82
+ return sourceIp;
83
+ }
84
+
85
+ /**
86
+ * Store SNI information in the session cache
87
+ *
88
+ * @param sourceIp - Client IP address
89
+ * @param sni - The extracted SNI value
90
+ * @param clientRandom - Optional TLS client random value
91
+ */
92
+ private static cacheSession(sourceIp: string, sni: string, clientRandom?: Buffer): void {
93
+ const key = this.createClientKey(sourceIp, clientRandom);
94
+ this.sessionCache.set(key, {
95
+ sni,
96
+ timestamp: Date.now(),
97
+ clientRandom
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Retrieve SNI information from the session cache
103
+ *
104
+ * @param sourceIp - Client IP address
105
+ * @param clientRandom - Optional TLS client random value
106
+ * @returns The cached SNI or undefined if not found
107
+ */
108
+ private static getCachedSession(sourceIp: string, clientRandom?: Buffer): string | undefined {
109
+ // Try with client random first for precision
110
+ if (clientRandom) {
111
+ const preciseKey = this.createClientKey(sourceIp, clientRandom);
112
+ const preciseSession = this.sessionCache.get(preciseKey);
113
+ if (preciseSession) {
114
+ return preciseSession.sni;
115
+ }
116
+ }
117
+
118
+ // Fall back to IP-only lookup
119
+ const ipKey = this.createClientKey(sourceIp);
120
+ const session = this.sessionCache.get(ipKey);
121
+ if (session) {
122
+ // Update the timestamp to keep the session alive
123
+ session.timestamp = Date.now();
124
+ return session.sni;
125
+ }
126
+
127
+ return undefined;
128
+ }
129
+
130
+ /**
131
+ * Extract the client random value from a ClientHello message
132
+ *
133
+ * @param buffer - The buffer containing the ClientHello
134
+ * @returns The 32-byte client random or undefined if extraction fails
135
+ */
136
+ private static extractClientRandom(buffer: Buffer): Buffer | undefined {
137
+ try {
138
+ if (!this.isClientHello(buffer) || buffer.length < 46) {
139
+ return undefined;
140
+ }
141
+
142
+ // In a ClientHello message, the client random starts at position 11
143
+ // after record header (5 bytes), handshake type (1 byte),
144
+ // handshake length (3 bytes), and client version (2 bytes)
145
+ return buffer.slice(11, 11 + 32);
146
+ } catch (error) {
147
+ return undefined;
148
+ }
149
+ }
150
+
25
151
  /**
26
152
  * Checks if a buffer contains a TLS handshake message (record type 22)
27
153
  * @param buffer - The buffer to check
@@ -153,6 +279,103 @@ export class SniHandler {
153
279
  return buffer[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE;
154
280
  }
155
281
 
282
+ /**
283
+ * Detects characteristics of a tab reactivation TLS handshake
284
+ * These often have specific patterns in Chrome and other browsers
285
+ *
286
+ * @param buffer - The buffer containing a ClientHello message
287
+ * @param enableLogging - Whether to enable logging
288
+ * @returns true if this appears to be a tab reactivation handshake
289
+ */
290
+ public static isTabReactivationHandshake(
291
+ buffer: Buffer,
292
+ enableLogging: boolean = false
293
+ ): boolean {
294
+ const log = (message: string) => {
295
+ if (enableLogging) {
296
+ console.log(`[Tab Reactivation] ${message}`);
297
+ }
298
+ };
299
+
300
+ if (!this.isClientHello(buffer)) {
301
+ return false;
302
+ }
303
+
304
+ try {
305
+ // Check for session ID presence (tab reactivation often has a session ID)
306
+ let pos = 5 + 1 + 3 + 2; // Position after handshake type, length and client version
307
+ pos += 32; // Skip client random
308
+
309
+ if (pos + 1 > buffer.length) return false;
310
+
311
+ const sessionIdLength = buffer[pos];
312
+
313
+ // Non-empty session ID is a good indicator
314
+ if (sessionIdLength > 0) {
315
+ log(`Detected non-empty session ID (length: ${sessionIdLength})`);
316
+
317
+ // Skip to extensions
318
+ pos += 1 + sessionIdLength;
319
+
320
+ // Skip cipher suites
321
+ if (pos + 2 > buffer.length) return false;
322
+ const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
323
+ pos += 2 + cipherSuitesLength;
324
+
325
+ // Skip compression methods
326
+ if (pos + 1 > buffer.length) return false;
327
+ const compressionMethodsLength = buffer[pos];
328
+ pos += 1 + compressionMethodsLength;
329
+
330
+ // Check for extensions
331
+ if (pos + 2 > buffer.length) return false;
332
+
333
+ // Look for specific extensions that indicate tab reactivation
334
+ const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
335
+ pos += 2;
336
+
337
+ // Extensions end position
338
+ const extensionsEnd = pos + extensionsLength;
339
+ if (extensionsEnd > buffer.length) return false;
340
+
341
+ // Tab reactivation often has session tickets but no SNI
342
+ let hasSessionTicket = false;
343
+ let hasSNI = false;
344
+ let hasPSK = false;
345
+
346
+ // Iterate through extensions
347
+ while (pos + 4 <= extensionsEnd) {
348
+ const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
349
+ pos += 2;
350
+
351
+ const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
352
+ pos += 2;
353
+
354
+ if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
355
+ hasSessionTicket = true;
356
+ } else if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
357
+ hasSNI = true;
358
+ } else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
359
+ hasPSK = true;
360
+ }
361
+
362
+ // Skip extension data
363
+ pos += extensionLength;
364
+ }
365
+
366
+ // Pattern for tab reactivation: session identifier + (ticket or PSK) but no SNI
367
+ if ((hasSessionTicket || hasPSK) && !hasSNI) {
368
+ log('Detected tab reactivation pattern: session resumption without SNI');
369
+ return true;
370
+ }
371
+ }
372
+ } catch (error) {
373
+ log(`Error checking for tab reactivation: ${error}`);
374
+ }
375
+
376
+ return false;
377
+ }
378
+
156
379
  /**
157
380
  * Extracts the SNI (Server Name Indication) from a TLS ClientHello message.
158
381
  * Implements robust parsing with support for session resumption edge cases.
@@ -523,7 +746,11 @@ export class SniHandler {
523
746
  pos += identityLength;
524
747
 
525
748
  // Skip obfuscated ticket age (4 bytes)
526
- pos += 4;
749
+ if (pos + 4 <= identitiesEnd) {
750
+ pos += 4;
751
+ } else {
752
+ break;
753
+ }
527
754
 
528
755
  // Try to parse the identity as UTF-8
529
756
  try {
@@ -673,6 +900,7 @@ export class SniHandler {
673
900
  * 4. Fragmented ClientHello messages
674
901
  * 5. TLS 1.3 Early Data (0-RTT)
675
902
  * 6. Chrome's connection racing behaviors
903
+ * 7. Tab reactivation patterns with session cache
676
904
  *
677
905
  * @param buffer - The buffer containing the TLS ClientHello message
678
906
  * @param connectionInfo - Optional connection information for fragment handling
@@ -718,15 +946,41 @@ export class SniHandler {
718
946
  const standardSni = this.extractSNI(processBuffer, enableLogging);
719
947
  if (standardSni) {
720
948
  log(`Found standard SNI: ${standardSni}`);
949
+
950
+ // If we extracted a standard SNI, cache it for future use
951
+ if (connectionInfo?.sourceIp) {
952
+ const clientRandom = this.extractClientRandom(processBuffer);
953
+ this.cacheSession(connectionInfo.sourceIp, standardSni, clientRandom);
954
+ log(`Cached SNI for future reference: ${standardSni}`);
955
+ }
956
+
721
957
  return standardSni;
722
958
  }
723
959
 
960
+ // Check for tab reactivation pattern
961
+ const isTabReactivation = this.isTabReactivationHandshake(processBuffer, enableLogging);
962
+ if (isTabReactivation && connectionInfo?.sourceIp) {
963
+ // Try to get the SNI from our session cache for tab reactivation
964
+ const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
965
+ if (cachedSni) {
966
+ log(`Retrieved cached SNI for tab reactivation: ${cachedSni}`);
967
+ return cachedSni;
968
+ }
969
+ log('Tab reactivation detected but no cached SNI found');
970
+ }
971
+
724
972
  // Check for TLS 1.3 early data (0-RTT)
725
973
  const hasEarly = this.hasEarlyData(processBuffer, enableLogging);
726
974
  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
975
+ log('TLS 1.3 Early Data detected, trying session cache');
976
+ // For 0-RTT, check the session cache
977
+ if (connectionInfo?.sourceIp) {
978
+ const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
979
+ if (cachedSni) {
980
+ log(`Retrieved cached SNI for 0-RTT: ${cachedSni}`);
981
+ return cachedSni;
982
+ }
983
+ }
730
984
  }
731
985
 
732
986
  // If standard extraction failed and we have a valid ClientHello,
@@ -738,18 +992,26 @@ export class SniHandler {
738
992
  const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
739
993
  if (pskSni) {
740
994
  log(`Extracted SNI from PSK extension: ${pskSni}`);
995
+
996
+ // Cache this SNI for future reference
997
+ if (connectionInfo?.sourceIp) {
998
+ const clientRandom = this.extractClientRandom(processBuffer);
999
+ this.cacheSession(connectionInfo.sourceIp, pskSni, clientRandom);
1000
+ log(`Cached PSK-derived SNI: ${pskSni}`);
1001
+ }
1002
+
741
1003
  return pskSni;
742
1004
  }
743
1005
 
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
1006
+ // If we have a session ticket but no SNI or PSK identity,
1007
+ // check our session cache as a last resort
1008
+ if (connectionInfo?.sourceIp) {
1009
+ const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
1010
+ if (cachedSni) {
1011
+ log(`Using cached SNI as last resort: ${cachedSni}`);
1012
+ return cachedSni;
1013
+ }
1014
+ }
753
1015
 
754
1016
  log('Failed to extract SNI from resumption mechanisms');
755
1017
  }
@@ -763,7 +1025,7 @@ export class SniHandler {
763
1025
  *
764
1026
  * The method uses connection tracking to handle fragmented ClientHello
765
1027
  * messages and various TLS 1.3 behaviors, including Chrome's connection
766
- * racing patterns.
1028
+ * racing patterns and tab reactivation behaviors.
767
1029
  *
768
1030
  * @param buffer - The buffer containing TLS data
769
1031
  * @param connectionInfo - Connection metadata (IPs and ports)
@@ -794,7 +1056,7 @@ export class SniHandler {
794
1056
  connectionInfo.timestamp = Date.now();
795
1057
  }
796
1058
 
797
- // Check if this is a TLS handshake
1059
+ // Check if this is a TLS handshake or application data
798
1060
  if (!this.isTlsHandshake(buffer) && !this.isTlsApplicationData(buffer)) {
799
1061
  log('Not a TLS handshake or application data packet');
800
1062
  return undefined;
@@ -804,15 +1066,26 @@ export class SniHandler {
804
1066
  const connectionId = this.createConnectionId(connectionInfo);
805
1067
  log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`);
806
1068
 
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;
1069
+ // Handle application data with cached SNI (for connection racing)
1070
+ if (this.isTlsApplicationData(buffer)) {
1071
+ // First check if explicit cachedSni was provided
1072
+ if (cachedSni) {
1073
+ log(`Using provided cached SNI for application data: ${cachedSni}`);
1074
+ return cachedSni;
1075
+ }
1076
+
1077
+ // Otherwise check our session cache
1078
+ const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
1079
+ if (sessionCachedSni) {
1080
+ log(`Using session-cached SNI for application data: ${sessionCachedSni}`);
1081
+ return sessionCachedSni;
1082
+ }
1083
+
1084
+ log('Application data packet without cached SNI, cannot determine hostname');
1085
+ return undefined;
813
1086
  }
814
1087
 
815
- // Try to extract SNI with full resumption support and fragment handling
1088
+ // For handshake messages, try the full extraction process
816
1089
  const sni = this.extractSNIWithResumptionSupport(
817
1090
  buffer,
818
1091
  connectionInfo,
@@ -828,6 +1101,13 @@ export class SniHandler {
828
1101
  // If it is, but we couldn't get an SNI, it might be a fragment or
829
1102
  // a connection race situation
830
1103
  if (this.isClientHello(buffer)) {
1104
+ // Check if we have a cached session for this IP
1105
+ const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
1106
+ if (sessionCachedSni) {
1107
+ log(`Using session cache for ClientHello without SNI: ${sessionCachedSni}`);
1108
+ return sessionCachedSni;
1109
+ }
1110
+
831
1111
  log('Valid ClientHello detected, but no SNI extracted - might need more data');
832
1112
  }
833
1113