@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.portproxy.d.ts +1 -0
- package/dist_ts/classes.portproxy.js +49 -1
- package/dist_ts/classes.snihandler.d.ts +66 -3
- package/dist_ts/classes.snihandler.js +364 -25
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +61 -0
- package/ts/classes.snihandler.ts +416 -24
package/ts/classes.portproxy.ts
CHANGED
|
@@ -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 = {
|
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,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
|
|
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,
|
|
728
|
-
//
|
|
729
|
-
|
|
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
|
-
//
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
//
|
|
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
|
|