@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartproxy",
3
- "version": "3.30.7",
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",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '3.30.7',
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
  }
@@ -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 The server name if found, otherwise undefined.
176
+ * @returns An object containing SNI and session information, or undefined if parsing fails.
109
177
  */
110
- function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
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
- return serverName;
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
- return undefined;
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
- // Update activity on data transfer
701
+
702
+ // Special handler for TLS handshake detection with NetworkProxy
506
703
  socket.on('data', (chunk: Buffer) => {
507
- this.updateActivity(record);
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
- // ContentType.handshake
512
- if (this.settings.enableDetailedLogging) {
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
- // No pending data, so just set up piping
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
- // Add the renegotiation listener for SNI validation
852
- if (serverName) {
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
- serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
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(