@push.rocks/smartproxy 3.30.7 → 3.31.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.js +307 -104
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +383 -131
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartproxy",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.31.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartproxy',
|
|
6
|
-
version: '3.
|
|
6
|
+
version: '3.31.0',
|
|
7
7
|
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
|
|
8
8
|
}
|
package/ts/classes.portproxy.ts
CHANGED
|
@@ -100,14 +100,82 @@ interface IConnectionRecord {
|
|
|
100
100
|
lastSleepDetection?: number; // Timestamp of the last sleep detection
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Structure to track TLS session information for proper resumption handling
|
|
105
|
+
*/
|
|
106
|
+
interface ITlsSessionInfo {
|
|
107
|
+
domain: string; // The SNI domain associated with this session
|
|
108
|
+
sessionId?: Buffer; // The TLS session ID (if available)
|
|
109
|
+
ticketId?: string; // Session ticket identifier for newer TLS versions
|
|
110
|
+
ticketTimestamp: number; // When this session was recorded
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Global cache of TLS session IDs to SNI domains
|
|
114
|
+
// This ensures resumed sessions maintain their SNI binding
|
|
115
|
+
const tlsSessionCache = new Map<string, ITlsSessionInfo>();
|
|
116
|
+
|
|
117
|
+
// Reference to session cleanup timer so we can clear it
|
|
118
|
+
let tlsSessionCleanupTimer: NodeJS.Timeout | null = null;
|
|
119
|
+
|
|
120
|
+
// Start the cleanup timer for session cache
|
|
121
|
+
function startSessionCleanupTimer() {
|
|
122
|
+
// Avoid creating multiple timers
|
|
123
|
+
if (tlsSessionCleanupTimer) {
|
|
124
|
+
clearInterval(tlsSessionCleanupTimer);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Create new cleanup timer
|
|
128
|
+
tlsSessionCleanupTimer = setInterval(() => {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
const expiryTime = 24 * 60 * 60 * 1000; // 24 hours
|
|
131
|
+
|
|
132
|
+
for (const [sessionId, info] of tlsSessionCache.entries()) {
|
|
133
|
+
if (now - info.ticketTimestamp > expiryTime) {
|
|
134
|
+
tlsSessionCache.delete(sessionId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}, 60 * 60 * 1000); // Clean up once per hour
|
|
138
|
+
|
|
139
|
+
// Make sure the interval doesn't keep the process alive
|
|
140
|
+
if (tlsSessionCleanupTimer.unref) {
|
|
141
|
+
tlsSessionCleanupTimer.unref();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Start the timer initially
|
|
146
|
+
startSessionCleanupTimer();
|
|
147
|
+
|
|
148
|
+
// Function to stop the cleanup timer (used during shutdown)
|
|
149
|
+
function stopSessionCleanupTimer() {
|
|
150
|
+
if (tlsSessionCleanupTimer) {
|
|
151
|
+
clearInterval(tlsSessionCleanupTimer);
|
|
152
|
+
tlsSessionCleanupTimer = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Return type for the extractSNIInfo function
|
|
158
|
+
*/
|
|
159
|
+
interface ISNIExtractResult {
|
|
160
|
+
serverName?: string; // The extracted SNI hostname
|
|
161
|
+
sessionId?: Buffer; // The TLS session ID if present
|
|
162
|
+
sessionIdKey?: string; // The hex string representation of session ID
|
|
163
|
+
sessionTicketId?: string; // Session ticket identifier for TLS 1.3+ resumption
|
|
164
|
+
hasSessionTicket?: boolean; // Whether a session ticket extension was found
|
|
165
|
+
isResumption: boolean; // Whether this appears to be a session resumption
|
|
166
|
+
resumedDomain?: string; // The domain associated with the session if resuming
|
|
167
|
+
}
|
|
168
|
+
|
|
103
169
|
/**
|
|
104
170
|
* Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
|
|
105
171
|
* Enhanced for robustness and detailed logging.
|
|
172
|
+
* Also extracts and tracks TLS Session IDs for session resumption handling.
|
|
173
|
+
*
|
|
106
174
|
* @param buffer - Buffer containing the TLS ClientHello.
|
|
107
175
|
* @param enableLogging - Whether to enable detailed logging.
|
|
108
|
-
* @returns
|
|
176
|
+
* @returns An object containing SNI and session information, or undefined if parsing fails.
|
|
109
177
|
*/
|
|
110
|
-
function
|
|
178
|
+
function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExtractResult | undefined {
|
|
111
179
|
try {
|
|
112
180
|
// Check if buffer is too small for TLS
|
|
113
181
|
if (buffer.length < 5) {
|
|
@@ -153,9 +221,38 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
153
221
|
|
|
154
222
|
offset += 2 + 32; // Skip client version and random
|
|
155
223
|
|
|
156
|
-
// Session ID
|
|
224
|
+
// Extract Session ID for session resumption tracking
|
|
157
225
|
const sessionIDLength = buffer.readUInt8(offset);
|
|
158
226
|
if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
|
|
227
|
+
|
|
228
|
+
// If there's a session ID, extract it
|
|
229
|
+
let sessionId: Buffer | undefined;
|
|
230
|
+
let sessionIdKey: string | undefined;
|
|
231
|
+
let isResumption = false;
|
|
232
|
+
let resumedDomain: string | undefined;
|
|
233
|
+
|
|
234
|
+
if (sessionIDLength > 0) {
|
|
235
|
+
sessionId = Buffer.from(buffer.slice(offset + 1, offset + 1 + sessionIDLength));
|
|
236
|
+
|
|
237
|
+
// Convert sessionId to a string key for our cache
|
|
238
|
+
sessionIdKey = sessionId.toString('hex');
|
|
239
|
+
|
|
240
|
+
if (enableLogging) {
|
|
241
|
+
console.log(`Session ID: ${sessionIdKey}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check if this is a session resumption attempt
|
|
245
|
+
if (tlsSessionCache.has(sessionIdKey)) {
|
|
246
|
+
const cachedInfo = tlsSessionCache.get(sessionIdKey)!;
|
|
247
|
+
resumedDomain = cachedInfo.domain;
|
|
248
|
+
isResumption = true;
|
|
249
|
+
|
|
250
|
+
if (enableLogging) {
|
|
251
|
+
console.log(`TLS Session Resumption detected for domain: ${resumedDomain}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
159
256
|
offset += 1 + sessionIDLength; // Skip session ID
|
|
160
257
|
|
|
161
258
|
// Cipher suites
|
|
@@ -194,6 +291,10 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
194
291
|
return undefined;
|
|
195
292
|
}
|
|
196
293
|
|
|
294
|
+
// Variables to track session tickets
|
|
295
|
+
let hasSessionTicket = false;
|
|
296
|
+
let sessionTicketId: string | undefined;
|
|
297
|
+
|
|
197
298
|
// Parse extensions
|
|
198
299
|
while (offset + 4 <= extensionsEnd) {
|
|
199
300
|
const extensionType = buffer.readUInt16BE(offset);
|
|
@@ -203,6 +304,33 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
203
304
|
console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
|
|
204
305
|
|
|
205
306
|
offset += 4;
|
|
307
|
+
|
|
308
|
+
// Check for Session Ticket extension (type 0x0023)
|
|
309
|
+
if (extensionType === 0x0023 && extensionLength > 0) {
|
|
310
|
+
hasSessionTicket = true;
|
|
311
|
+
|
|
312
|
+
// Extract a hash of the ticket for tracking
|
|
313
|
+
if (extensionLength > 16) { // Ensure we have enough bytes to create a meaningful ID
|
|
314
|
+
const ticketBytes = buffer.slice(offset, offset + Math.min(16, extensionLength));
|
|
315
|
+
sessionTicketId = ticketBytes.toString('hex');
|
|
316
|
+
|
|
317
|
+
if (enableLogging) {
|
|
318
|
+
console.log(`Session Ticket found, ID: ${sessionTicketId}`);
|
|
319
|
+
|
|
320
|
+
// Check if this is a known session ticket
|
|
321
|
+
if (tlsSessionCache.has(`ticket:${sessionTicketId}`)) {
|
|
322
|
+
const cachedInfo = tlsSessionCache.get(`ticket:${sessionTicketId}`);
|
|
323
|
+
console.log(`TLS Session Ticket Resumption detected for domain: ${cachedInfo?.domain}`);
|
|
324
|
+
|
|
325
|
+
// Set isResumption and resumedDomain if not already set
|
|
326
|
+
if (!isResumption && !resumedDomain) {
|
|
327
|
+
isResumption = true;
|
|
328
|
+
resumedDomain = cachedInfo?.domain;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
206
334
|
|
|
207
335
|
if (extensionType === 0x0000) {
|
|
208
336
|
// SNI extension
|
|
@@ -245,7 +373,43 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
245
373
|
|
|
246
374
|
const serverName = buffer.toString('utf8', offset, offset + nameLen);
|
|
247
375
|
if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
|
|
248
|
-
|
|
376
|
+
|
|
377
|
+
// Store the session ID to domain mapping for future resumptions
|
|
378
|
+
if (sessionIdKey && sessionId && serverName) {
|
|
379
|
+
tlsSessionCache.set(sessionIdKey, {
|
|
380
|
+
domain: serverName,
|
|
381
|
+
sessionId: sessionId,
|
|
382
|
+
ticketTimestamp: Date.now()
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
if (enableLogging) {
|
|
386
|
+
console.log(`Stored session ${sessionIdKey} for domain ${serverName}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Also store session ticket information if present
|
|
391
|
+
if (sessionTicketId && serverName) {
|
|
392
|
+
tlsSessionCache.set(`ticket:${sessionTicketId}`, {
|
|
393
|
+
domain: serverName,
|
|
394
|
+
ticketId: sessionTicketId,
|
|
395
|
+
ticketTimestamp: Date.now()
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (enableLogging) {
|
|
399
|
+
console.log(`Stored session ticket ${sessionTicketId} for domain ${serverName}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Return the complete extraction result
|
|
404
|
+
return {
|
|
405
|
+
serverName,
|
|
406
|
+
sessionId,
|
|
407
|
+
sessionIdKey,
|
|
408
|
+
sessionTicketId,
|
|
409
|
+
isResumption,
|
|
410
|
+
resumedDomain,
|
|
411
|
+
hasSessionTicket
|
|
412
|
+
};
|
|
249
413
|
}
|
|
250
414
|
|
|
251
415
|
offset += nameLen;
|
|
@@ -257,13 +421,46 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
|
|
|
257
421
|
}
|
|
258
422
|
|
|
259
423
|
if (enableLogging) console.log('No SNI extension found');
|
|
260
|
-
|
|
424
|
+
|
|
425
|
+
// Even without SNI, we might be dealing with a session resumption
|
|
426
|
+
if (isResumption && resumedDomain) {
|
|
427
|
+
return {
|
|
428
|
+
serverName: resumedDomain, // Use the domain from previous session
|
|
429
|
+
sessionId,
|
|
430
|
+
sessionIdKey,
|
|
431
|
+
sessionTicketId,
|
|
432
|
+
hasSessionTicket,
|
|
433
|
+
isResumption: true,
|
|
434
|
+
resumedDomain
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Return a basic result with just the session info
|
|
439
|
+
return {
|
|
440
|
+
isResumption,
|
|
441
|
+
sessionId,
|
|
442
|
+
sessionIdKey,
|
|
443
|
+
sessionTicketId,
|
|
444
|
+
hasSessionTicket,
|
|
445
|
+
resumedDomain
|
|
446
|
+
};
|
|
261
447
|
} catch (err) {
|
|
262
448
|
console.log(`Error extracting SNI: ${err}`);
|
|
263
449
|
return undefined;
|
|
264
450
|
}
|
|
265
451
|
}
|
|
266
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Legacy wrapper for extractSNIInfo to maintain backward compatibility
|
|
455
|
+
* @param buffer - Buffer containing the TLS ClientHello
|
|
456
|
+
* @param enableLogging - Whether to enable detailed logging
|
|
457
|
+
* @returns The server name if found, otherwise undefined
|
|
458
|
+
*/
|
|
459
|
+
function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
|
|
460
|
+
const result = extractSNIInfo(buffer, enableLogging);
|
|
461
|
+
return result?.serverName;
|
|
462
|
+
}
|
|
463
|
+
|
|
267
464
|
// Helper: Check if a port falls within any of the given port ranges
|
|
268
465
|
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
|
269
466
|
return ranges.some((range) => port >= range.from && port <= range.to);
|
|
@@ -501,28 +698,17 @@ export class PortProxy {
|
|
|
501
698
|
}
|
|
502
699
|
this.cleanupConnection(record, 'client_closed');
|
|
503
700
|
});
|
|
504
|
-
|
|
505
|
-
//
|
|
701
|
+
|
|
702
|
+
// Special handler for TLS handshake detection with NetworkProxy
|
|
506
703
|
socket.on('data', (chunk: Buffer) => {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
// Check for potential TLS renegotiation or reconnection packets
|
|
704
|
+
// Check for TLS handshake packets (ContentType.handshake)
|
|
510
705
|
if (chunk.length > 0 && chunk[0] === 22) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
console.log(
|
|
514
|
-
`[${connectionId}] Detected potential TLS handshake data while connected to NetworkProxy`
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// NOTE: We don't need to explicitly forward the renegotiation packets
|
|
519
|
-
// because socket.pipe(proxySocket) is already handling that.
|
|
520
|
-
// The pipe ensures all data (including renegotiation) flows through properly.
|
|
521
|
-
// Just update the activity timestamp to prevent timeouts
|
|
522
|
-
record.lastActivity = Date.now();
|
|
706
|
+
console.log(`[${connectionId}] Detected potential TLS handshake with NetworkProxy, updating activity`);
|
|
707
|
+
this.updateActivity(record);
|
|
523
708
|
}
|
|
524
709
|
});
|
|
525
710
|
|
|
711
|
+
// Update activity on data transfer from the proxy socket
|
|
526
712
|
proxySocket.on('data', () => this.updateActivity(record));
|
|
527
713
|
|
|
528
714
|
if (this.settings.enableDetailedLogging) {
|
|
@@ -778,6 +964,82 @@ export class PortProxy {
|
|
|
778
964
|
return this.initiateCleanupOnce(record, 'write_error');
|
|
779
965
|
}
|
|
780
966
|
|
|
967
|
+
// Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
|
|
968
|
+
if (serverName && record.isTLS) {
|
|
969
|
+
// This listener handles TLS renegotiation detection
|
|
970
|
+
socket.on('data', (renegChunk) => {
|
|
971
|
+
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
972
|
+
// Always update activity timestamp for any handshake packet
|
|
973
|
+
this.updateActivity(record);
|
|
974
|
+
|
|
975
|
+
try {
|
|
976
|
+
// Extract all TLS information including session resumption data
|
|
977
|
+
const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
|
|
978
|
+
let newSNI = sniInfo?.serverName;
|
|
979
|
+
|
|
980
|
+
// Handle session resumption - if we recognize the session ID, we know what domain it belongs to
|
|
981
|
+
if (sniInfo?.isResumption && sniInfo.resumedDomain) {
|
|
982
|
+
console.log(`[${connectionId}] Rehandshake with session resumption for domain: ${sniInfo.resumedDomain}`);
|
|
983
|
+
newSNI = sniInfo.resumedDomain;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// IMPORTANT: If we can't extract an SNI from renegotiation, we MUST allow it through
|
|
987
|
+
if (newSNI === undefined) {
|
|
988
|
+
console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Check if the SNI has changed
|
|
993
|
+
if (newSNI !== serverName) {
|
|
994
|
+
console.log(`[${connectionId}] Rehandshake with different SNI: ${newSNI} vs original ${serverName}`);
|
|
995
|
+
|
|
996
|
+
// Allow if the new SNI matches existing domain config or find a new matching config
|
|
997
|
+
let allowed = false;
|
|
998
|
+
|
|
999
|
+
if (record.domainConfig) {
|
|
1000
|
+
allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (!allowed) {
|
|
1004
|
+
const newDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
1005
|
+
config.domains.some((d) => plugins.minimatch(newSNI, d))
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
if (newDomainConfig) {
|
|
1009
|
+
const effectiveAllowedIPs = [
|
|
1010
|
+
...newDomainConfig.allowedIPs,
|
|
1011
|
+
...(this.settings.defaultAllowedIPs || []),
|
|
1012
|
+
];
|
|
1013
|
+
const effectiveBlockedIPs = [
|
|
1014
|
+
...(newDomainConfig.blockedIPs || []),
|
|
1015
|
+
...(this.settings.defaultBlockedIPs || []),
|
|
1016
|
+
];
|
|
1017
|
+
|
|
1018
|
+
allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
|
|
1019
|
+
|
|
1020
|
+
if (allowed) {
|
|
1021
|
+
record.domainConfig = newDomainConfig;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (allowed) {
|
|
1027
|
+
console.log(`[${connectionId}] Updated domain for connection from ${record.remoteIP} to: ${newSNI}`);
|
|
1028
|
+
record.lockedDomain = newSNI;
|
|
1029
|
+
} else {
|
|
1030
|
+
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
|
|
1031
|
+
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
1032
|
+
}
|
|
1033
|
+
} else {
|
|
1034
|
+
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
|
|
1035
|
+
}
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
781
1043
|
// Now set up piping for future data and resume the socket
|
|
782
1044
|
socket.pipe(targetSocket);
|
|
783
1045
|
targetSocket.pipe(socket);
|
|
@@ -811,7 +1073,83 @@ export class PortProxy {
|
|
|
811
1073
|
}
|
|
812
1074
|
});
|
|
813
1075
|
} else {
|
|
814
|
-
//
|
|
1076
|
+
// Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
|
|
1077
|
+
if (serverName && record.isTLS) {
|
|
1078
|
+
// This listener handles TLS renegotiation detection
|
|
1079
|
+
socket.on('data', (renegChunk) => {
|
|
1080
|
+
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
1081
|
+
// Always update activity timestamp for any handshake packet
|
|
1082
|
+
this.updateActivity(record);
|
|
1083
|
+
|
|
1084
|
+
try {
|
|
1085
|
+
// Extract all TLS information including session resumption data
|
|
1086
|
+
const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
|
|
1087
|
+
let newSNI = sniInfo?.serverName;
|
|
1088
|
+
|
|
1089
|
+
// Handle session resumption - if we recognize the session ID, we know what domain it belongs to
|
|
1090
|
+
if (sniInfo?.isResumption && sniInfo.resumedDomain) {
|
|
1091
|
+
console.log(`[${connectionId}] Rehandshake with session resumption for domain: ${sniInfo.resumedDomain}`);
|
|
1092
|
+
newSNI = sniInfo.resumedDomain;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// IMPORTANT: If we can't extract an SNI from renegotiation, we MUST allow it through
|
|
1096
|
+
if (newSNI === undefined) {
|
|
1097
|
+
console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Check if the SNI has changed
|
|
1102
|
+
if (newSNI !== serverName) {
|
|
1103
|
+
console.log(`[${connectionId}] Rehandshake with different SNI: ${newSNI} vs original ${serverName}`);
|
|
1104
|
+
|
|
1105
|
+
// Allow if the new SNI matches existing domain config or find a new matching config
|
|
1106
|
+
let allowed = false;
|
|
1107
|
+
|
|
1108
|
+
if (record.domainConfig) {
|
|
1109
|
+
allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (!allowed) {
|
|
1113
|
+
const newDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
1114
|
+
config.domains.some((d) => plugins.minimatch(newSNI, d))
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
if (newDomainConfig) {
|
|
1118
|
+
const effectiveAllowedIPs = [
|
|
1119
|
+
...newDomainConfig.allowedIPs,
|
|
1120
|
+
...(this.settings.defaultAllowedIPs || []),
|
|
1121
|
+
];
|
|
1122
|
+
const effectiveBlockedIPs = [
|
|
1123
|
+
...(newDomainConfig.blockedIPs || []),
|
|
1124
|
+
...(this.settings.defaultBlockedIPs || []),
|
|
1125
|
+
];
|
|
1126
|
+
|
|
1127
|
+
allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
|
|
1128
|
+
|
|
1129
|
+
if (allowed) {
|
|
1130
|
+
record.domainConfig = newDomainConfig;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (allowed) {
|
|
1136
|
+
console.log(`[${connectionId}] Updated domain for connection from ${record.remoteIP} to: ${newSNI}`);
|
|
1137
|
+
record.lockedDomain = newSNI;
|
|
1138
|
+
} else {
|
|
1139
|
+
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
|
|
1140
|
+
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
1141
|
+
}
|
|
1142
|
+
} else {
|
|
1143
|
+
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
|
|
1144
|
+
}
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Now set up piping
|
|
815
1153
|
socket.pipe(targetSocket);
|
|
816
1154
|
targetSocket.pipe(socket);
|
|
817
1155
|
socket.resume(); // Resume the socket after piping is established
|
|
@@ -848,113 +1186,8 @@ export class PortProxy {
|
|
|
848
1186
|
record.pendingData = [];
|
|
849
1187
|
record.pendingDataSize = 0;
|
|
850
1188
|
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
// This listener will check for TLS renegotiation attempts
|
|
854
|
-
// Note: We don't need to explicitly forward the renegotiation packets
|
|
855
|
-
// since socket.pipe(targetSocket) is already set up earlier and handles that
|
|
856
|
-
socket.on('data', (renegChunk: Buffer) => {
|
|
857
|
-
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
858
|
-
try {
|
|
859
|
-
// Try to extract SNI from potential renegotiation
|
|
860
|
-
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
|
861
|
-
|
|
862
|
-
// IMPORTANT: If we can't extract an SNI from renegotiation, we MUST allow it through
|
|
863
|
-
// Otherwise valid renegotiations that don't explicitly repeat the SNI will break
|
|
864
|
-
if (newSNI === undefined) {
|
|
865
|
-
if (this.settings.enableDetailedLogging) {
|
|
866
|
-
console.log(
|
|
867
|
-
`[${connectionId}] Rehandshake detected without SNI, allowing it through.`
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
// Let it pass through - this is critical for Chrome's TLS handling
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Check if the SNI has changed
|
|
875
|
-
if (newSNI && newSNI !== record.lockedDomain) {
|
|
876
|
-
// Always check whether the new SNI would be allowed by the EXISTING domain config first
|
|
877
|
-
// This ensures we're using the same ruleset that allowed the initial connection
|
|
878
|
-
let allowed = false;
|
|
879
|
-
|
|
880
|
-
// First check if the exact original domain config would allow this new SNI
|
|
881
|
-
if (record.domainConfig) {
|
|
882
|
-
// Check if the new SNI matches any domain pattern in the original domain config
|
|
883
|
-
allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
|
|
884
|
-
|
|
885
|
-
if (allowed && this.settings.enableDetailedLogging) {
|
|
886
|
-
console.log(
|
|
887
|
-
`[${connectionId}] Rehandshake with new SNI: ${newSNI} matched existing domain config ` +
|
|
888
|
-
`patterns ${record.domainConfig.domains.join(', ')}. Allowing connection reuse.`
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// If not allowed by the existing domain config, try to find another domain config
|
|
894
|
-
if (!allowed) {
|
|
895
|
-
const newDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
896
|
-
config.domains.some((d) => plugins.minimatch(newSNI, d))
|
|
897
|
-
);
|
|
898
|
-
|
|
899
|
-
// If we found a matching domain config, check IP rules
|
|
900
|
-
if (newDomainConfig) {
|
|
901
|
-
const effectiveAllowedIPs = [
|
|
902
|
-
...newDomainConfig.allowedIPs,
|
|
903
|
-
...(this.settings.defaultAllowedIPs || []),
|
|
904
|
-
];
|
|
905
|
-
const effectiveBlockedIPs = [
|
|
906
|
-
...(newDomainConfig.blockedIPs || []),
|
|
907
|
-
...(this.settings.defaultBlockedIPs || []),
|
|
908
|
-
];
|
|
909
|
-
|
|
910
|
-
// Check if the IP is allowed for the new domain
|
|
911
|
-
allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
|
|
912
|
-
|
|
913
|
-
if (allowed && this.settings.enableDetailedLogging) {
|
|
914
|
-
console.log(
|
|
915
|
-
`[${connectionId}] Rehandshake with new SNI: ${newSNI} (previously ${record.lockedDomain}). ` +
|
|
916
|
-
`New domain is allowed by different domain config rules, permitting connection reuse.`
|
|
917
|
-
);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// Update the domain config reference to the new one
|
|
921
|
-
if (allowed) {
|
|
922
|
-
record.domainConfig = newDomainConfig;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
if (allowed) {
|
|
928
|
-
// Update the locked domain to the new domain
|
|
929
|
-
record.lockedDomain = newSNI;
|
|
930
|
-
if (this.settings.enableDetailedLogging) {
|
|
931
|
-
console.log(
|
|
932
|
-
`[${connectionId}] Updated locked domain for connection from ${record.remoteIP} to: ${newSNI}`
|
|
933
|
-
);
|
|
934
|
-
}
|
|
935
|
-
} else {
|
|
936
|
-
// If we get here, either no matching domain config was found or the IP is not allowed
|
|
937
|
-
console.log(
|
|
938
|
-
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. ` +
|
|
939
|
-
`New domain not allowed by any rules. Terminating connection.`
|
|
940
|
-
);
|
|
941
|
-
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
942
|
-
}
|
|
943
|
-
} else if (newSNI && this.settings.enableDetailedLogging) {
|
|
944
|
-
console.log(
|
|
945
|
-
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
|
|
946
|
-
);
|
|
947
|
-
}
|
|
948
|
-
} catch (err) {
|
|
949
|
-
// Always allow the renegotiation to continue if we encounter an error
|
|
950
|
-
// This ensures Chrome can complete its TLS renegotiation
|
|
951
|
-
console.log(
|
|
952
|
-
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
});
|
|
957
|
-
}
|
|
1189
|
+
// Renegotiation detection is now handled before piping is established
|
|
1190
|
+
// This ensures the data listener receives all packets properly
|
|
958
1191
|
|
|
959
1192
|
// Set connection timeout with simpler logic
|
|
960
1193
|
if (record.cleanupTimer) {
|
|
@@ -1684,6 +1917,12 @@ export class PortProxy {
|
|
|
1684
1917
|
|
|
1685
1918
|
// Save domain config in connection record
|
|
1686
1919
|
connectionRecord.domainConfig = domainConfig;
|
|
1920
|
+
|
|
1921
|
+
// Always set the lockedDomain, even for non-SNI connections
|
|
1922
|
+
if (serverName) {
|
|
1923
|
+
connectionRecord.lockedDomain = serverName;
|
|
1924
|
+
console.log(`[${connectionId}] Locked connection to domain: ${serverName}`);
|
|
1925
|
+
}
|
|
1687
1926
|
|
|
1688
1927
|
// IP validation is skipped if allowedIPs is empty
|
|
1689
1928
|
if (domainConfig) {
|
|
@@ -1852,7 +2091,17 @@ export class PortProxy {
|
|
|
1852
2091
|
);
|
|
1853
2092
|
}
|
|
1854
2093
|
|
|
1855
|
-
|
|
2094
|
+
// Extract all TLS information including session resumption
|
|
2095
|
+
const sniInfo = extractSNIInfo(chunk, this.settings.enableTlsDebugLogging);
|
|
2096
|
+
|
|
2097
|
+
if (sniInfo?.isResumption && sniInfo.resumedDomain) {
|
|
2098
|
+
// This is a session resumption with a known domain
|
|
2099
|
+
serverName = sniInfo.resumedDomain;
|
|
2100
|
+
console.log(`[${connectionId}] TLS Session resumption detected for domain: ${serverName}`);
|
|
2101
|
+
} else {
|
|
2102
|
+
// Normal SNI extraction
|
|
2103
|
+
serverName = sniInfo?.serverName || '';
|
|
2104
|
+
}
|
|
1856
2105
|
}
|
|
1857
2106
|
|
|
1858
2107
|
// Lock the connection to the negotiated SNI.
|
|
@@ -2168,6 +2417,9 @@ export class PortProxy {
|
|
|
2168
2417
|
public async stop() {
|
|
2169
2418
|
console.log('PortProxy shutting down...');
|
|
2170
2419
|
this.isShuttingDown = true;
|
|
2420
|
+
|
|
2421
|
+
// Stop the session cleanup timer
|
|
2422
|
+
stopSessionCleanupTimer();
|
|
2171
2423
|
|
|
2172
2424
|
// Stop accepting new connections
|
|
2173
2425
|
const closeServerPromises: Promise<void>[] = this.netServers.map(
|