@push.rocks/smartproxy 3.37.2 → 3.38.2
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 +27 -4
- package/dist_ts/classes.snihandler.d.ts +91 -4
- package/dist_ts/classes.snihandler.js +425 -13
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +33 -3
- package/ts/classes.snihandler.ts +518 -13
package/ts/classes.snihandler.ts
CHANGED
|
@@ -2,15 +2,25 @@ import { Buffer } from 'buffer';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* SNI (Server Name Indication) handler for TLS connections.
|
|
5
|
-
* Provides robust extraction of SNI values from TLS ClientHello messages
|
|
5
|
+
* Provides robust extraction of SNI values from TLS ClientHello messages
|
|
6
|
+
* with support for fragmented packets, TLS 1.3 resumption, and Chrome-specific
|
|
7
|
+
* connection behaviors.
|
|
6
8
|
*/
|
|
7
9
|
export class SniHandler {
|
|
8
10
|
// TLS record types and constants
|
|
9
11
|
private static readonly TLS_HANDSHAKE_RECORD_TYPE = 22;
|
|
12
|
+
private static readonly TLS_APPLICATION_DATA_TYPE = 23; // TLS Application Data record type
|
|
10
13
|
private static readonly TLS_CLIENT_HELLO_HANDSHAKE_TYPE = 1;
|
|
11
14
|
private static readonly TLS_SNI_EXTENSION_TYPE = 0x0000;
|
|
12
15
|
private static readonly TLS_SESSION_TICKET_EXTENSION_TYPE = 0x0023;
|
|
13
16
|
private static readonly TLS_SNI_HOST_NAME_TYPE = 0;
|
|
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 = 0x002D; // PSK Key Exchange Modes
|
|
19
|
+
private static readonly TLS_EARLY_DATA_EXTENSION_TYPE = 0x002A; // Early Data (0-RTT) extension
|
|
20
|
+
|
|
21
|
+
// Buffer for handling fragmented ClientHello messages
|
|
22
|
+
private static fragmentedBuffers: Map<string, Buffer> = new Map();
|
|
23
|
+
private static fragmentTimeout: number = 1000; // ms to wait for fragments before cleanup
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
26
|
* Checks if a buffer contains a TLS handshake message (record type 22)
|
|
@@ -20,6 +30,107 @@ export class SniHandler {
|
|
|
20
30
|
public static isTlsHandshake(buffer: Buffer): boolean {
|
|
21
31
|
return buffer.length > 0 && buffer[0] === this.TLS_HANDSHAKE_RECORD_TYPE;
|
|
22
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Checks if a buffer contains TLS application data (record type 23)
|
|
36
|
+
* @param buffer - The buffer to check
|
|
37
|
+
* @returns true if the buffer starts with a TLS application data record type
|
|
38
|
+
*/
|
|
39
|
+
public static isTlsApplicationData(buffer: Buffer): boolean {
|
|
40
|
+
return buffer.length > 0 && buffer[0] === this.TLS_APPLICATION_DATA_TYPE;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a connection ID based on source/destination information
|
|
45
|
+
* Used to track fragmented ClientHello messages across multiple packets
|
|
46
|
+
*
|
|
47
|
+
* @param connectionInfo - Object containing connection identifiers (IP/port)
|
|
48
|
+
* @returns A string ID for the connection
|
|
49
|
+
*/
|
|
50
|
+
public static createConnectionId(connectionInfo: {
|
|
51
|
+
sourceIp?: string;
|
|
52
|
+
sourcePort?: number;
|
|
53
|
+
destIp?: string;
|
|
54
|
+
destPort?: number;
|
|
55
|
+
}): string {
|
|
56
|
+
const { sourceIp, sourcePort, destIp, destPort } = connectionInfo;
|
|
57
|
+
return `${sourceIp}:${sourcePort}-${destIp}:${destPort}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Handles potential fragmented ClientHello messages by buffering and reassembling
|
|
62
|
+
* TLS record fragments that might span multiple TCP packets.
|
|
63
|
+
*
|
|
64
|
+
* @param buffer - The current buffer fragment
|
|
65
|
+
* @param connectionId - Unique identifier for the connection
|
|
66
|
+
* @param enableLogging - Whether to enable logging
|
|
67
|
+
* @returns A complete buffer if reassembly is successful, or undefined if more fragments are needed
|
|
68
|
+
*/
|
|
69
|
+
public static handleFragmentedClientHello(
|
|
70
|
+
buffer: Buffer,
|
|
71
|
+
connectionId: string,
|
|
72
|
+
enableLogging: boolean = false
|
|
73
|
+
): Buffer | undefined {
|
|
74
|
+
const log = (message: string) => {
|
|
75
|
+
if (enableLogging) {
|
|
76
|
+
console.log(`[SNI Fragment] ${message}`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Check if we've seen this connection before
|
|
81
|
+
if (!this.fragmentedBuffers.has(connectionId)) {
|
|
82
|
+
// New connection, start with this buffer
|
|
83
|
+
this.fragmentedBuffers.set(connectionId, buffer);
|
|
84
|
+
|
|
85
|
+
// Set timeout to clean up if we don't get a complete ClientHello
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
if (this.fragmentedBuffers.has(connectionId)) {
|
|
88
|
+
this.fragmentedBuffers.delete(connectionId);
|
|
89
|
+
log(`Connection ${connectionId} timed out waiting for complete ClientHello`);
|
|
90
|
+
}
|
|
91
|
+
}, this.fragmentTimeout);
|
|
92
|
+
|
|
93
|
+
// Evaluate if this buffer already contains a complete ClientHello
|
|
94
|
+
try {
|
|
95
|
+
if (buffer.length >= 5) {
|
|
96
|
+
const recordLength = (buffer[3] << 8) + buffer[4];
|
|
97
|
+
if (buffer.length >= recordLength + 5) {
|
|
98
|
+
log(`Initial buffer contains complete ClientHello, length: ${buffer.length}`);
|
|
99
|
+
return buffer;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
log(`Error checking initial buffer completeness: ${e}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
log(`Started buffering connection ${connectionId}, initial size: ${buffer.length}`);
|
|
107
|
+
return undefined; // Need more fragments
|
|
108
|
+
} else {
|
|
109
|
+
// Existing connection, append this buffer
|
|
110
|
+
const existingBuffer = this.fragmentedBuffers.get(connectionId)!;
|
|
111
|
+
const newBuffer = Buffer.concat([existingBuffer, buffer]);
|
|
112
|
+
this.fragmentedBuffers.set(connectionId, newBuffer);
|
|
113
|
+
|
|
114
|
+
log(`Appended to buffer for ${connectionId}, new size: ${newBuffer.length}`);
|
|
115
|
+
|
|
116
|
+
// Check if we now have a complete ClientHello
|
|
117
|
+
try {
|
|
118
|
+
if (newBuffer.length >= 5) {
|
|
119
|
+
const recordLength = (newBuffer[3] << 8) + newBuffer[4];
|
|
120
|
+
if (newBuffer.length >= recordLength + 5) {
|
|
121
|
+
log(`Assembled complete ClientHello, length: ${newBuffer.length}`);
|
|
122
|
+
// Complete message received, remove from tracking
|
|
123
|
+
this.fragmentedBuffers.delete(connectionId);
|
|
124
|
+
return newBuffer;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
log(`Error checking reassembled buffer completeness: ${e}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return undefined; // Still need more fragments
|
|
132
|
+
}
|
|
133
|
+
}
|
|
23
134
|
|
|
24
135
|
/**
|
|
25
136
|
* Checks if a buffer contains a TLS ClientHello message
|
|
@@ -178,6 +289,7 @@ export class SniHandler {
|
|
|
178
289
|
|
|
179
290
|
// Track if we found session tickets (for improved resumption handling)
|
|
180
291
|
let hasSessionTicket = false;
|
|
292
|
+
let hasPskExtension = false;
|
|
181
293
|
|
|
182
294
|
// Iterate through extensions
|
|
183
295
|
while (pos + 4 <= extensionsEnd) {
|
|
@@ -275,15 +387,21 @@ export class SniHandler {
|
|
|
275
387
|
log('Found session ticket extension');
|
|
276
388
|
hasSessionTicket = true;
|
|
277
389
|
pos += extensionLength; // Skip this extension
|
|
390
|
+
} else if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
|
|
391
|
+
// TLS 1.3 PSK extension - mark for resumption support
|
|
392
|
+
log('Found PSK extension (TLS 1.3 resumption indicator)');
|
|
393
|
+
hasPskExtension = true;
|
|
394
|
+
// We'll skip the extension here and process it separately if needed
|
|
395
|
+
pos += extensionLength;
|
|
278
396
|
} else {
|
|
279
397
|
// Skip this extension
|
|
280
398
|
pos += extensionLength;
|
|
281
399
|
}
|
|
282
400
|
}
|
|
283
401
|
|
|
284
|
-
// Log if we found
|
|
285
|
-
if (hasSessionTicket) {
|
|
286
|
-
log('Session
|
|
402
|
+
// Log if we found session resumption indicators but no SNI
|
|
403
|
+
if (hasSessionTicket || hasPskExtension) {
|
|
404
|
+
log('Session resumption indicators present but no SNI found');
|
|
287
405
|
}
|
|
288
406
|
|
|
289
407
|
log('No SNI extension found in ClientHello');
|
|
@@ -294,36 +412,423 @@ export class SniHandler {
|
|
|
294
412
|
}
|
|
295
413
|
}
|
|
296
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Attempts to extract SNI from the PSK extension in a TLS 1.3 ClientHello.
|
|
417
|
+
*
|
|
418
|
+
* In TLS 1.3, when a client attempts to resume a session, it may include
|
|
419
|
+
* the server name in the PSK identity hint rather than in the SNI extension.
|
|
420
|
+
*
|
|
421
|
+
* @param buffer - The buffer containing the TLS ClientHello message
|
|
422
|
+
* @param enableLogging - Whether to enable detailed debug logging
|
|
423
|
+
* @returns The extracted server name or undefined if not found
|
|
424
|
+
*/
|
|
425
|
+
public static extractSNIFromPSKExtension(
|
|
426
|
+
buffer: Buffer,
|
|
427
|
+
enableLogging: boolean = false
|
|
428
|
+
): string | undefined {
|
|
429
|
+
const log = (message: string) => {
|
|
430
|
+
if (enableLogging) {
|
|
431
|
+
console.log(`[PSK-SNI Extraction] ${message}`);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
// Ensure this is a ClientHello
|
|
437
|
+
if (!this.isClientHello(buffer)) {
|
|
438
|
+
log('Not a ClientHello message');
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Find the start position of extensions
|
|
443
|
+
let pos = 5; // Start after record header
|
|
444
|
+
|
|
445
|
+
// Skip handshake type (1 byte)
|
|
446
|
+
pos += 1;
|
|
447
|
+
|
|
448
|
+
// Skip handshake length (3 bytes)
|
|
449
|
+
pos += 3;
|
|
450
|
+
|
|
451
|
+
// Skip client version (2 bytes)
|
|
452
|
+
pos += 2;
|
|
453
|
+
|
|
454
|
+
// Skip client random (32 bytes)
|
|
455
|
+
pos += 32;
|
|
456
|
+
|
|
457
|
+
// Skip session ID
|
|
458
|
+
if (pos + 1 > buffer.length) return undefined;
|
|
459
|
+
const sessionIdLength = buffer[pos];
|
|
460
|
+
pos += 1 + sessionIdLength;
|
|
461
|
+
|
|
462
|
+
// Skip cipher suites
|
|
463
|
+
if (pos + 2 > buffer.length) return undefined;
|
|
464
|
+
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
465
|
+
pos += 2 + cipherSuitesLength;
|
|
466
|
+
|
|
467
|
+
// Skip compression methods
|
|
468
|
+
if (pos + 1 > buffer.length) return undefined;
|
|
469
|
+
const compressionMethodsLength = buffer[pos];
|
|
470
|
+
pos += 1 + compressionMethodsLength;
|
|
471
|
+
|
|
472
|
+
// Check if we have extensions
|
|
473
|
+
if (pos + 2 > buffer.length) {
|
|
474
|
+
log('No extensions present');
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Get extensions length
|
|
479
|
+
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
480
|
+
pos += 2;
|
|
481
|
+
|
|
482
|
+
// Extensions end position
|
|
483
|
+
const extensionsEnd = pos + extensionsLength;
|
|
484
|
+
if (extensionsEnd > buffer.length) return undefined;
|
|
485
|
+
|
|
486
|
+
// Look for PSK extension
|
|
487
|
+
while (pos + 4 <= extensionsEnd) {
|
|
488
|
+
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
489
|
+
pos += 2;
|
|
490
|
+
|
|
491
|
+
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
492
|
+
pos += 2;
|
|
493
|
+
|
|
494
|
+
if (extensionType === this.TLS_PSK_EXTENSION_TYPE) {
|
|
495
|
+
log('Found PSK extension');
|
|
496
|
+
|
|
497
|
+
// PSK extension structure:
|
|
498
|
+
// 2 bytes: identities list length
|
|
499
|
+
if (pos + 2 > extensionsEnd) break;
|
|
500
|
+
const identitiesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
501
|
+
pos += 2;
|
|
502
|
+
|
|
503
|
+
// End of identities list
|
|
504
|
+
const identitiesEnd = pos + identitiesLength;
|
|
505
|
+
if (identitiesEnd > extensionsEnd) break;
|
|
506
|
+
|
|
507
|
+
// Process each PSK identity
|
|
508
|
+
while (pos + 2 <= identitiesEnd) {
|
|
509
|
+
// Identity length (2 bytes)
|
|
510
|
+
if (pos + 2 > identitiesEnd) break;
|
|
511
|
+
const identityLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
512
|
+
pos += 2;
|
|
513
|
+
|
|
514
|
+
if (pos + identityLength > identitiesEnd) break;
|
|
515
|
+
|
|
516
|
+
// Try to extract hostname from identity
|
|
517
|
+
// Chrome often embeds the hostname in the PSK identity
|
|
518
|
+
// This is a heuristic as there's no standard format
|
|
519
|
+
if (identityLength > 0) {
|
|
520
|
+
const identity = buffer.slice(pos, pos + identityLength);
|
|
521
|
+
|
|
522
|
+
// Skip identity bytes
|
|
523
|
+
pos += identityLength;
|
|
524
|
+
|
|
525
|
+
// Skip obfuscated ticket age (4 bytes)
|
|
526
|
+
pos += 4;
|
|
527
|
+
|
|
528
|
+
// Try to parse the identity as UTF-8
|
|
529
|
+
try {
|
|
530
|
+
const identityStr = identity.toString('utf8');
|
|
531
|
+
log(`PSK identity: ${identityStr}`);
|
|
532
|
+
|
|
533
|
+
// Check if the identity contains hostname hints
|
|
534
|
+
// Chrome often embeds the hostname in a known format
|
|
535
|
+
// Try to extract using common patterns
|
|
536
|
+
|
|
537
|
+
// Pattern 1: Look for domain name pattern
|
|
538
|
+
const domainPattern = /([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?/i;
|
|
539
|
+
const domainMatch = identityStr.match(domainPattern);
|
|
540
|
+
if (domainMatch && domainMatch[0]) {
|
|
541
|
+
log(`Found domain in PSK identity: ${domainMatch[0]}`);
|
|
542
|
+
return domainMatch[0];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Pattern 2: Chrome sometimes uses a specific format with delimiters
|
|
546
|
+
// This is a heuristic approach since the format isn't standardized
|
|
547
|
+
const parts = identityStr.split('|');
|
|
548
|
+
if (parts.length > 1) {
|
|
549
|
+
for (const part of parts) {
|
|
550
|
+
if (part.includes('.') && !part.includes('/')) {
|
|
551
|
+
const possibleDomain = part.trim();
|
|
552
|
+
if (/^[a-z0-9.-]+$/i.test(possibleDomain)) {
|
|
553
|
+
log(`Found possible domain in PSK delimiter format: ${possibleDomain}`);
|
|
554
|
+
return possibleDomain;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
} catch (e) {
|
|
560
|
+
log('Failed to parse PSK identity as UTF-8');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
// Skip this extension
|
|
566
|
+
pos += extensionLength;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
log('No hostname found in PSK extension');
|
|
571
|
+
return undefined;
|
|
572
|
+
} catch (error) {
|
|
573
|
+
log(`Error parsing PSK: ${error instanceof Error ? error.message : String(error)}`);
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Checks if the buffer contains TLS 1.3 early data (0-RTT)
|
|
580
|
+
* @param buffer - The buffer to check
|
|
581
|
+
* @param enableLogging - Whether to enable logging
|
|
582
|
+
* @returns true if early data is detected
|
|
583
|
+
*/
|
|
584
|
+
public static hasEarlyData(
|
|
585
|
+
buffer: Buffer,
|
|
586
|
+
enableLogging: boolean = false
|
|
587
|
+
): boolean {
|
|
588
|
+
const log = (message: string) => {
|
|
589
|
+
if (enableLogging) {
|
|
590
|
+
console.log(`[Early Data] ${message}`);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
// Check if this is a valid ClientHello first
|
|
596
|
+
if (!this.isClientHello(buffer)) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Find the extensions section
|
|
601
|
+
let pos = 5; // Start after record header
|
|
602
|
+
|
|
603
|
+
// Skip handshake type (1 byte)
|
|
604
|
+
pos += 1;
|
|
605
|
+
|
|
606
|
+
// Skip handshake length (3 bytes)
|
|
607
|
+
pos += 3;
|
|
608
|
+
|
|
609
|
+
// Skip client version (2 bytes)
|
|
610
|
+
pos += 2;
|
|
611
|
+
|
|
612
|
+
// Skip client random (32 bytes)
|
|
613
|
+
pos += 32;
|
|
614
|
+
|
|
615
|
+
// Skip session ID
|
|
616
|
+
if (pos + 1 > buffer.length) return false;
|
|
617
|
+
const sessionIdLength = buffer[pos];
|
|
618
|
+
pos += 1 + sessionIdLength;
|
|
619
|
+
|
|
620
|
+
// Skip cipher suites
|
|
621
|
+
if (pos + 2 > buffer.length) return false;
|
|
622
|
+
const cipherSuitesLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
623
|
+
pos += 2 + cipherSuitesLength;
|
|
624
|
+
|
|
625
|
+
// Skip compression methods
|
|
626
|
+
if (pos + 1 > buffer.length) return false;
|
|
627
|
+
const compressionMethodsLength = buffer[pos];
|
|
628
|
+
pos += 1 + compressionMethodsLength;
|
|
629
|
+
|
|
630
|
+
// Check if we have extensions
|
|
631
|
+
if (pos + 2 > buffer.length) return false;
|
|
632
|
+
|
|
633
|
+
// Get extensions length
|
|
634
|
+
const extensionsLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
635
|
+
pos += 2;
|
|
636
|
+
|
|
637
|
+
// Extensions end position
|
|
638
|
+
const extensionsEnd = pos + extensionsLength;
|
|
639
|
+
if (extensionsEnd > buffer.length) return false;
|
|
640
|
+
|
|
641
|
+
// Look for early data extension
|
|
642
|
+
while (pos + 4 <= extensionsEnd) {
|
|
643
|
+
const extensionType = (buffer[pos] << 8) + buffer[pos + 1];
|
|
644
|
+
pos += 2;
|
|
645
|
+
|
|
646
|
+
const extensionLength = (buffer[pos] << 8) + buffer[pos + 1];
|
|
647
|
+
pos += 2;
|
|
648
|
+
|
|
649
|
+
if (extensionType === this.TLS_EARLY_DATA_EXTENSION_TYPE) {
|
|
650
|
+
log('Early Data (0-RTT) extension detected');
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Skip to next extension
|
|
655
|
+
pos += extensionLength;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return false;
|
|
659
|
+
} catch (error) {
|
|
660
|
+
log(`Error checking for early data: ${error}`);
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
297
665
|
/**
|
|
298
666
|
* Attempts to extract SNI from an initial ClientHello packet and handles
|
|
299
667
|
* session resumption edge cases more robustly than the standard extraction.
|
|
300
668
|
*
|
|
301
|
-
* This method
|
|
302
|
-
*
|
|
669
|
+
* This method handles:
|
|
670
|
+
* 1. Standard SNI extraction
|
|
671
|
+
* 2. TLS 1.3 PSK-based resumption (Chrome, Firefox, etc.)
|
|
672
|
+
* 3. Session ticket-based resumption
|
|
673
|
+
* 4. Fragmented ClientHello messages
|
|
674
|
+
* 5. TLS 1.3 Early Data (0-RTT)
|
|
675
|
+
* 6. Chrome's connection racing behaviors
|
|
303
676
|
*
|
|
304
677
|
* @param buffer - The buffer containing the TLS ClientHello message
|
|
678
|
+
* @param connectionInfo - Optional connection information for fragment handling
|
|
305
679
|
* @param enableLogging - Whether to enable detailed debug logging
|
|
306
680
|
* @returns The extracted server name or undefined if not found
|
|
307
681
|
*/
|
|
308
682
|
public static extractSNIWithResumptionSupport(
|
|
309
|
-
buffer: Buffer,
|
|
683
|
+
buffer: Buffer,
|
|
684
|
+
connectionInfo?: {
|
|
685
|
+
sourceIp?: string;
|
|
686
|
+
sourcePort?: number;
|
|
687
|
+
destIp?: string;
|
|
688
|
+
destPort?: number;
|
|
689
|
+
},
|
|
310
690
|
enableLogging: boolean = false
|
|
311
691
|
): string | undefined {
|
|
692
|
+
const log = (message: string) => {
|
|
693
|
+
if (enableLogging) {
|
|
694
|
+
console.log(`[SNI Extraction] ${message}`);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// Check if we need to handle fragmented packets
|
|
699
|
+
let processBuffer = buffer;
|
|
700
|
+
if (connectionInfo) {
|
|
701
|
+
const connectionId = this.createConnectionId(connectionInfo);
|
|
702
|
+
const reassembledBuffer = this.handleFragmentedClientHello(
|
|
703
|
+
buffer,
|
|
704
|
+
connectionId,
|
|
705
|
+
enableLogging
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
if (!reassembledBuffer) {
|
|
709
|
+
log(`Waiting for more fragments on connection ${connectionId}`);
|
|
710
|
+
return undefined; // Need more fragments to complete ClientHello
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
processBuffer = reassembledBuffer;
|
|
714
|
+
log(`Using reassembled buffer of length ${processBuffer.length}`);
|
|
715
|
+
}
|
|
716
|
+
|
|
312
717
|
// First try the standard SNI extraction
|
|
313
|
-
const standardSni = this.extractSNI(
|
|
718
|
+
const standardSni = this.extractSNI(processBuffer, enableLogging);
|
|
314
719
|
if (standardSni) {
|
|
720
|
+
log(`Found standard SNI: ${standardSni}`);
|
|
315
721
|
return standardSni;
|
|
316
722
|
}
|
|
317
723
|
|
|
724
|
+
// Check for TLS 1.3 early data (0-RTT)
|
|
725
|
+
const hasEarly = this.hasEarlyData(processBuffer, enableLogging);
|
|
726
|
+
if (hasEarly) {
|
|
727
|
+
log('TLS 1.3 Early Data detected, using special handling');
|
|
728
|
+
// In 0-RTT, Chrome often relies on server remembering the SNI from previous sessions
|
|
729
|
+
// We could implement session tracking here if necessary
|
|
730
|
+
}
|
|
731
|
+
|
|
318
732
|
// If standard extraction failed and we have a valid ClientHello,
|
|
319
733
|
// this might be a session resumption with non-standard format
|
|
320
|
-
if (this.isClientHello(
|
|
321
|
-
|
|
322
|
-
|
|
734
|
+
if (this.isClientHello(processBuffer)) {
|
|
735
|
+
log('Detected ClientHello without standard SNI, possible session resumption');
|
|
736
|
+
|
|
737
|
+
// Try to extract from PSK extension (TLS 1.3 resumption)
|
|
738
|
+
const pskSni = this.extractSNIFromPSKExtension(processBuffer, enableLogging);
|
|
739
|
+
if (pskSni) {
|
|
740
|
+
log(`Extracted SNI from PSK extension: ${pskSni}`);
|
|
741
|
+
return pskSni;
|
|
323
742
|
}
|
|
324
743
|
|
|
325
|
-
//
|
|
326
|
-
//
|
|
744
|
+
// Special handling for Chrome connection racing
|
|
745
|
+
// Chrome often opens multiple connections in parallel with different
|
|
746
|
+
// characteristics to improve performance
|
|
747
|
+
// Here we would look for specific patterns in ClientHello that indicate
|
|
748
|
+
// it's part of a connection race
|
|
749
|
+
|
|
750
|
+
// Detect if this is likely a secondary connection in a race
|
|
751
|
+
// by examining the cipher suites and extensions
|
|
752
|
+
// This would require session state tracking across connections
|
|
753
|
+
|
|
754
|
+
log('Failed to extract SNI from resumption mechanisms');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Main entry point for SNI extraction that handles all edge cases.
|
|
762
|
+
* This should be called for each TLS packet received from a client.
|
|
763
|
+
*
|
|
764
|
+
* The method uses connection tracking to handle fragmented ClientHello
|
|
765
|
+
* messages and various TLS 1.3 behaviors, including Chrome's connection
|
|
766
|
+
* racing patterns.
|
|
767
|
+
*
|
|
768
|
+
* @param buffer - The buffer containing TLS data
|
|
769
|
+
* @param connectionInfo - Connection metadata (IPs and ports)
|
|
770
|
+
* @param enableLogging - Whether to enable detailed debug logging
|
|
771
|
+
* @param cachedSni - Optional cached SNI from previous connections (for racing detection)
|
|
772
|
+
* @returns The extracted server name or undefined if not found or more data needed
|
|
773
|
+
*/
|
|
774
|
+
public static processTlsPacket(
|
|
775
|
+
buffer: Buffer,
|
|
776
|
+
connectionInfo: {
|
|
777
|
+
sourceIp: string;
|
|
778
|
+
sourcePort: number;
|
|
779
|
+
destIp: string;
|
|
780
|
+
destPort: number;
|
|
781
|
+
timestamp?: number;
|
|
782
|
+
},
|
|
783
|
+
enableLogging: boolean = false,
|
|
784
|
+
cachedSni?: string
|
|
785
|
+
): string | undefined {
|
|
786
|
+
const log = (message: string) => {
|
|
787
|
+
if (enableLogging) {
|
|
788
|
+
console.log(`[TLS Packet] ${message}`);
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// Add timestamp if not provided
|
|
793
|
+
if (!connectionInfo.timestamp) {
|
|
794
|
+
connectionInfo.timestamp = Date.now();
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Check if this is a TLS handshake
|
|
798
|
+
if (!this.isTlsHandshake(buffer) && !this.isTlsApplicationData(buffer)) {
|
|
799
|
+
log('Not a TLS handshake or application data packet');
|
|
800
|
+
return undefined;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Create connection ID for tracking
|
|
804
|
+
const connectionId = this.createConnectionId(connectionInfo);
|
|
805
|
+
log(`Processing TLS packet for connection ${connectionId}, buffer length: ${buffer.length}`);
|
|
806
|
+
|
|
807
|
+
// Handle special case: if we already have a cached SNI from a previous
|
|
808
|
+
// connection from the same client IP within a short time window,
|
|
809
|
+
// this might be a connection racing situation
|
|
810
|
+
if (cachedSni && this.isTlsApplicationData(buffer)) {
|
|
811
|
+
log(`Using cached SNI from connection racing: ${cachedSni}`);
|
|
812
|
+
return cachedSni;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Try to extract SNI with full resumption support and fragment handling
|
|
816
|
+
const sni = this.extractSNIWithResumptionSupport(
|
|
817
|
+
buffer,
|
|
818
|
+
connectionInfo,
|
|
819
|
+
enableLogging
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
if (sni) {
|
|
823
|
+
log(`Successfully extracted SNI: ${sni}`);
|
|
824
|
+
return sni;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// If we couldn't extract an SNI, check if this is a valid ClientHello
|
|
828
|
+
// If it is, but we couldn't get an SNI, it might be a fragment or
|
|
829
|
+
// a connection race situation
|
|
830
|
+
if (this.isClientHello(buffer)) {
|
|
831
|
+
log('Valid ClientHello detected, but no SNI extracted - might need more data');
|
|
327
832
|
}
|
|
328
833
|
|
|
329
834
|
return undefined;
|