@push.rocks/smartproxy 3.39.0 → 3.41.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.
@@ -51,6 +51,7 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
51
51
  enableDetailedLogging?: boolean; // Enable detailed connection logging
52
52
  enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
53
53
  enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
54
+ allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true)
54
55
 
55
56
  // Rate limiting and security
56
57
  maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
@@ -236,6 +237,8 @@ export class PortProxy {
236
237
  enableDetailedLogging: settingsArg.enableDetailedLogging || false,
237
238
  enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
238
239
  enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
240
+ allowSessionTicket: settingsArg.allowSessionTicket !== undefined
241
+ ? settingsArg.allowSessionTicket : true,
239
242
 
240
243
  // Rate limiting defaults
241
244
  maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
@@ -935,6 +938,21 @@ export class PortProxy {
935
938
  destPort: record.incoming.localPort || 0
936
939
  };
937
940
 
941
+ // Check for session tickets if allowSessionTicket is disabled
942
+ if (this.settings.allowSessionTicket === false) {
943
+ // Analyze for session resumption attempt (session ticket or PSK)
944
+ const hasSessionTicket = SniHandler.hasSessionResumption(renegChunk, this.settings.enableTlsDebugLogging);
945
+
946
+ if (hasSessionTicket) {
947
+ console.log(
948
+ `[${connectionId}] Session ticket detected in renegotiation with allowSessionTicket=false. ` +
949
+ `Terminating connection to force new TLS handshake.`
950
+ );
951
+ this.initiateCleanupOnce(record, 'session_ticket_blocked');
952
+ return;
953
+ }
954
+ }
955
+
938
956
  const newSNI = SniHandler.extractSNIWithResumptionSupport(renegChunk, connInfo, this.settings.enableTlsDebugLogging);
939
957
 
940
958
  // Skip if no SNI was found
@@ -970,6 +988,9 @@ export class PortProxy {
970
988
 
971
989
  if (this.settings.enableDetailedLogging) {
972
990
  console.log(`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`);
991
+ if (this.settings.allowSessionTicket === false) {
992
+ console.log(`[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`);
993
+ }
973
994
  }
974
995
  }
975
996
 
@@ -1541,6 +1562,26 @@ export class PortProxy {
1541
1562
  if (SniHandler.isTlsHandshake(chunk)) {
1542
1563
  connectionRecord.isTLS = true;
1543
1564
 
1565
+ // Check for session tickets if allowSessionTicket is disabled
1566
+ if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
1567
+ // Analyze for session resumption attempt
1568
+ const hasSessionTicket = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging);
1569
+
1570
+ if (hasSessionTicket) {
1571
+ console.log(
1572
+ `[${connectionId}] Session ticket detected in initial ClientHello with allowSessionTicket=false. ` +
1573
+ `Terminating connection to force new TLS handshake.`
1574
+ );
1575
+ if (connectionRecord.incomingTerminationReason === null) {
1576
+ connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
1577
+ this.incrementTerminationStat('incoming', 'session_ticket_blocked');
1578
+ }
1579
+ socket.end();
1580
+ this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
1581
+ return;
1582
+ }
1583
+ }
1584
+
1544
1585
  // Try to extract SNI for domain-specific NetworkProxy handling
1545
1586
  const connInfo = {
1546
1587
  sourceIp: remoteIP,
@@ -1886,6 +1927,26 @@ export class PortProxy {
1886
1927
  `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
1887
1928
  );
1888
1929
  }
1930
+
1931
+ // Check for session tickets if allowSessionTicket is disabled
1932
+ if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
1933
+ // Analyze for session resumption attempt
1934
+ const hasSessionTicket = SniHandler.hasSessionResumption(chunk, this.settings.enableTlsDebugLogging);
1935
+
1936
+ if (hasSessionTicket) {
1937
+ console.log(
1938
+ `[${connectionId}] Session ticket detected in initial ClientHello with allowSessionTicket=false. ` +
1939
+ `Terminating connection to force new TLS handshake.`
1940
+ );
1941
+ if (connectionRecord.incomingTerminationReason === null) {
1942
+ connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
1943
+ this.incrementTerminationStat('incoming', 'session_ticket_blocked');
1944
+ }
1945
+ socket.end();
1946
+ this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
1947
+ return;
1948
+ }
1949
+ }
1889
1950
 
1890
1951
  // Create connection info object for SNI extraction
1891
1952
  const connInfo = {
@@ -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,215 @@ export class SniHandler {
153
279
  return buffer[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE;
154
280
  }
155
281
 
282
+ /**
283
+ * Checks if a ClientHello message contains session resumption indicators
284
+ * such as session tickets or PSK (Pre-Shared Key) extensions.
285
+ *
286
+ * @param buffer - The buffer containing a ClientHello message
287
+ * @param enableLogging - Whether to enable logging
288
+ * @returns true if the ClientHello contains session resumption mechanisms
289
+ */
290
+ public static hasSessionResumption(
291
+ buffer: Buffer,
292
+ enableLogging: boolean = false
293
+ ): boolean {
294
+ const log = (message: string) => {
295
+ if (enableLogging) {
296
+ console.log(`[Session Resumption] ${message}`);
297
+ }
298
+ };
299
+
300
+ if (!this.isClientHello(buffer)) {
301
+ return false;
302
+ }
303
+
304
+ try {
305
+ // Check for session ID presence first
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
+ let hasNonEmptySessionId = sessionIdLength > 0;
313
+
314
+ if (hasNonEmptySessionId) {
315
+ log(`Detected non-empty session ID (length: ${sessionIdLength})`);
316
+ }
317
+
318
+ // Continue to check for extensions
319
+ pos += 1 + sessionIdLength;
320
+
321
+ // Skip cipher suites
322
+ if (pos + 2 > buffer.length) return false;
323
+ const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
324
+ pos += 2 + cipherSuitesLength;
325
+
326
+ // Skip compression methods
327
+ if (pos + 1 > buffer.length) return false;
328
+ const compressionMethodsLength = buffer[pos];
329
+ pos += 1 + compressionMethodsLength;
330
+
331
+ // Check for extensions
332
+ if (pos + 2 > buffer.length) return false;
333
+
334
+ // Look for session resumption extensions
335
+ const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
336
+ pos += 2;
337
+
338
+ // Extensions end position
339
+ const extensionsEnd = pos + extensionsLength;
340
+ if (extensionsEnd > buffer.length) return false;
341
+
342
+ // Track resumption indicators
343
+ let hasSessionTicket = false;
344
+ let hasPSK = false;
345
+ let hasEarlyData = false;
346
+
347
+ // Iterate through extensions
348
+ while (pos + 4 <= extensionsEnd) {
349
+ const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
350
+ pos += 2;
351
+
352
+ const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
353
+ pos += 2;
354
+
355
+ if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
356
+ log('Found session ticket extension');
357
+ hasSessionTicket = true;
358
+
359
+ // Check if session ticket has non-zero length (active ticket)
360
+ if (extensionLength > 0) {
361
+ log(`Session ticket has length ${extensionLength} - active ticket present`);
362
+ }
363
+ } else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
364
+ log('Found PSK extension (TLS 1.3 resumption mechanism)');
365
+ hasPSK = true;
366
+ } else if (extensionType === this.TLS_EARLY_DATA_EXTENSION_TYPE) {
367
+ log('Found Early Data extension (TLS 1.3 0-RTT)');
368
+ hasEarlyData = true;
369
+ }
370
+
371
+ // Skip extension data
372
+ pos += extensionLength;
373
+ }
374
+
375
+ // Consider it a resumption if any resumption mechanism is present
376
+ const isResumption = hasSessionTicket || hasPSK || hasEarlyData ||
377
+ (hasNonEmptySessionId && !hasPSK); // Legacy resumption
378
+
379
+ if (isResumption) {
380
+ log('Session resumption detected: ' +
381
+ (hasSessionTicket ? 'session ticket, ' : '') +
382
+ (hasPSK ? 'PSK, ' : '') +
383
+ (hasEarlyData ? 'early data, ' : '') +
384
+ (hasNonEmptySessionId ? 'session ID' : ''));
385
+ }
386
+
387
+ return isResumption;
388
+ } catch (error) {
389
+ log(`Error checking for session resumption: ${error}`);
390
+ return false;
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Detects characteristics of a tab reactivation TLS handshake
396
+ * These often have specific patterns in Chrome and other browsers
397
+ *
398
+ * @param buffer - The buffer containing a ClientHello message
399
+ * @param enableLogging - Whether to enable logging
400
+ * @returns true if this appears to be a tab reactivation handshake
401
+ */
402
+ public static isTabReactivationHandshake(
403
+ buffer: Buffer,
404
+ enableLogging: boolean = false
405
+ ): boolean {
406
+ const log = (message: string) => {
407
+ if (enableLogging) {
408
+ console.log(`[Tab Reactivation] ${message}`);
409
+ }
410
+ };
411
+
412
+ if (!this.isClientHello(buffer)) {
413
+ return false;
414
+ }
415
+
416
+ try {
417
+ // Check for session ID presence (tab reactivation often has a session ID)
418
+ let pos = 5 + 1 + 3 + 2; // Position after handshake type, length and client version
419
+ pos += 32; // Skip client random
420
+
421
+ if (pos + 1 > buffer.length) return false;
422
+
423
+ const sessionIdLength = buffer[pos];
424
+
425
+ // Non-empty session ID is a good indicator
426
+ if (sessionIdLength > 0) {
427
+ log(`Detected non-empty session ID (length: ${sessionIdLength})`);
428
+
429
+ // Skip to extensions
430
+ pos += 1 + sessionIdLength;
431
+
432
+ // Skip cipher suites
433
+ if (pos + 2 > buffer.length) return false;
434
+ const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
435
+ pos += 2 + cipherSuitesLength;
436
+
437
+ // Skip compression methods
438
+ if (pos + 1 > buffer.length) return false;
439
+ const compressionMethodsLength = buffer[pos];
440
+ pos += 1 + compressionMethodsLength;
441
+
442
+ // Check for extensions
443
+ if (pos + 2 > buffer.length) return false;
444
+
445
+ // Look for specific extensions that indicate tab reactivation
446
+ const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
447
+ pos += 2;
448
+
449
+ // Extensions end position
450
+ const extensionsEnd = pos + extensionsLength;
451
+ if (extensionsEnd > buffer.length) return false;
452
+
453
+ // Tab reactivation often has session tickets but no SNI
454
+ let hasSessionTicket = false;
455
+ let hasSNI = false;
456
+ let hasPSK = false;
457
+
458
+ // Iterate through extensions
459
+ while (pos + 4 <= extensionsEnd) {
460
+ const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
461
+ pos += 2;
462
+
463
+ const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
464
+ pos += 2;
465
+
466
+ if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
467
+ hasSessionTicket = true;
468
+ } else if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
469
+ hasSNI = true;
470
+ } else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
471
+ hasPSK = true;
472
+ }
473
+
474
+ // Skip extension data
475
+ pos += extensionLength;
476
+ }
477
+
478
+ // Pattern for tab reactivation: session identifier + (ticket or PSK) but no SNI
479
+ if ((hasSessionTicket || hasPSK) && !hasSNI) {
480
+ log('Detected tab reactivation pattern: session resumption without SNI');
481
+ return true;
482
+ }
483
+ }
484
+ } catch (error) {
485
+ log(`Error checking for tab reactivation: ${error}`);
486
+ }
487
+
488
+ return false;
489
+ }
490
+
156
491
  /**
157
492
  * Extracts the SNI (Server Name Indication) from a TLS ClientHello message.
158
493
  * Implements robust parsing with support for session resumption edge cases.
@@ -523,7 +858,11 @@ export class SniHandler {
523
858
  pos += identityLength;
524
859
 
525
860
  // Skip obfuscated ticket age (4 bytes)
526
- pos += 4;
861
+ if (pos + 4 <= identitiesEnd) {
862
+ pos += 4;
863
+ } else {
864
+ break;
865
+ }
527
866
 
528
867
  // Try to parse the identity as UTF-8
529
868
  try {
@@ -673,6 +1012,7 @@ export class SniHandler {
673
1012
  * 4. Fragmented ClientHello messages
674
1013
  * 5. TLS 1.3 Early Data (0-RTT)
675
1014
  * 6. Chrome's connection racing behaviors
1015
+ * 7. Tab reactivation patterns with session cache
676
1016
  *
677
1017
  * @param buffer - The buffer containing the TLS ClientHello message
678
1018
  * @param connectionInfo - Optional connection information for fragment handling
@@ -718,15 +1058,41 @@ export class SniHandler {
718
1058
  const standardSni = this.extractSNI(processBuffer, enableLogging);
719
1059
  if (standardSni) {
720
1060
  log(`Found standard SNI: ${standardSni}`);
1061
+
1062
+ // If we extracted a standard SNI, cache it for future use
1063
+ if (connectionInfo?.sourceIp) {
1064
+ const clientRandom = this.extractClientRandom(processBuffer);
1065
+ this.cacheSession(connectionInfo.sourceIp, standardSni, clientRandom);
1066
+ log(`Cached SNI for future reference: ${standardSni}`);
1067
+ }
1068
+
721
1069
  return standardSni;
722
1070
  }
723
1071
 
1072
+ // Check for tab reactivation pattern
1073
+ const isTabReactivation = this.isTabReactivationHandshake(processBuffer, enableLogging);
1074
+ if (isTabReactivation && connectionInfo?.sourceIp) {
1075
+ // Try to get the SNI from our session cache for tab reactivation
1076
+ const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
1077
+ if (cachedSni) {
1078
+ log(`Retrieved cached SNI for tab reactivation: ${cachedSni}`);
1079
+ return cachedSni;
1080
+ }
1081
+ log('Tab reactivation detected but no cached SNI found');
1082
+ }
1083
+
724
1084
  // Check for TLS 1.3 early data (0-RTT)
725
1085
  const hasEarly = this.hasEarlyData(processBuffer, enableLogging);
726
1086
  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
1087
+ log('TLS 1.3 Early Data detected, trying session cache');
1088
+ // For 0-RTT, check the session cache
1089
+ if (connectionInfo?.sourceIp) {
1090
+ const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
1091
+ if (cachedSni) {
1092
+ log(`Retrieved cached SNI for 0-RTT: ${cachedSni}`);
1093
+ return cachedSni;
1094
+ }
1095
+ }
730
1096
  }
731
1097
 
732
1098
  // If standard extraction failed and we have a valid ClientHello,
@@ -738,18 +1104,26 @@ export class SniHandler {
738
1104
  const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
739
1105
  if (pskSni) {
740
1106
  log(`Extracted SNI from PSK extension: ${pskSni}`);
1107
+
1108
+ // Cache this SNI for future reference
1109
+ if (connectionInfo?.sourceIp) {
1110
+ const clientRandom = this.extractClientRandom(processBuffer);
1111
+ this.cacheSession(connectionInfo.sourceIp, pskSni, clientRandom);
1112
+ log(`Cached PSK-derived SNI: ${pskSni}`);
1113
+ }
1114
+
741
1115
  return pskSni;
742
1116
  }
743
1117
 
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
1118
+ // If we have a session ticket but no SNI or PSK identity,
1119
+ // check our session cache as a last resort
1120
+ if (connectionInfo?.sourceIp) {
1121
+ const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
1122
+ if (cachedSni) {
1123
+ log(`Using cached SNI as last resort: ${cachedSni}`);
1124
+ return cachedSni;
1125
+ }
1126
+ }
753
1127
 
754
1128
  log('Failed to extract SNI from resumption mechanisms');
755
1129
  }
@@ -763,7 +1137,7 @@ export class SniHandler {
763
1137
  *
764
1138
  * The method uses connection tracking to handle fragmented ClientHello
765
1139
  * messages and various TLS 1.3 behaviors, including Chrome's connection
766
- * racing patterns.
1140
+ * racing patterns and tab reactivation behaviors.
767
1141
  *
768
1142
  * @param buffer - The buffer containing TLS data
769
1143
  * @param connectionInfo - Connection metadata (IPs and ports)
@@ -794,7 +1168,7 @@ export class SniHandler {
794
1168
  connectionInfo.timestamp = Date.now();
795
1169
  }
796
1170
 
797
- // Check if this is a TLS handshake
1171
+ // Check if this is a TLS handshake or application data
798
1172
  if (!this.isTlsHandshake(buffer) && !this.isTlsApplicationData(buffer)) {
799
1173
  log('Not a TLS handshake or application data packet');
800
1174
  return undefined;
@@ -804,15 +1178,26 @@ export class SniHandler {
804
1178
  const connectionId = this.createConnectionId(connectionInfo);
805
1179
  log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`);
806
1180
 
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;
1181
+ // Handle application data with cached SNI (for connection racing)
1182
+ if (this.isTlsApplicationData(buffer)) {
1183
+ // First check if explicit cachedSni was provided
1184
+ if (cachedSni) {
1185
+ log(`Using provided cached SNI for application data: ${cachedSni}`);
1186
+ return cachedSni;
1187
+ }
1188
+
1189
+ // Otherwise check our session cache
1190
+ const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
1191
+ if (sessionCachedSni) {
1192
+ log(`Using session-cached SNI for application data: ${sessionCachedSni}`);
1193
+ return sessionCachedSni;
1194
+ }
1195
+
1196
+ log('Application data packet without cached SNI, cannot determine hostname');
1197
+ return undefined;
813
1198
  }
814
1199
 
815
- // Try to extract SNI with full resumption support and fragment handling
1200
+ // For handshake messages, try the full extraction process
816
1201
  const sni = this.extractSNIWithResumptionSupport(
817
1202
  buffer,
818
1203
  connectionInfo,
@@ -828,6 +1213,13 @@ export class SniHandler {
828
1213
  // If it is, but we couldn't get an SNI, it might be a fragment or
829
1214
  // a connection race situation
830
1215
  if (this.isClientHello(buffer)) {
1216
+ // Check if we have a cached session for this IP
1217
+ const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
1218
+ if (sessionCachedSni) {
1219
+ log(`Using session cache for ClientHello without SNI: ${sessionCachedSni}`);
1220
+ return sessionCachedSni;
1221
+ }
1222
+
831
1223
  log('Valid ClientHello detected, but no SNI extracted - might need more data');
832
1224
  }
833
1225