@push.rocks/smartproxy 3.41.4 → 3.41.6
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.js +58 -26
- package/dist_ts/classes.snihandler.js +106 -62
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +71 -30
- package/ts/classes.snihandler.ts +328 -256
package/ts/classes.snihandler.ts
CHANGED
|
@@ -9,29 +9,32 @@ import { Buffer } from 'buffer';
|
|
|
9
9
|
export class SniHandler {
|
|
10
10
|
// TLS record types and constants
|
|
11
11
|
private static readonly TLS_HANDSHAKE_RECORD_TYPE = 22;
|
|
12
|
-
private static readonly TLS_APPLICATION_DATA_TYPE = 23;
|
|
12
|
+
private static readonly TLS_APPLICATION_DATA_TYPE = 23; // TLS Application Data record type
|
|
13
13
|
private static readonly TLS_CLIENT_HELLO_HANDSHAKE_TYPE = 1;
|
|
14
14
|
private static readonly TLS_SNI_EXTENSION_TYPE = 0x0000;
|
|
15
15
|
private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023;
|
|
16
16
|
private static readonly TLS_SNI_HOST_NAME_TYPE = 0;
|
|
17
17
|
private static readonly TLS_PSK_EXTENSION_TYPE = 0x0029; // Pre-Shared Key extension type for TLS 1.3
|
|
18
|
-
private static readonly TLS_PSK_KE_MODES_EXTENSION_TYPE =
|
|
19
|
-
private static readonly TLS_EARLY_DATA_EXTENSION_TYPE =
|
|
20
|
-
|
|
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
21
|
// Buffer for handling fragmented ClientHello messages
|
|
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
25
|
// Session tracking for tab reactivation scenarios
|
|
26
|
-
private static sessionCache: Map<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
private static sessionCache: Map<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
sni: string;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
clientRandom?: Buffer;
|
|
32
|
+
}
|
|
33
|
+
> = new Map();
|
|
34
|
+
|
|
32
35
|
// Longer timeout for session cache (24 hours by default)
|
|
33
36
|
private static sessionCacheTimeout: number = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
|
34
|
-
|
|
37
|
+
|
|
35
38
|
// Cleanup interval for session cache (run every hour)
|
|
36
39
|
private static sessionCleanupInterval: NodeJS.Timeout | null = null;
|
|
37
40
|
|
|
@@ -53,14 +56,14 @@ export class SniHandler {
|
|
|
53
56
|
private static cleanupSessionCache(): void {
|
|
54
57
|
const now = Date.now();
|
|
55
58
|
const expiredKeys: string[] = [];
|
|
56
|
-
|
|
59
|
+
|
|
57
60
|
this.sessionCache.forEach((session, key) => {
|
|
58
61
|
if (now - session.timestamp > this.sessionCacheTimeout) {
|
|
59
62
|
expiredKeys.push(key);
|
|
60
63
|
}
|
|
61
64
|
});
|
|
62
|
-
|
|
63
|
-
expiredKeys.forEach(key => {
|
|
65
|
+
|
|
66
|
+
expiredKeys.forEach((key) => {
|
|
64
67
|
this.sessionCache.delete(key);
|
|
65
68
|
});
|
|
66
69
|
}
|
|
@@ -68,7 +71,7 @@ export class SniHandler {
|
|
|
68
71
|
/**
|
|
69
72
|
* Create a client identity key for session tracking
|
|
70
73
|
* Uses source IP and optional client random for uniqueness
|
|
71
|
-
*
|
|
74
|
+
*
|
|
72
75
|
* @param sourceIp - Client IP address
|
|
73
76
|
* @param clientRandom - Optional TLS client random value
|
|
74
77
|
* @returns A string key for the session cache
|
|
@@ -84,7 +87,7 @@ export class SniHandler {
|
|
|
84
87
|
|
|
85
88
|
/**
|
|
86
89
|
* Store SNI information in the session cache
|
|
87
|
-
*
|
|
90
|
+
*
|
|
88
91
|
* @param sourceIp - Client IP address
|
|
89
92
|
* @param sni - The extracted SNI value
|
|
90
93
|
* @param clientRandom - Optional TLS client random value
|
|
@@ -94,13 +97,13 @@ export class SniHandler {
|
|
|
94
97
|
this.sessionCache.set(key, {
|
|
95
98
|
sni,
|
|
96
99
|
timestamp: Date.now(),
|
|
97
|
-
clientRandom
|
|
100
|
+
clientRandom,
|
|
98
101
|
});
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
/**
|
|
102
105
|
* Retrieve SNI information from the session cache
|
|
103
|
-
*
|
|
106
|
+
*
|
|
104
107
|
* @param sourceIp - Client IP address
|
|
105
108
|
* @param clientRandom - Optional TLS client random value
|
|
106
109
|
* @returns The cached SNI or undefined if not found
|
|
@@ -114,7 +117,7 @@ export class SniHandler {
|
|
|
114
117
|
return preciseSession.sni;
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
|
-
|
|
120
|
+
|
|
118
121
|
// Fall back to IP-only lookup
|
|
119
122
|
const ipKey = this.createClientKey(sourceIp);
|
|
120
123
|
const session = this.sessionCache.get(ipKey);
|
|
@@ -123,13 +126,13 @@ export class SniHandler {
|
|
|
123
126
|
session.timestamp = Date.now();
|
|
124
127
|
return session.sni;
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
|
|
127
130
|
return undefined;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
/**
|
|
131
134
|
* Extract the client random value from a ClientHello message
|
|
132
|
-
*
|
|
135
|
+
*
|
|
133
136
|
* @param buffer - The buffer containing the ClientHello
|
|
134
137
|
* @returns The 32-byte client random or undefined if extraction fails
|
|
135
138
|
*/
|
|
@@ -138,9 +141,9 @@ export class SniHandler {
|
|
|
138
141
|
if (!this.isClientHello(buffer) || buffer.length < 46) {
|
|
139
142
|
return undefined;
|
|
140
143
|
}
|
|
141
|
-
|
|
144
|
+
|
|
142
145
|
// In a ClientHello message, the client random starts at position 11
|
|
143
|
-
// after record header (5 bytes), handshake type (1 byte),
|
|
146
|
+
// after record header (5 bytes), handshake type (1 byte),
|
|
144
147
|
// handshake length (3 bytes), and client version (2 bytes)
|
|
145
148
|
return buffer.slice(11, 11 + 32);
|
|
146
149
|
} catch (error) {
|
|
@@ -156,7 +159,7 @@ export class SniHandler {
|
|
|
156
159
|
public static isTlsHandshake(buffer: Buffer): boolean {
|
|
157
160
|
return buffer.length > 0 && buffer[0] === this.TLS_HANDSHAKE_RECORD_TYPE;
|
|
158
161
|
}
|
|
159
|
-
|
|
162
|
+
|
|
160
163
|
/**
|
|
161
164
|
* Checks if a buffer contains TLS application data (record type 23)
|
|
162
165
|
* @param buffer - The buffer to check
|
|
@@ -165,28 +168,28 @@ export class SniHandler {
|
|
|
165
168
|
public static isTlsApplicationData(buffer: Buffer): boolean {
|
|
166
169
|
return buffer.length > 0 && buffer[0] === this.TLS_APPLICATION_DATA_TYPE;
|
|
167
170
|
}
|
|
168
|
-
|
|
171
|
+
|
|
169
172
|
/**
|
|
170
173
|
* Creates a connection ID based on source/destination information
|
|
171
174
|
* Used to track fragmented ClientHello messages across multiple packets
|
|
172
|
-
*
|
|
175
|
+
*
|
|
173
176
|
* @param connectionInfo - Object containing connection identifiers (IP/port)
|
|
174
177
|
* @returns A string ID for the connection
|
|
175
178
|
*/
|
|
176
|
-
public static createConnectionId(connectionInfo: {
|
|
177
|
-
sourceIp?: string;
|
|
178
|
-
sourcePort?: number;
|
|
179
|
-
destIp?: string;
|
|
180
|
-
destPort?: number;
|
|
179
|
+
public static createConnectionId(connectionInfo: {
|
|
180
|
+
sourceIp?: string;
|
|
181
|
+
sourcePort?: number;
|
|
182
|
+
destIp?: string;
|
|
183
|
+
destPort?: number;
|
|
181
184
|
}): string {
|
|
182
185
|
const { sourceIp, sourcePort, destIp, destPort } = connectionInfo;
|
|
183
186
|
return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`;
|
|
184
187
|
}
|
|
185
|
-
|
|
188
|
+
|
|
186
189
|
/**
|
|
187
190
|
* Handles potential fragmented ClientHello messages by buffering and reassembling
|
|
188
191
|
* TLS record fragments that might span multiple TCP packets.
|
|
189
|
-
*
|
|
192
|
+
*
|
|
190
193
|
* @param buffer - The current buffer fragment
|
|
191
194
|
* @param connectionId - Unique identifier for the connection
|
|
192
195
|
* @param enableLogging - Whether to enable logging
|
|
@@ -202,12 +205,12 @@ export class SniHandler {
|
|
|
202
205
|
console.log(`[SNI Fragment] ${message}`);
|
|
203
206
|
}
|
|
204
207
|
};
|
|
205
|
-
|
|
208
|
+
|
|
206
209
|
// Check if we've seen this connection before
|
|
207
210
|
if (!this.fragmentedBuffers.has(connectionId)) {
|
|
208
211
|
// New connection, start with this buffer
|
|
209
212
|
this.fragmentedBuffers.set(connectionId, buffer);
|
|
210
|
-
|
|
213
|
+
|
|
211
214
|
// Set timeout to clean up if we don't get a complete ClientHello
|
|
212
215
|
setTimeout(() => {
|
|
213
216
|
if (this.fragmentedBuffers.has(connectionId)) {
|
|
@@ -215,20 +218,28 @@ export class SniHandler {
|
|
|
215
218
|
log(`Connection ${connectionId} timed out waiting for complete ClientHello`);
|
|
216
219
|
}
|
|
217
220
|
}, this.fragmentTimeout);
|
|
218
|
-
|
|
221
|
+
|
|
219
222
|
// Evaluate if this buffer already contains a complete ClientHello
|
|
220
223
|
try {
|
|
221
224
|
if (buffer.length >= 5) {
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
// Get the record length from TLS header
|
|
226
|
+
const recordLength = (buffer[3] << 8) + buffer[4] + 5; // +5 for the TLS record header itself
|
|
227
|
+
log(`Initial buffer size: ${buffer.length}, expected record length: ${recordLength}`);
|
|
228
|
+
|
|
229
|
+
// Check if this buffer already contains a complete TLS record
|
|
230
|
+
if (buffer.length >= recordLength) {
|
|
224
231
|
log(`Initial buffer contains complete ClientHello, length: ${buffer.length}`);
|
|
225
232
|
return buffer;
|
|
226
233
|
}
|
|
234
|
+
} else {
|
|
235
|
+
log(
|
|
236
|
+
`Initial buffer too small (${buffer.length} bytes), needs at least 5 bytes for TLS header`
|
|
237
|
+
);
|
|
227
238
|
}
|
|
228
239
|
} catch (e) {
|
|
229
240
|
log(`Error checking initial buffer completeness: ${e}`);
|
|
230
241
|
}
|
|
231
|
-
|
|
242
|
+
|
|
232
243
|
log(`Started buffering connection ${connectionId}, initial size: ${buffer.length}`);
|
|
233
244
|
return undefined; // Need more fragments
|
|
234
245
|
} else {
|
|
@@ -236,24 +247,69 @@ export class SniHandler {
|
|
|
236
247
|
const existingBuffer = this.fragmentedBuffers.get(connectionId)!;
|
|
237
248
|
const newBuffer = Buffer.concat([existingBuffer, buffer]);
|
|
238
249
|
this.fragmentedBuffers.set(connectionId, newBuffer);
|
|
239
|
-
|
|
250
|
+
|
|
240
251
|
log(`Appended to buffer for ${connectionId}, new size: ${newBuffer.length}`);
|
|
241
|
-
|
|
252
|
+
|
|
242
253
|
// Check if we now have a complete ClientHello
|
|
243
254
|
try {
|
|
244
255
|
if (newBuffer.length >= 5) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
256
|
+
// Get the record length from TLS header
|
|
257
|
+
const recordLength = (newBuffer[3] << 8) + newBuffer[4] + 5; // +5 for the TLS record header itself
|
|
258
|
+
log(
|
|
259
|
+
`Reassembled buffer size: ${newBuffer.length}, expected record length: ${recordLength}`
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Check if we have a complete TLS record now
|
|
263
|
+
if (newBuffer.length >= recordLength) {
|
|
264
|
+
log(
|
|
265
|
+
`Assembled complete ClientHello, length: ${newBuffer.length}, needed: ${recordLength}`
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Extract the complete TLS record (might be followed by more data)
|
|
269
|
+
const completeRecord = newBuffer.slice(0, recordLength);
|
|
270
|
+
|
|
271
|
+
// Check if this record is indeed a ClientHello (type 1) at position 5
|
|
272
|
+
if (
|
|
273
|
+
completeRecord.length > 5 &&
|
|
274
|
+
completeRecord[5] === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE
|
|
275
|
+
) {
|
|
276
|
+
log(`Verified record is a ClientHello handshake message`);
|
|
277
|
+
|
|
278
|
+
// Complete message received, remove from tracking
|
|
279
|
+
this.fragmentedBuffers.delete(connectionId);
|
|
280
|
+
return completeRecord;
|
|
281
|
+
} else {
|
|
282
|
+
log(`Record is complete but not a ClientHello handshake, continuing to buffer`);
|
|
283
|
+
// This might be another TLS record type preceding the ClientHello
|
|
284
|
+
|
|
285
|
+
// Try checking for a ClientHello starting at the end of this record
|
|
286
|
+
if (newBuffer.length > recordLength + 5) {
|
|
287
|
+
const nextRecordType = newBuffer[recordLength];
|
|
288
|
+
log(
|
|
289
|
+
`Next record type: ${nextRecordType} (looking for ${this.TLS_HANDSHAKE_RECORD_TYPE})`
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (nextRecordType === this.TLS_HANDSHAKE_RECORD_TYPE) {
|
|
293
|
+
const handshakeType = newBuffer[recordLength + 5];
|
|
294
|
+
log(
|
|
295
|
+
`Next handshake type: ${handshakeType} (looking for ${this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE})`
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
if (handshakeType === this.TLS_CLIENT_HELLO_HANDSHAKE_TYPE) {
|
|
299
|
+
// Found a ClientHello in the next record, return the entire buffer
|
|
300
|
+
log(`Found ClientHello in subsequent record, returning full buffer`);
|
|
301
|
+
this.fragmentedBuffers.delete(connectionId);
|
|
302
|
+
return newBuffer;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
251
307
|
}
|
|
252
308
|
}
|
|
253
309
|
} catch (e) {
|
|
254
310
|
log(`Error checking reassembled buffer completeness: ${e}`);
|
|
255
311
|
}
|
|
256
|
-
|
|
312
|
+
|
|
257
313
|
return undefined; // Still need more fragments
|
|
258
314
|
}
|
|
259
315
|
}
|
|
@@ -282,7 +338,7 @@ export class SniHandler {
|
|
|
282
338
|
/**
|
|
283
339
|
* Checks if a ClientHello message contains session resumption indicators
|
|
284
340
|
* such as session tickets or PSK (Pre-Shared Key) extensions.
|
|
285
|
-
*
|
|
341
|
+
*
|
|
286
342
|
* @param buffer - The buffer containing a ClientHello message
|
|
287
343
|
* @param enableLogging - Whether to enable logging
|
|
288
344
|
* @returns Object containing details about session resumption and SNI presence
|
|
@@ -296,66 +352,66 @@ export class SniHandler {
|
|
|
296
352
|
console.log(`[Session Resumption] ${message}`);
|
|
297
353
|
}
|
|
298
354
|
};
|
|
299
|
-
|
|
355
|
+
|
|
300
356
|
if (!this.isClientHello(buffer)) {
|
|
301
357
|
return { isResumption: false, hasSNI: false };
|
|
302
358
|
}
|
|
303
|
-
|
|
359
|
+
|
|
304
360
|
try {
|
|
305
361
|
// Check for session ID presence first
|
|
306
362
|
let pos = 5 + 1 + 3 + 2; // Position after handshake type, length and client version
|
|
307
363
|
pos += 32; // Skip client random
|
|
308
|
-
|
|
364
|
+
|
|
309
365
|
if (pos + 1 > buffer.length) return { isResumption: false, hasSNI: false };
|
|
310
|
-
|
|
366
|
+
|
|
311
367
|
const sessionIdLength = buffer[pos];
|
|
312
368
|
let hasNonEmptySessionId = sessionIdLength > 0;
|
|
313
|
-
|
|
369
|
+
|
|
314
370
|
if (hasNonEmptySessionId) {
|
|
315
371
|
log(`Detected non-empty session ID (length: ${sessionIdLength})`);
|
|
316
372
|
}
|
|
317
|
-
|
|
373
|
+
|
|
318
374
|
// Continue to check for extensions
|
|
319
375
|
pos += 1 + sessionIdLength;
|
|
320
|
-
|
|
376
|
+
|
|
321
377
|
// Skip cipher suites
|
|
322
378
|
if (pos + 2 > buffer.length) return { isResumption: false, hasSNI: false };
|
|
323
379
|
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
324
380
|
pos += 2 + cipherSuitesLength;
|
|
325
|
-
|
|
381
|
+
|
|
326
382
|
// Skip compression methods
|
|
327
383
|
if (pos + 1 > buffer.length) return { isResumption: false, hasSNI: false };
|
|
328
384
|
const compressionMethodsLength = buffer[pos];
|
|
329
385
|
pos += 1 + compressionMethodsLength;
|
|
330
|
-
|
|
386
|
+
|
|
331
387
|
// Check for extensions
|
|
332
388
|
if (pos + 2 > buffer.length) return { isResumption: false, hasSNI: false };
|
|
333
|
-
|
|
389
|
+
|
|
334
390
|
// Look for session resumption extensions
|
|
335
391
|
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
336
392
|
pos += 2;
|
|
337
|
-
|
|
393
|
+
|
|
338
394
|
// Extensions end position
|
|
339
395
|
const extensionsEnd = pos + extensionsLength;
|
|
340
396
|
if (extensionsEnd > buffer.length) return { isResumption: false, hasSNI: false };
|
|
341
|
-
|
|
397
|
+
|
|
342
398
|
// Track resumption indicators
|
|
343
399
|
let hasSessionTicket = false;
|
|
344
400
|
let hasPSK = false;
|
|
345
401
|
let hasEarlyData = false;
|
|
346
|
-
|
|
402
|
+
|
|
347
403
|
// Iterate through extensions
|
|
348
404
|
while (pos + 4 <= extensionsEnd) {
|
|
349
405
|
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
350
406
|
pos += 2;
|
|
351
|
-
|
|
407
|
+
|
|
352
408
|
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
353
409
|
pos += 2;
|
|
354
|
-
|
|
410
|
+
|
|
355
411
|
if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
|
|
356
412
|
log('Found session ticket extension');
|
|
357
413
|
hasSessionTicket = true;
|
|
358
|
-
|
|
414
|
+
|
|
359
415
|
// Check if session ticket has non-zero length (active ticket)
|
|
360
416
|
if (extensionLength > 0) {
|
|
361
417
|
log(`Session ticket has length ${extensionLength} - active ticket present`);
|
|
@@ -367,37 +423,37 @@ export class SniHandler {
|
|
|
367
423
|
log('Found Early Data extension (TLS 1.3 0-RTT)');
|
|
368
424
|
hasEarlyData = true;
|
|
369
425
|
}
|
|
370
|
-
|
|
426
|
+
|
|
371
427
|
// Skip extension data
|
|
372
428
|
pos += extensionLength;
|
|
373
429
|
}
|
|
374
|
-
|
|
430
|
+
|
|
375
431
|
// Check if SNI is included
|
|
376
432
|
let hasSNI = false;
|
|
377
|
-
|
|
433
|
+
|
|
378
434
|
// Reset position and scan again for SNI extension
|
|
379
435
|
pos = 5 + 1 + 3 + 2; // Reset to after handshake type, length and client version
|
|
380
436
|
pos += 32; // Skip client random
|
|
381
|
-
|
|
437
|
+
|
|
382
438
|
if (pos + 1 <= buffer.length) {
|
|
383
439
|
const sessionIdLength = buffer[pos];
|
|
384
440
|
pos += 1 + sessionIdLength;
|
|
385
|
-
|
|
441
|
+
|
|
386
442
|
// Skip cipher suites
|
|
387
443
|
if (pos + 2 <= buffer.length) {
|
|
388
444
|
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
389
445
|
pos += 2 + cipherSuitesLength;
|
|
390
|
-
|
|
446
|
+
|
|
391
447
|
// Skip compression methods
|
|
392
448
|
if (pos + 1 <= buffer.length) {
|
|
393
449
|
const compressionMethodsLength = buffer[pos];
|
|
394
450
|
pos += 1 + compressionMethodsLength;
|
|
395
|
-
|
|
451
|
+
|
|
396
452
|
// Check for extensions
|
|
397
453
|
if (pos + 2 <= buffer.length) {
|
|
398
454
|
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
399
455
|
pos += 2;
|
|
400
|
-
|
|
456
|
+
|
|
401
457
|
// Extensions end position
|
|
402
458
|
const extensionsEnd = pos + extensionsLength;
|
|
403
459
|
if (extensionsEnd <= buffer.length) {
|
|
@@ -405,22 +461,22 @@ export class SniHandler {
|
|
|
405
461
|
while (pos + 4 <= extensionsEnd) {
|
|
406
462
|
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
407
463
|
pos += 2;
|
|
408
|
-
|
|
464
|
+
|
|
409
465
|
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
410
466
|
pos += 2;
|
|
411
|
-
|
|
467
|
+
|
|
412
468
|
if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
|
|
413
469
|
// Check that the SNI extension actually has content
|
|
414
470
|
if (extensionLength > 0) {
|
|
415
471
|
hasSNI = true;
|
|
416
|
-
|
|
472
|
+
|
|
417
473
|
// Try to extract the actual SNI value for logging
|
|
418
474
|
try {
|
|
419
475
|
// Skip to server_name_list_length (2 bytes)
|
|
420
476
|
const tempPos = pos;
|
|
421
477
|
if (tempPos + 2 <= extensionsEnd) {
|
|
422
478
|
const nameListLength = (buffer[tempPos] << 8) + buffer[tempPos + 1];
|
|
423
|
-
|
|
479
|
+
|
|
424
480
|
// Skip server_name_list_length (2 bytes)
|
|
425
481
|
if (tempPos + 2 + 1 <= extensionsEnd) {
|
|
426
482
|
// Check name_type (should be 0 for hostname)
|
|
@@ -429,10 +485,12 @@ export class SniHandler {
|
|
|
429
485
|
if (tempPos + 3 + 2 <= extensionsEnd) {
|
|
430
486
|
// Get name_length (2 bytes)
|
|
431
487
|
const nameLength = (buffer[tempPos + 3] << 8) + buffer[tempPos + 4];
|
|
432
|
-
|
|
488
|
+
|
|
433
489
|
// Extract the hostname
|
|
434
490
|
if (tempPos + 5 + nameLength <= extensionsEnd) {
|
|
435
|
-
const hostname = buffer
|
|
491
|
+
const hostname = buffer
|
|
492
|
+
.slice(tempPos + 5, tempPos + 5 + nameLength)
|
|
493
|
+
.toString('utf8');
|
|
436
494
|
log(`Found SNI extension with server_name: ${hostname}`);
|
|
437
495
|
}
|
|
438
496
|
}
|
|
@@ -448,7 +506,7 @@ export class SniHandler {
|
|
|
448
506
|
}
|
|
449
507
|
break;
|
|
450
508
|
}
|
|
451
|
-
|
|
509
|
+
|
|
452
510
|
// Skip extension data
|
|
453
511
|
pos += extensionLength;
|
|
454
512
|
}
|
|
@@ -457,50 +515,54 @@ export class SniHandler {
|
|
|
457
515
|
}
|
|
458
516
|
}
|
|
459
517
|
}
|
|
460
|
-
|
|
518
|
+
|
|
461
519
|
// Consider it a resumption if any resumption mechanism is present
|
|
462
|
-
const isResumption =
|
|
463
|
-
|
|
464
|
-
|
|
520
|
+
const isResumption =
|
|
521
|
+
hasSessionTicket || hasPSK || hasEarlyData || (hasNonEmptySessionId && !hasPSK); // Legacy resumption
|
|
522
|
+
|
|
465
523
|
if (isResumption) {
|
|
466
|
-
log(
|
|
524
|
+
log(
|
|
525
|
+
'Session resumption detected: ' +
|
|
467
526
|
(hasSessionTicket ? 'session ticket, ' : '') +
|
|
468
527
|
(hasPSK ? 'PSK, ' : '') +
|
|
469
528
|
(hasEarlyData ? 'early data, ' : '') +
|
|
470
529
|
(hasNonEmptySessionId ? 'session ID' : '') +
|
|
471
|
-
(hasSNI ? ', with SNI' : ', without SNI')
|
|
530
|
+
(hasSNI ? ', with SNI' : ', without SNI')
|
|
531
|
+
);
|
|
472
532
|
}
|
|
473
|
-
|
|
533
|
+
|
|
474
534
|
// Return an object with both flags
|
|
475
535
|
// For clarity: connections should be blocked if they have session resumption without SNI
|
|
476
536
|
if (isResumption) {
|
|
477
|
-
log(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
537
|
+
log(
|
|
538
|
+
`Resumption summary - hasSNI: ${hasSNI ? 'yes' : 'no'}, resumption type: ${
|
|
539
|
+
hasSessionTicket ? 'session ticket, ' : ''
|
|
540
|
+
}${hasPSK ? 'PSK, ' : ''}${hasEarlyData ? 'early data, ' : ''}${
|
|
541
|
+
hasNonEmptySessionId ? 'session ID' : ''
|
|
542
|
+
}`
|
|
543
|
+
);
|
|
482
544
|
}
|
|
483
|
-
|
|
484
|
-
return {
|
|
485
|
-
isResumption,
|
|
486
|
-
hasSNI
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
isResumption,
|
|
548
|
+
hasSNI,
|
|
487
549
|
};
|
|
488
550
|
} catch (error) {
|
|
489
551
|
log(`Error checking for session resumption: ${error}`);
|
|
490
552
|
return { isResumption: false, hasSNI: false };
|
|
491
553
|
}
|
|
492
554
|
}
|
|
493
|
-
|
|
555
|
+
|
|
494
556
|
/**
|
|
495
557
|
* Detects characteristics of a tab reactivation TLS handshake
|
|
496
558
|
* These often have specific patterns in Chrome and other browsers
|
|
497
|
-
*
|
|
559
|
+
*
|
|
498
560
|
* @param buffer - The buffer containing a ClientHello message
|
|
499
561
|
* @param enableLogging - Whether to enable logging
|
|
500
562
|
* @returns true if this appears to be a tab reactivation handshake
|
|
501
563
|
*/
|
|
502
564
|
public static isTabReactivationHandshake(
|
|
503
|
-
buffer: Buffer,
|
|
565
|
+
buffer: Buffer,
|
|
504
566
|
enableLogging: boolean = false
|
|
505
567
|
): boolean {
|
|
506
568
|
const log = (message: string) => {
|
|
@@ -508,61 +570,61 @@ export class SniHandler {
|
|
|
508
570
|
console.log(`[Tab Reactivation] ${message}`);
|
|
509
571
|
}
|
|
510
572
|
};
|
|
511
|
-
|
|
573
|
+
|
|
512
574
|
if (!this.isClientHello(buffer)) {
|
|
513
575
|
return false;
|
|
514
576
|
}
|
|
515
|
-
|
|
577
|
+
|
|
516
578
|
try {
|
|
517
579
|
// Check for session ID presence (tab reactivation often has a session ID)
|
|
518
580
|
let pos = 5 + 1 + 3 + 2; // Position after handshake type, length and client version
|
|
519
581
|
pos += 32; // Skip client random
|
|
520
|
-
|
|
582
|
+
|
|
521
583
|
if (pos + 1 > buffer.length) return false;
|
|
522
|
-
|
|
584
|
+
|
|
523
585
|
const sessionIdLength = buffer[pos];
|
|
524
|
-
|
|
586
|
+
|
|
525
587
|
// Non-empty session ID is a good indicator
|
|
526
588
|
if (sessionIdLength > 0) {
|
|
527
589
|
log(`Detected non-empty session ID (length: ${sessionIdLength})`);
|
|
528
|
-
|
|
590
|
+
|
|
529
591
|
// Skip to extensions
|
|
530
592
|
pos += 1 + sessionIdLength;
|
|
531
|
-
|
|
593
|
+
|
|
532
594
|
// Skip cipher suites
|
|
533
595
|
if (pos + 2 > buffer.length) return false;
|
|
534
596
|
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
535
597
|
pos += 2 + cipherSuitesLength;
|
|
536
|
-
|
|
598
|
+
|
|
537
599
|
// Skip compression methods
|
|
538
600
|
if (pos + 1 > buffer.length) return false;
|
|
539
601
|
const compressionMethodsLength = buffer[pos];
|
|
540
602
|
pos += 1 + compressionMethodsLength;
|
|
541
|
-
|
|
603
|
+
|
|
542
604
|
// Check for extensions
|
|
543
605
|
if (pos + 2 > buffer.length) return false;
|
|
544
|
-
|
|
606
|
+
|
|
545
607
|
// Look for specific extensions that indicate tab reactivation
|
|
546
608
|
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
547
609
|
pos += 2;
|
|
548
|
-
|
|
610
|
+
|
|
549
611
|
// Extensions end position
|
|
550
612
|
const extensionsEnd = pos + extensionsLength;
|
|
551
613
|
if (extensionsEnd > buffer.length) return false;
|
|
552
|
-
|
|
614
|
+
|
|
553
615
|
// Tab reactivation often has session tickets but no SNI
|
|
554
616
|
let hasSessionTicket = false;
|
|
555
617
|
let hasSNI = false;
|
|
556
618
|
let hasPSK = false;
|
|
557
|
-
|
|
619
|
+
|
|
558
620
|
// Iterate through extensions
|
|
559
621
|
while (pos + 4 <= extensionsEnd) {
|
|
560
622
|
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
561
623
|
pos += 2;
|
|
562
|
-
|
|
624
|
+
|
|
563
625
|
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
564
626
|
pos += 2;
|
|
565
|
-
|
|
627
|
+
|
|
566
628
|
if (extensionType === this.TLS_SESSION_TICKET_EXTENSION_TYPE) {
|
|
567
629
|
hasSessionTicket = true;
|
|
568
630
|
} else if (extensionType === this.TLS_SNI_EXTENSION_TYPE) {
|
|
@@ -570,11 +632,11 @@ export class SniHandler {
|
|
|
570
632
|
} else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
|
|
571
633
|
hasPSK = true;
|
|
572
634
|
}
|
|
573
|
-
|
|
635
|
+
|
|
574
636
|
// Skip extension data
|
|
575
637
|
pos += extensionLength;
|
|
576
638
|
}
|
|
577
|
-
|
|
639
|
+
|
|
578
640
|
// Pattern for tab reactivation: session identifier + (ticket or PSK) but no SNI
|
|
579
641
|
if ((hasSessionTicket || hasPSK) && !hasSNI) {
|
|
580
642
|
log('Detected tab reactivation pattern: session resumption without SNI');
|
|
@@ -584,14 +646,14 @@ export class SniHandler {
|
|
|
584
646
|
} catch (error) {
|
|
585
647
|
log(`Error checking for tab reactivation: ${error}`);
|
|
586
648
|
}
|
|
587
|
-
|
|
649
|
+
|
|
588
650
|
return false;
|
|
589
651
|
}
|
|
590
652
|
|
|
591
653
|
/**
|
|
592
654
|
* Extracts the SNI (Server Name Indication) from a TLS ClientHello message.
|
|
593
655
|
* Implements robust parsing with support for session resumption edge cases.
|
|
594
|
-
*
|
|
656
|
+
*
|
|
595
657
|
* @param buffer - The buffer containing the TLS ClientHello message
|
|
596
658
|
* @param enableLogging - Whether to enable detailed debug logging
|
|
597
659
|
* @returns The extracted server name or undefined if not found
|
|
@@ -849,10 +911,10 @@ export class SniHandler {
|
|
|
849
911
|
|
|
850
912
|
/**
|
|
851
913
|
* Attempts to extract SNI from the PSK extension in a TLS 1.3 ClientHello.
|
|
852
|
-
*
|
|
853
|
-
* In TLS 1.3, when a client attempts to resume a session, it may include
|
|
914
|
+
*
|
|
915
|
+
* In TLS 1.3, when a client attempts to resume a session, it may include
|
|
854
916
|
* the server name in the PSK identity hint rather than in the SNI extension.
|
|
855
|
-
*
|
|
917
|
+
*
|
|
856
918
|
* @param buffer - The buffer containing the TLS ClientHello message
|
|
857
919
|
* @param enableLogging - Whether to enable detailed debug logging
|
|
858
920
|
* @returns The extracted server name or undefined if not found
|
|
@@ -876,44 +938,44 @@ export class SniHandler {
|
|
|
876
938
|
|
|
877
939
|
// Find the start position of extensions
|
|
878
940
|
let pos = 5; // Start after record header
|
|
879
|
-
|
|
941
|
+
|
|
880
942
|
// Skip handshake type (1 byte)
|
|
881
943
|
pos += 1;
|
|
882
|
-
|
|
944
|
+
|
|
883
945
|
// Skip handshake length (3 bytes)
|
|
884
946
|
pos += 3;
|
|
885
|
-
|
|
947
|
+
|
|
886
948
|
// Skip client version (2 bytes)
|
|
887
949
|
pos += 2;
|
|
888
|
-
|
|
950
|
+
|
|
889
951
|
// Skip client random (32 bytes)
|
|
890
952
|
pos += 32;
|
|
891
|
-
|
|
953
|
+
|
|
892
954
|
// Skip session ID
|
|
893
955
|
if (pos + 1 > buffer.length) return undefined;
|
|
894
956
|
const sessionIdLength = buffer[pos];
|
|
895
957
|
pos += 1 + sessionIdLength;
|
|
896
|
-
|
|
958
|
+
|
|
897
959
|
// Skip cipher suites
|
|
898
960
|
if (pos + 2 > buffer.length) return undefined;
|
|
899
961
|
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
900
962
|
pos += 2 + cipherSuitesLength;
|
|
901
|
-
|
|
963
|
+
|
|
902
964
|
// Skip compression methods
|
|
903
965
|
if (pos + 1 > buffer.length) return undefined;
|
|
904
966
|
const compressionMethodsLength = buffer[pos];
|
|
905
967
|
pos += 1 + compressionMethodsLength;
|
|
906
|
-
|
|
968
|
+
|
|
907
969
|
// Check if we have extensions
|
|
908
970
|
if (pos + 2 > buffer.length) {
|
|
909
971
|
log('No extensions present');
|
|
910
972
|
return undefined;
|
|
911
973
|
}
|
|
912
|
-
|
|
974
|
+
|
|
913
975
|
// Get extensions length
|
|
914
976
|
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
915
977
|
pos += 2;
|
|
916
|
-
|
|
978
|
+
|
|
917
979
|
// Extensions end position
|
|
918
980
|
const extensionsEnd = pos + extensionsLength;
|
|
919
981
|
if (extensionsEnd > buffer.length) return undefined;
|
|
@@ -922,65 +984,66 @@ export class SniHandler {
|
|
|
922
984
|
while (pos + 4 <= extensionsEnd) {
|
|
923
985
|
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
924
986
|
pos += 2;
|
|
925
|
-
|
|
987
|
+
|
|
926
988
|
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
927
989
|
pos += 2;
|
|
928
|
-
|
|
990
|
+
|
|
929
991
|
if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
|
|
930
992
|
log('Found PSK extension');
|
|
931
|
-
|
|
993
|
+
|
|
932
994
|
// PSK extension structure:
|
|
933
995
|
// 2 bytes: identities list length
|
|
934
996
|
if (pos + 2 > extensionsEnd) break;
|
|
935
997
|
const identitiesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
936
998
|
pos += 2;
|
|
937
|
-
|
|
999
|
+
|
|
938
1000
|
// End of identities list
|
|
939
1001
|
const identitiesEnd = pos + identitiesLength;
|
|
940
1002
|
if (identitiesEnd > extensionsEnd) break;
|
|
941
|
-
|
|
1003
|
+
|
|
942
1004
|
// Process each PSK identity
|
|
943
1005
|
while (pos + 2 <= identitiesEnd) {
|
|
944
1006
|
// Identity length (2 bytes)
|
|
945
1007
|
if (pos + 2 > identitiesEnd) break;
|
|
946
1008
|
const identityLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
947
1009
|
pos += 2;
|
|
948
|
-
|
|
1010
|
+
|
|
949
1011
|
if (pos + identityLength > identitiesEnd) break;
|
|
950
|
-
|
|
1012
|
+
|
|
951
1013
|
// Try to extract hostname from identity
|
|
952
1014
|
// Chrome often embeds the hostname in the PSK identity
|
|
953
1015
|
// This is a heuristic as there's no standard format
|
|
954
1016
|
if (identityLength > 0) {
|
|
955
1017
|
const identity = buffer.slice(pos, pos + identityLength);
|
|
956
|
-
|
|
1018
|
+
|
|
957
1019
|
// Skip identity bytes
|
|
958
1020
|
pos += identityLength;
|
|
959
|
-
|
|
1021
|
+
|
|
960
1022
|
// Skip obfuscated ticket age (4 bytes)
|
|
961
1023
|
if (pos + 4 <= identitiesEnd) {
|
|
962
1024
|
pos += 4;
|
|
963
1025
|
} else {
|
|
964
1026
|
break;
|
|
965
1027
|
}
|
|
966
|
-
|
|
1028
|
+
|
|
967
1029
|
// Try to parse the identity as UTF-8
|
|
968
1030
|
try {
|
|
969
1031
|
const identityStr = identity.toString('utf8');
|
|
970
1032
|
log(`PSK identity: ${identityStr}`);
|
|
971
|
-
|
|
1033
|
+
|
|
972
1034
|
// Check if the identity contains hostname hints
|
|
973
1035
|
// Chrome often embeds the hostname in a known format
|
|
974
1036
|
// Try to extract using common patterns
|
|
975
|
-
|
|
1037
|
+
|
|
976
1038
|
// Pattern 1: Look for domain name pattern
|
|
977
|
-
const domainPattern =
|
|
1039
|
+
const domainPattern =
|
|
1040
|
+
/([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/i;
|
|
978
1041
|
const domainMatch = identityStr.match(domainPattern);
|
|
979
1042
|
if (domainMatch && domainMatch[0]) {
|
|
980
1043
|
log(`Found domain in PSK identity: ${domainMatch[0]}`);
|
|
981
1044
|
return domainMatch[0];
|
|
982
1045
|
}
|
|
983
|
-
|
|
1046
|
+
|
|
984
1047
|
// Pattern 2: Chrome sometimes uses a specific format with delimiters
|
|
985
1048
|
// This is a heuristic approach since the format isn't standardized
|
|
986
1049
|
const parts = identityStr.split('|');
|
|
@@ -1005,7 +1068,7 @@ export class SniHandler {
|
|
|
1005
1068
|
pos += extensionLength;
|
|
1006
1069
|
}
|
|
1007
1070
|
}
|
|
1008
|
-
|
|
1071
|
+
|
|
1009
1072
|
log('No hostname found in PSK extension');
|
|
1010
1073
|
return undefined;
|
|
1011
1074
|
} catch (error) {
|
|
@@ -1020,91 +1083,88 @@ export class SniHandler {
|
|
|
1020
1083
|
* @param enableLogging - Whether to enable logging
|
|
1021
1084
|
* @returns true if early data is detected
|
|
1022
1085
|
*/
|
|
1023
|
-
public static hasEarlyData(
|
|
1024
|
-
buffer: Buffer,
|
|
1025
|
-
enableLogging: boolean = false
|
|
1026
|
-
): boolean {
|
|
1086
|
+
public static hasEarlyData(buffer: Buffer, enableLogging: boolean = false): boolean {
|
|
1027
1087
|
const log = (message: string) => {
|
|
1028
1088
|
if (enableLogging) {
|
|
1029
1089
|
console.log(`[Early Data] ${message}`);
|
|
1030
1090
|
}
|
|
1031
1091
|
};
|
|
1032
|
-
|
|
1092
|
+
|
|
1033
1093
|
try {
|
|
1034
1094
|
// Check if this is a valid ClientHello first
|
|
1035
1095
|
if (!this.isClientHello(buffer)) {
|
|
1036
1096
|
return false;
|
|
1037
1097
|
}
|
|
1038
|
-
|
|
1098
|
+
|
|
1039
1099
|
// Find the extensions section
|
|
1040
1100
|
let pos = 5; // Start after record header
|
|
1041
|
-
|
|
1101
|
+
|
|
1042
1102
|
// Skip handshake type (1 byte)
|
|
1043
1103
|
pos += 1;
|
|
1044
|
-
|
|
1104
|
+
|
|
1045
1105
|
// Skip handshake length (3 bytes)
|
|
1046
1106
|
pos += 3;
|
|
1047
|
-
|
|
1107
|
+
|
|
1048
1108
|
// Skip client version (2 bytes)
|
|
1049
1109
|
pos += 2;
|
|
1050
|
-
|
|
1110
|
+
|
|
1051
1111
|
// Skip client random (32 bytes)
|
|
1052
1112
|
pos += 32;
|
|
1053
|
-
|
|
1113
|
+
|
|
1054
1114
|
// Skip session ID
|
|
1055
1115
|
if (pos + 1 > buffer.length) return false;
|
|
1056
1116
|
const sessionIdLength = buffer[pos];
|
|
1057
1117
|
pos += 1 + sessionIdLength;
|
|
1058
|
-
|
|
1118
|
+
|
|
1059
1119
|
// Skip cipher suites
|
|
1060
1120
|
if (pos + 2 > buffer.length) return false;
|
|
1061
1121
|
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
1062
1122
|
pos += 2 + cipherSuitesLength;
|
|
1063
|
-
|
|
1123
|
+
|
|
1064
1124
|
// Skip compression methods
|
|
1065
1125
|
if (pos + 1 > buffer.length) return false;
|
|
1066
1126
|
const compressionMethodsLength = buffer[pos];
|
|
1067
1127
|
pos += 1 + compressionMethodsLength;
|
|
1068
|
-
|
|
1128
|
+
|
|
1069
1129
|
// Check if we have extensions
|
|
1070
1130
|
if (pos + 2 > buffer.length) return false;
|
|
1071
|
-
|
|
1131
|
+
|
|
1072
1132
|
// Get extensions length
|
|
1073
1133
|
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
1074
1134
|
pos += 2;
|
|
1075
|
-
|
|
1135
|
+
|
|
1076
1136
|
// Extensions end position
|
|
1077
1137
|
const extensionsEnd = pos + extensionsLength;
|
|
1078
1138
|
if (extensionsEnd > buffer.length) return false;
|
|
1079
|
-
|
|
1139
|
+
|
|
1080
1140
|
// Look for early data extension
|
|
1081
1141
|
while (pos + 4 <= extensionsEnd) {
|
|
1082
1142
|
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
1083
1143
|
pos += 2;
|
|
1084
|
-
|
|
1144
|
+
|
|
1085
1145
|
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
1086
1146
|
pos += 2;
|
|
1087
|
-
|
|
1147
|
+
|
|
1088
1148
|
if (extensionType === this.TLS_EARLY_DATA_EXTENSION_TYPE) {
|
|
1089
1149
|
log('Early Data (0-RTT) extension detected');
|
|
1090
1150
|
return true;
|
|
1091
1151
|
}
|
|
1092
|
-
|
|
1152
|
+
|
|
1093
1153
|
// Skip to next extension
|
|
1094
1154
|
pos += extensionLength;
|
|
1095
1155
|
}
|
|
1096
|
-
|
|
1156
|
+
|
|
1097
1157
|
return false;
|
|
1098
1158
|
} catch (error) {
|
|
1099
1159
|
log(`Error checking for early data: ${error}`);
|
|
1100
1160
|
return false;
|
|
1101
1161
|
}
|
|
1102
1162
|
}
|
|
1103
|
-
|
|
1163
|
+
|
|
1104
1164
|
/**
|
|
1105
1165
|
* Attempts to extract SNI from an initial ClientHello packet and handles
|
|
1106
1166
|
* session resumption edge cases more robustly than the standard extraction.
|
|
1107
|
-
*
|
|
1167
|
+
*
|
|
1108
1168
|
* This method handles:
|
|
1109
1169
|
* 1. Standard SNI extraction
|
|
1110
1170
|
* 2. TLS 1.3 PSK-based resumption (Chrome, Firefox, etc.)
|
|
@@ -1113,7 +1173,7 @@ export class SniHandler {
|
|
|
1113
1173
|
* 5. TLS 1.3 Early Data (0-RTT)
|
|
1114
1174
|
* 6. Chrome's connection racing behaviors
|
|
1115
1175
|
* 7. Tab reactivation patterns with session cache
|
|
1116
|
-
*
|
|
1176
|
+
*
|
|
1117
1177
|
* @param buffer - The buffer containing the TLS ClientHello message
|
|
1118
1178
|
* @param connectionInfo - Optional connection information for fragment handling
|
|
1119
1179
|
* @param enableLogging - Whether to enable detailed debug logging
|
|
@@ -1121,11 +1181,11 @@ export class SniHandler {
|
|
|
1121
1181
|
*/
|
|
1122
1182
|
public static extractSNIWithResumptionSupport(
|
|
1123
1183
|
buffer: Buffer,
|
|
1124
|
-
connectionInfo?: {
|
|
1125
|
-
sourceIp?: string;
|
|
1126
|
-
sourcePort?: number;
|
|
1127
|
-
destIp?: string;
|
|
1128
|
-
destPort?: number;
|
|
1184
|
+
connectionInfo?: {
|
|
1185
|
+
sourceIp?: string;
|
|
1186
|
+
sourcePort?: number;
|
|
1187
|
+
destIp?: string;
|
|
1188
|
+
destPort?: number;
|
|
1129
1189
|
},
|
|
1130
1190
|
enableLogging: boolean = false
|
|
1131
1191
|
): string | undefined {
|
|
@@ -1134,111 +1194,127 @@ export class SniHandler {
|
|
|
1134
1194
|
console.log(`[SNI Extraction] ${message}`);
|
|
1135
1195
|
}
|
|
1136
1196
|
};
|
|
1137
|
-
|
|
1197
|
+
|
|
1198
|
+
// Log buffer details for debugging
|
|
1199
|
+
if (enableLogging) {
|
|
1200
|
+
log(`Buffer size: ${buffer.length} bytes`);
|
|
1201
|
+
log(`Buffer starts with: ${buffer.slice(0, Math.min(10, buffer.length)).toString('hex')}`);
|
|
1202
|
+
|
|
1203
|
+
if (buffer.length >= 5) {
|
|
1204
|
+
const recordType = buffer[0];
|
|
1205
|
+
const majorVersion = buffer[1];
|
|
1206
|
+
const minorVersion = buffer[2];
|
|
1207
|
+
const recordLength = (buffer[3] << 8) + buffer[4];
|
|
1208
|
+
|
|
1209
|
+
log(
|
|
1210
|
+
`TLS Record: type=${recordType}, version=${majorVersion}.${minorVersion}, length=${recordLength}`
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1138
1215
|
// Check if we need to handle fragmented packets
|
|
1139
1216
|
let processBuffer = buffer;
|
|
1140
1217
|
if (connectionInfo) {
|
|
1141
1218
|
const connectionId = this.createConnectionId(connectionInfo);
|
|
1142
1219
|
const reassembledBuffer = this.handleFragmentedClientHello(
|
|
1143
|
-
buffer,
|
|
1144
|
-
connectionId,
|
|
1220
|
+
buffer,
|
|
1221
|
+
connectionId,
|
|
1145
1222
|
enableLogging
|
|
1146
1223
|
);
|
|
1147
|
-
|
|
1224
|
+
|
|
1148
1225
|
if (!reassembledBuffer) {
|
|
1149
1226
|
log(`Waiting for more fragments on connection ${connectionId}`);
|
|
1150
1227
|
return undefined; // Need more fragments to complete ClientHello
|
|
1151
1228
|
}
|
|
1152
|
-
|
|
1229
|
+
|
|
1153
1230
|
processBuffer = reassembledBuffer;
|
|
1154
1231
|
log(`Using reassembled buffer of length ${processBuffer.length}`);
|
|
1155
1232
|
}
|
|
1156
|
-
|
|
1233
|
+
|
|
1157
1234
|
// First try the standard SNI extraction
|
|
1158
1235
|
const standardSni = this.extractSNI(processBuffer, enableLogging);
|
|
1159
1236
|
if (standardSni) {
|
|
1160
1237
|
log(`Found standard SNI: ${standardSni}`);
|
|
1161
|
-
|
|
1238
|
+
|
|
1162
1239
|
// If we extracted a standard SNI, cache it for future use
|
|
1163
1240
|
if (connectionInfo?.sourceIp) {
|
|
1164
1241
|
const clientRandom = this.extractClientRandom(processBuffer);
|
|
1165
1242
|
this.cacheSession(connectionInfo.sourceIp, standardSni, clientRandom);
|
|
1166
1243
|
log(`Cached SNI for future reference: ${standardSni}`);
|
|
1167
1244
|
}
|
|
1168
|
-
|
|
1245
|
+
|
|
1169
1246
|
return standardSni;
|
|
1170
1247
|
}
|
|
1171
|
-
|
|
1172
|
-
// Check for
|
|
1173
|
-
|
|
1174
|
-
if (isTabReactivation && connectionInfo?.sourceIp) {
|
|
1175
|
-
// Try to get the SNI from our session cache for tab reactivation
|
|
1176
|
-
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
1177
|
-
if (cachedSni) {
|
|
1178
|
-
log(`Retrieved cached SNI for tab reactivation: ${cachedSni}`);
|
|
1179
|
-
return cachedSni;
|
|
1180
|
-
}
|
|
1181
|
-
log('Tab reactivation detected but no cached SNI found');
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
// Check for TLS 1.3 early data (0-RTT)
|
|
1185
|
-
const hasEarly = this.hasEarlyData(processBuffer, enableLogging);
|
|
1186
|
-
if (hasEarly) {
|
|
1187
|
-
log('TLS 1.3 Early Data detected, trying session cache');
|
|
1188
|
-
// For 0-RTT, check the session cache
|
|
1189
|
-
if (connectionInfo?.sourceIp) {
|
|
1190
|
-
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
1191
|
-
if (cachedSni) {
|
|
1192
|
-
log(`Retrieved cached SNI for 0-RTT: ${cachedSni}`);
|
|
1193
|
-
return cachedSni;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// If standard extraction failed and we have a valid ClientHello,
|
|
1199
|
-
// this might be a session resumption with non-standard format
|
|
1248
|
+
|
|
1249
|
+
// Check for session resumption when standard SNI extraction fails
|
|
1250
|
+
// This may help in chained proxy scenarios
|
|
1200
1251
|
if (this.isClientHello(processBuffer)) {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1252
|
+
const resumptionInfo = this.hasSessionResumption(processBuffer, enableLogging);
|
|
1253
|
+
|
|
1254
|
+
if (resumptionInfo.isResumption) {
|
|
1255
|
+
log(`Detected session resumption in ClientHello without standard SNI`);
|
|
1256
|
+
|
|
1257
|
+
// Try to extract SNI from PSK extension
|
|
1258
|
+
const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
|
|
1259
|
+
if (pskSni) {
|
|
1260
|
+
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
|
1261
|
+
|
|
1262
|
+
// Cache this SNI
|
|
1263
|
+
if (connectionInfo?.sourceIp) {
|
|
1264
|
+
const clientRandom = this.extractClientRandom(processBuffer);
|
|
1265
|
+
this.cacheSession(connectionInfo.sourceIp, pskSni, clientRandom);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
return pskSni;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// If session resumption has SNI in a non-standard location,
|
|
1272
|
+
// we need to apply heuristics
|
|
1209
1273
|
if (connectionInfo?.sourceIp) {
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1274
|
+
const cachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
1275
|
+
if (cachedSni) {
|
|
1276
|
+
log(`Using cached SNI for session resumption: ${cachedSni}`);
|
|
1277
|
+
return cachedSni;
|
|
1278
|
+
}
|
|
1213
1279
|
}
|
|
1214
|
-
|
|
1215
|
-
return pskSni;
|
|
1216
1280
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Try tab reactivation and other recovery methods...
|
|
1284
|
+
// (existing code remains unchanged)
|
|
1285
|
+
|
|
1286
|
+
// Log detailed info about the ClientHello when SNI extraction fails
|
|
1287
|
+
if (this.isClientHello(processBuffer) && enableLogging) {
|
|
1288
|
+
log(`SNI extraction failed for ClientHello. Buffer details:`);
|
|
1289
|
+
|
|
1290
|
+
if (processBuffer.length >= 43) {
|
|
1291
|
+
// ClientHello with at least client random
|
|
1292
|
+
const clientRandom = processBuffer.slice(11, 11 + 32).toString('hex');
|
|
1293
|
+
log(`Client Random: ${clientRandom}`);
|
|
1294
|
+
|
|
1295
|
+
// Log session ID length and presence
|
|
1296
|
+
const sessionIdLength = processBuffer[43];
|
|
1297
|
+
log(`Session ID length: ${sessionIdLength}`);
|
|
1298
|
+
|
|
1299
|
+
if (sessionIdLength > 0 && processBuffer.length >= 44 + sessionIdLength) {
|
|
1300
|
+
const sessionId = processBuffer.slice(44, 44 + sessionIdLength).toString('hex');
|
|
1301
|
+
log(`Session ID: ${sessionId}`);
|
|
1225
1302
|
}
|
|
1226
1303
|
}
|
|
1227
|
-
|
|
1228
|
-
log('Failed to extract SNI from resumption mechanisms');
|
|
1229
1304
|
}
|
|
1230
|
-
|
|
1305
|
+
|
|
1306
|
+
// Existing code for fallback methods continues...
|
|
1231
1307
|
return undefined;
|
|
1232
1308
|
}
|
|
1233
|
-
|
|
1309
|
+
|
|
1234
1310
|
/**
|
|
1235
1311
|
* Main entry point for SNI extraction that handles all edge cases.
|
|
1236
1312
|
* This should be called for each TLS packet received from a client.
|
|
1237
|
-
*
|
|
1313
|
+
*
|
|
1238
1314
|
* The method uses connection tracking to handle fragmented ClientHello
|
|
1239
1315
|
* messages and various TLS 1.3 behaviors, including Chrome's connection
|
|
1240
1316
|
* racing patterns and tab reactivation behaviors.
|
|
1241
|
-
*
|
|
1317
|
+
*
|
|
1242
1318
|
* @param buffer - The buffer containing TLS data
|
|
1243
1319
|
* @param connectionInfo - Connection metadata (IPs and ports)
|
|
1244
1320
|
* @param enableLogging - Whether to enable detailed debug logging
|
|
@@ -1262,22 +1338,22 @@ export class SniHandler {
|
|
|
1262
1338
|
console.log(`[TLS Packet] ${message}`);
|
|
1263
1339
|
}
|
|
1264
1340
|
};
|
|
1265
|
-
|
|
1341
|
+
|
|
1266
1342
|
// Add timestamp if not provided
|
|
1267
1343
|
if (!connectionInfo.timestamp) {
|
|
1268
1344
|
connectionInfo.timestamp = Date.now();
|
|
1269
1345
|
}
|
|
1270
|
-
|
|
1346
|
+
|
|
1271
1347
|
// Check if this is a TLS handshake or application data
|
|
1272
1348
|
if (!this.isTlsHandshake(buffer) && !this.isTlsApplicationData(buffer)) {
|
|
1273
1349
|
log('Not a TLS handshake or application data packet');
|
|
1274
1350
|
return undefined;
|
|
1275
1351
|
}
|
|
1276
|
-
|
|
1352
|
+
|
|
1277
1353
|
// Create connection ID for tracking
|
|
1278
1354
|
const connectionId = this.createConnectionId(connectionInfo);
|
|
1279
1355
|
log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`);
|
|
1280
|
-
|
|
1356
|
+
|
|
1281
1357
|
// Handle application data with cached SNI (for connection racing)
|
|
1282
1358
|
if (this.isTlsApplicationData(buffer)) {
|
|
1283
1359
|
// First check if explicit cachedSni was provided
|
|
@@ -1285,30 +1361,26 @@ export class SniHandler {
|
|
|
1285
1361
|
log(`Using provided cached SNI for application data: ${cachedSni}`);
|
|
1286
1362
|
return cachedSni;
|
|
1287
1363
|
}
|
|
1288
|
-
|
|
1364
|
+
|
|
1289
1365
|
// Otherwise check our session cache
|
|
1290
1366
|
const sessionCachedSni = this.getCachedSession(connectionInfo.sourceIp);
|
|
1291
1367
|
if (sessionCachedSni) {
|
|
1292
1368
|
log(`Using session-cached SNI for application data: ${sessionCachedSni}`);
|
|
1293
1369
|
return sessionCachedSni;
|
|
1294
1370
|
}
|
|
1295
|
-
|
|
1371
|
+
|
|
1296
1372
|
log('Application data packet without cached SNI, cannot determine hostname');
|
|
1297
1373
|
return undefined;
|
|
1298
1374
|
}
|
|
1299
|
-
|
|
1375
|
+
|
|
1300
1376
|
// For handshake messages, try the full extraction process
|
|
1301
|
-
const sni = this.extractSNIWithResumptionSupport(
|
|
1302
|
-
|
|
1303
|
-
connectionInfo,
|
|
1304
|
-
enableLogging
|
|
1305
|
-
);
|
|
1306
|
-
|
|
1377
|
+
const sni = this.extractSNIWithResumptionSupport(buffer, connectionInfo, enableLogging);
|
|
1378
|
+
|
|
1307
1379
|
if (sni) {
|
|
1308
1380
|
log(`Successfully extracted SNI: ${sni}`);
|
|
1309
1381
|
return sni;
|
|
1310
1382
|
}
|
|
1311
|
-
|
|
1383
|
+
|
|
1312
1384
|
// If we couldn't extract an SNI, check if this is a valid ClientHello
|
|
1313
1385
|
// If it is, but we couldn't get an SNI, it might be a fragment or
|
|
1314
1386
|
// a connection race situation
|
|
@@ -1319,10 +1391,10 @@ export class SniHandler {
|
|
|
1319
1391
|
log(`Using session cache for ClientHello without SNI: ${sessionCachedSni}`);
|
|
1320
1392
|
return sessionCachedSni;
|
|
1321
1393
|
}
|
|
1322
|
-
|
|
1394
|
+
|
|
1323
1395
|
log('Valid ClientHello detected, but no SNI extracted - might need more data');
|
|
1324
1396
|
}
|
|
1325
|
-
|
|
1397
|
+
|
|
1326
1398
|
return undefined;
|
|
1327
1399
|
}
|
|
1328
|
-
}
|
|
1400
|
+
}
|