@push.rocks/smartproxy 3.37.0 → 3.37.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.
@@ -1,5 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import { NetworkProxy } from './classes.networkproxy.js';
3
+ import { SniHandler } from './classes.snihandler.js';
3
4
 
4
5
  /** Domain configuration with per-domain allowed port ranges */
5
6
  export interface IDomainConfig {
@@ -117,192 +118,8 @@ interface IConnectionRecord {
117
118
  domainSwitches?: number; // Number of times the domain has been switched on this connection
118
119
  }
119
120
 
120
- /**
121
- * Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
122
- * Enhanced for robustness and detailed logging.
123
- * @param buffer - Buffer containing the TLS ClientHello.
124
- * @param enableLogging - Whether to enable detailed logging.
125
- * @returns The server name if found, otherwise undefined.
126
- */
127
- function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
128
- try {
129
- // Check if buffer is too small for TLS
130
- if (buffer.length < 5) {
131
- if (enableLogging) console.log('Buffer too small for TLS header');
132
- return undefined;
133
- }
134
-
135
- // Check record type (has to be handshake - 22)
136
- const recordType = buffer.readUInt8(0);
137
- if (recordType !== 22) {
138
- if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
139
- return undefined;
140
- }
141
-
142
- // Check TLS version (has to be 3.1 or higher)
143
- const majorVersion = buffer.readUInt8(1);
144
- const minorVersion = buffer.readUInt8(2);
145
- if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
146
-
147
- // Check record length
148
- const recordLength = buffer.readUInt16BE(3);
149
- if (buffer.length < 5 + recordLength) {
150
- if (enableLogging)
151
- console.log(
152
- `Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`
153
- );
154
- return undefined;
155
- }
156
-
157
- let offset = 5;
158
- const handshakeType = buffer.readUInt8(offset);
159
- if (handshakeType !== 1) {
160
- if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
161
- return undefined;
162
- }
163
-
164
- offset += 4; // Skip handshake header (type + length)
165
-
166
- // Client version
167
- const clientMajorVersion = buffer.readUInt8(offset);
168
- const clientMinorVersion = buffer.readUInt8(offset + 1);
169
- if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
170
-
171
- offset += 2 + 32; // Skip client version and random
172
-
173
- // Session ID
174
- const sessionIDLength = buffer.readUInt8(offset);
175
- if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
176
- offset += 1 + sessionIDLength; // Skip session ID
177
-
178
- // Cipher suites
179
- if (offset + 2 > buffer.length) {
180
- if (enableLogging) console.log('Buffer too small for cipher suites length');
181
- return undefined;
182
- }
183
- const cipherSuitesLength = buffer.readUInt16BE(offset);
184
- if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
185
- offset += 2 + cipherSuitesLength; // Skip cipher suites
186
-
187
- // Compression methods
188
- if (offset + 1 > buffer.length) {
189
- if (enableLogging) console.log('Buffer too small for compression methods length');
190
- return undefined;
191
- }
192
- const compressionMethodsLength = buffer.readUInt8(offset);
193
- if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
194
- offset += 1 + compressionMethodsLength; // Skip compression methods
195
-
196
- // Extensions
197
- if (offset + 2 > buffer.length) {
198
- if (enableLogging) console.log('Buffer too small for extensions length');
199
- return undefined;
200
- }
201
- const extensionsLength = buffer.readUInt16BE(offset);
202
- if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
203
- offset += 2;
204
- const extensionsEnd = offset + extensionsLength;
205
-
206
- if (extensionsEnd > buffer.length) {
207
- if (enableLogging)
208
- console.log(
209
- `Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`
210
- );
211
- return undefined;
212
- }
213
-
214
- // Parse extensions
215
- while (offset + 4 <= extensionsEnd) {
216
- const extensionType = buffer.readUInt16BE(offset);
217
- const extensionLength = buffer.readUInt16BE(offset + 2);
218
-
219
- if (enableLogging)
220
- console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
221
-
222
- offset += 4;
223
-
224
- if (extensionType === 0x0000) {
225
- // SNI extension
226
- if (offset + 2 > buffer.length) {
227
- if (enableLogging) console.log('Buffer too small for SNI list length');
228
- return undefined;
229
- }
230
-
231
- const sniListLength = buffer.readUInt16BE(offset);
232
- if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
233
- offset += 2;
234
- const sniListEnd = offset + sniListLength;
235
-
236
- if (sniListEnd > buffer.length) {
237
- if (enableLogging)
238
- console.log(
239
- `Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`
240
- );
241
- return undefined;
242
- }
243
-
244
- while (offset + 3 < sniListEnd) {
245
- const nameType = buffer.readUInt8(offset++);
246
- const nameLen = buffer.readUInt16BE(offset);
247
- offset += 2;
248
-
249
- if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
250
-
251
- if (nameType === 0) {
252
- // host_name
253
- if (offset + nameLen > buffer.length) {
254
- if (enableLogging)
255
- console.log(
256
- `Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${
257
- buffer.length
258
- }`
259
- );
260
- return undefined;
261
- }
262
-
263
- const serverName = buffer.toString('utf8', offset, offset + nameLen);
264
- if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
265
- return serverName;
266
- }
267
-
268
- offset += nameLen;
269
- }
270
- break;
271
- } else {
272
- offset += extensionLength;
273
- }
274
- }
275
-
276
- if (enableLogging) console.log('No SNI extension found');
277
- return undefined;
278
- } catch (err) {
279
- console.log(`Error extracting SNI: ${err}`);
280
- return undefined;
281
- }
282
- }
283
-
284
- /**
285
- * Checks if a TLS record is a proper ClientHello message (more accurate than just checking record type)
286
- * @param buffer - Buffer containing the TLS record
287
- * @returns true if the buffer contains a proper ClientHello message
288
- */
289
- function isClientHello(buffer: Buffer): boolean {
290
- try {
291
- if (buffer.length < 9) return false; // Too small for a proper ClientHello
292
-
293
- // Check record type (has to be handshake - 22)
294
- if (buffer.readUInt8(0) !== 22) return false;
295
-
296
- // After the TLS record header (5 bytes), check the handshake type (1 for ClientHello)
297
- if (buffer.readUInt8(5) !== 1) return false;
298
-
299
- // Basic checks passed, this appears to be a ClientHello
300
- return true;
301
- } catch (err) {
302
- console.log(`Error checking for ClientHello: ${err}`);
303
- return false;
304
- }
305
- }
121
+ // SNI functions are now imported from SniHandler class
122
+ // No need for wrapper functions
306
123
 
