@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.portproxy.d.ts +3 -0
- package/dist_ts/classes.portproxy.js +49 -6
- package/dist_ts/classes.snihandler.d.ts +57 -3
- package/dist_ts/classes.snihandler.js +267 -24
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +80 -6
- package/ts/classes.snihandler.ts +304 -24
package/ts/classes.snihandler.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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,
|
|
728
|
-
//
|
|
729
|
-
|
|
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
|
-
//
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
//
|
|
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
|
|