307
124
  // Helper: Check if a port falls within any of the given port ranges
308
125
  const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
@@ -346,10 +163,7 @@ const generateConnectionId = (): string => {
346
163
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
347
164
  };
348
165
 
349
- // Helper: Check if a buffer contains a TLS handshake
350
- const isTlsHandshake = (buffer: Buffer): boolean => {
351
- return buffer.length > 0 && buffer[0] === 22; // ContentType.handshake
352
- };
166
+ // SNI functions are now imported from SniHandler class
353
167
 
354
168
  // Helper: Ensure timeout values don't exceed Node.js max safe integer
355
169
  const ensureSafeTimeout = (timeout: number): number => {
@@ -752,44 +566,104 @@ export class PortProxy {
752
566
  connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
753
567
  }
754
568
 
569
+ // Create a safe queue for incoming data using a Buffer array
570
+ // We'll use this to ensure we don't lose data during handler transitions
571
+ const dataQueue: Buffer[] = [];
572
+ let queueSize = 0;
573
+ let processingQueue = false;
574
+ let drainPending = false;
575
+
576
+ // Flag to track if we've switched to the final piping mechanism
577
+ // Once this is true, we no longer buffer data in dataQueue
578
+ let pipingEstablished = false;
579
+
755
580
  // Pause the incoming socket to prevent buffer overflows
581
+ // This ensures we control the flow of data until piping is set up
756
582
  socket.pause();
757
583
 
758
- // Temporary handler to collect data during connection setup
759
- const tempDataHandler = (chunk: Buffer) => {
760
- // Track bytes received
761
- record.bytesReceived += chunk.length;
584
+ // Function to safely process the data queue without losing events
585
+ const processDataQueue = () => {
586
+ if (processingQueue || dataQueue.length === 0 || pipingEstablished) return;
587
+
588
+ processingQueue = true;
589
+
590
+ try {
591
+ // Process all queued chunks with the current active handler
592
+ while (dataQueue.length > 0) {
593
+ const chunk = dataQueue.shift()!;
594
+ queueSize -= chunk.length;
595
+
596
+ // Once piping is established, we shouldn't get here,
597
+ // but just in case, pass to the outgoing socket directly
598
+ if (pipingEstablished && record.outgoing) {
599
+ record.outgoing.write(chunk);
600
+ continue;
601
+ }
602
+
603
+ // Track bytes received
604
+ record.bytesReceived += chunk.length;
762
605
 
763
- // Check for TLS handshake
764
- if (!record.isTLS && isTlsHandshake(chunk)) {
765
- record.isTLS = true;
606
+ // Check for TLS handshake
607
+ if (!record.isTLS && SniHandler.isTlsHandshake(chunk)) {
608
+ record.isTLS = true;
766
609
 
767
- if (this.settings.enableTlsDebugLogging) {
768
- console.log(
769
- `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
770
- );
771
- }
772
- }
610
+ if (this.settings.enableTlsDebugLogging) {
611
+ console.log(
612
+ `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
613
+ );
614
+ }
615
+ }
773
616
 
774
- // Check if adding this chunk would exceed the buffer limit
775
- const newSize = record.pendingDataSize + chunk.length;
617
+ // Check if adding this chunk would exceed the buffer limit
618
+ const newSize = record.pendingDataSize + chunk.length;
776
619
 
777
- if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
778
- console.log(
779
- `[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
780
- );
781
- socket.end(); // Gracefully close the socket
782
- return this.initiateCleanupOnce(record, 'buffer_limit_exceeded');
620
+ if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
621
+ console.log(
622
+ `[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
623
+ );
624
+ socket.end(); // Gracefully close the socket
625
+ this.initiateCleanupOnce(record, 'buffer_limit_exceeded');
626
+ return;
627
+ }
628
+
629
+ // Buffer the chunk and update the size counter
630
+ record.pendingData.push(Buffer.from(chunk));
631
+ record.pendingDataSize = newSize;
632
+ this.updateActivity(record);
633
+ }
634
+ } finally {
635
+ processingQueue = false;
636
+
637
+ // If there's a pending drain and we've processed everything,
638
+ // signal we're ready for more data if we haven't established piping yet
639
+ if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
640
+ drainPending = false;
641
+ socket.resume();
642
+ }
783
643
  }
644
+ };
784
645
 
785
- // Buffer the chunk and update the size counter
786
- record.pendingData.push(Buffer.from(chunk));
787
- record.pendingDataSize = newSize;
788
- this.updateActivity(record);
646
+ // Unified data handler that safely queues incoming data
647
+ const safeDataHandler = (chunk: Buffer) => {
648
+ // If piping is already established, just let the pipe handle it
649
+ if (pipingEstablished) return;
650
+
651
+ // Add to our queue for orderly processing
652
+ dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
653
+ queueSize += chunk.length;
654
+
655
+ // If queue is getting large, pause socket until we catch up
656
+ if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
657
+ socket.pause();
658
+ drainPending = true;
659
+ }
660
+
661
+ // Process the queue
662
+ processDataQueue();
789
663
  };
790
664
 
791
- // Add the temp handler to capture all incoming data during connection setup
792
- socket.on('data', tempDataHandler);
665
+ // Add our safe data handler
666
+ socket.on('data', safeDataHandler);
793
667
 
794
668
  // Add initial chunk to pending data if present
795
669
  if (initialChunk) {
@@ -962,56 +836,32 @@ export class PortProxy {
962
836
  // Add the normal error handler for established connections
963
837
  targetSocket.on('error', this.handleError('outgoing', record));
964
838
 
965
- // Remove temporary data handler
966
- socket.removeListener('data', tempDataHandler);
967
-
968
- // Flush all pending data to target
969
- if (record.pendingData.length > 0) {
970
- const combinedData = Buffer.concat(record.pendingData);
971
- targetSocket.write(combinedData, (err) => {
972
- if (err) {
973
- console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
974
- return this.initiateCleanupOnce(record, 'write_error');
975
- }
976
-
977
- // Now set up piping for future data and resume the socket
978
- socket.pipe(targetSocket);
979
- targetSocket.pipe(socket);
980
- socket.resume(); // Resume the socket after piping is established
981
-
982
- if (this.settings.enableDetailedLogging) {
983
- console.log(
984
- `[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
985
- `${
986
- serverName
987
- ? ` (SNI: ${serverName})`
988
- : domainConfig
989
- ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
990
- : ''
991
- }` +
992
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
993
- record.hasKeepAlive ? 'Yes' : 'No'
994
- }`
995
- );
996
- } else {
997
- console.log(
998
- `Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
999
- `${
1000
- serverName
1001
- ? ` (SNI: ${serverName})`
1002
- : domainConfig
1003
- ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
1004
- : ''
1005
- }`
1006
- );
1007
- }
1008
- });
1009
- } else {
1010
- // No pending data, so just set up piping
839
+ // Process any remaining data in the queue before switching to piping
840
+ processDataQueue();
841
+
842
+ // Setup function to establish piping - we'll use this after flushing data
843
+ const setupPiping = () => {
844
+ // Mark that we're switching to piping mode
845
+ pipingEstablished = true;
846
+
847
+ // Setup piping in both directions
1011
848
  socket.pipe(targetSocket);
1012
849
  targetSocket.pipe(socket);
1013
- socket.resume(); // Resume the socket after piping is established
1014
-
850
+
851
+ // Resume the socket to ensure data flows
852
+ socket.resume();
853
+
854
+ // Process any data that might be queued in the interim
855
+ if (dataQueue.length > 0) {
856
+ // Write any remaining queued data directly to the target socket
857
+ for (const chunk of dataQueue) {
858
+ targetSocket.write(chunk);
859
+ }
860
+ // Clear the queue
861
+ dataQueue.length = 0;
862
+ queueSize = 0;
863
+ }
864
+
1015
865
  if (this.settings.enableDetailedLogging) {
1016
866
  console.log(
1017
867
  `[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
@@ -1038,6 +888,23 @@ export class PortProxy {
1038
888
  }`
1039
889
  );
1040
890
  }
891
+ };
892
+
893
+ // Flush all pending data to target
894
+ if (record.pendingData.length > 0) {
895
+ const combinedData = Buffer.concat(record.pendingData);
896
+ targetSocket.write(combinedData, (err) => {
897
+ if (err) {
898
+ console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
899
+ return this.initiateCleanupOnce(record, 'write_error');
900
+ }
901
+
902
+ // Establish piping now that we've flushed the buffered data
903
+ setupPiping();
904
+ });
905
+ } else {
906
+ // No pending data, just establish piping immediately
907
+ setupPiping();
1041
908
  }
1042
909
 
1043
910
  // Clear the buffer now that we've processed it
@@ -1045,14 +912,15 @@ export class PortProxy {
1045
912
  record.pendingDataSize = 0;
1046
913
 
1047
914
  // Add the renegotiation handler for SNI validation with strict domain enforcement
915
+ // This will be called after we've established piping
1048
916
  if (serverName) {
1049
917
  // Define a handler for checking renegotiation with improved detection
1050
918
  const renegotiationHandler = (renegChunk: Buffer) => {
1051
919
  // Only process if this looks like a TLS ClientHello
1052
- if (isClientHello(renegChunk)) {
920
+ if (SniHandler.isClientHello(renegChunk)) {
1053
921
  try {
1054
922
  // Extract SNI from ClientHello
1055
- const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
923
+ const newSNI = SniHandler.extractSNIWithResumptionSupport(renegChunk, this.settings.enableTlsDebugLogging);
1056
924
 
1057
925
  // Skip if no SNI was found
1058
926
  if (!newSNI) return;
@@ -1081,8 +949,13 @@ export class PortProxy {
1081
949
  // Store the handler in the connection record so we can remove it during cleanup
1082
950
  record.renegotiationHandler = renegotiationHandler;
1083
951
 
1084
- // Add the listener
952
+ // The renegotiation handler is added when piping is established
953
+ // Making it part of setupPiping ensures proper sequencing of event handlers
1085
954
  socket.on('data', renegotiationHandler);
955
+
956
+ if (this.settings.enableDetailedLogging) {
957
+ console.log(`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`);
958
+ }
1086
959
  }
1087
960
 
1088
961
  // Set connection timeout with simpler logic
@@ -1242,13 +1115,16 @@ export class PortProxy {
1242
1115
  const bytesReceived = record.bytesReceived;
1243
1116
  const bytesSent = record.bytesSent;
1244
1117
 
1245
- // Remove the renegotiation handler if present
1246
- if (record.renegotiationHandler && record.incoming) {
1118
+ // Remove all data handlers (both standard and renegotiation) to make sure we clean up properly
1119
+ if (record.incoming) {
1247
1120
  try {
1248
- record.incoming.removeListener('data', record.renegotiationHandler);
1121
+ // Remove our safe data handler
1122
+ record.incoming.removeAllListeners('data');
1123
+
1124
+ // Reset the handler references
1249
1125
  record.renegotiationHandler = undefined;
1250
1126
  } catch (err) {
1251
- console.log(`[${record.id}] Error removing renegotiation handler: ${err}`);
1127
+ console.log(`[${record.id}] Error removing data handlers: ${err}`);
1252
1128
  }
1253
1129
  }
1254
1130
 
@@ -1644,7 +1520,7 @@ export class PortProxy {
1644
1520
  connectionRecord.hasReceivedInitialData = true;
1645
1521
 
1646
1522
  // Check if this looks like a TLS handshake
1647
- if (isTlsHandshake(chunk)) {
1523
+ if (SniHandler.isTlsHandshake(chunk)) {
1648
1524
  connectionRecord.isTLS = true;
1649
1525
 
1650
1526
  // Forward directly to NetworkProxy without SNI processing
@@ -1706,7 +1582,7 @@ export class PortProxy {
1706
1582
  this.updateActivity(connectionRecord);
1707
1583
 
1708
1584
  // Check for TLS handshake if this is the first chunk
1709
- if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
1585
+ if (!connectionRecord.isTLS && SniHandler.isTlsHandshake(chunk)) {
1710
1586
  connectionRecord.isTLS = true;
1711
1587
 
1712
1588
  if (this.settings.enableTlsDebugLogging) {
@@ -1714,7 +1590,7 @@ export class PortProxy {
1714
1590
  `[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
1715
1591
  );
1716
1592
  // Try to extract SNI and log detailed debug info
1717
- extractSNI(chunk, true);
1593
+ SniHandler.extractSNIWithResumptionSupport(chunk, true);
1718
1594
  }
1719
1595
  }
1720
1596
  });
@@ -1743,7 +1619,7 @@ export class PortProxy {
1743
1619
  connectionRecord.hasReceivedInitialData = true;
1744
1620
 
1745
1621
  // Check if this looks like a TLS handshake
1746
- const isTlsHandshakeDetected = initialChunk && isTlsHandshake(initialChunk);
1622
+ const isTlsHandshakeDetected = initialChunk && SniHandler.isTlsHandshake(initialChunk);
1747
1623
  if (isTlsHandshakeDetected) {
1748
1624
  connectionRecord.isTLS = true;
1749
1625
 
@@ -1912,7 +1788,7 @@ export class PortProxy {
1912
1788
  // Try to extract SNI
1913
1789
  let serverName = '';
1914
1790
 
1915
- if (isTlsHandshake(chunk)) {
1791
+ if (SniHandler.isTlsHandshake(chunk)) {
1916
1792
  connectionRecord.isTLS = true;
1917
1793
 
1918
1794
  if (this.settings.enableTlsDebugLogging) {
@@ -1921,7 +1797,7 @@ export class PortProxy {
1921
1797
  );
1922
1798
  }
1923
1799
 
1924
- serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
1800
+ serverName = SniHandler.extractSNIWithResumptionSupport(chunk, this.settings.enableTlsDebugLogging) || '';
1925
1801
  }
1926
1802
 
1927
1803
  // Lock the connection to the negotiated SNI.