@push.rocks/smartproxy 19.3.6 → 19.3.9

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.
@@ -372,14 +372,14 @@ export class RouteConnectionHandler {
372
372
  initialChunk?: Buffer
373
373
  ): void {
374
374
  const connectionId = record.id;
375
- const action = route.action;
375
+ const action = route.action as IRouteAction;
376
376
 
377
377
  // Check if this route uses NFTables for forwarding
378
378
  if (action.forwardingEngine === 'nftables') {
379
379
  // NFTables handles packet forwarding at the kernel level
380
380
  // The application should NOT interfere with these connections
381
381
 
382
- // Just log the connection for monitoring purposes
382
+ // Log the connection for monitoring purposes
383
383
  if (this.settings.enableDetailedLogging) {
384
384
  console.log(
385
385
  `[${record.id}] NFTables forwarding (kernel-level): ` +
@@ -407,9 +407,14 @@ export class RouteConnectionHandler {
407
407
  );
408
408
  }
409
409
  }
410
-
411
- // For NFTables routes, continue processing the connection normally
412
- // since the packet forwarding happens transparently at the kernel level
410
+
411
+ // For NFTables routes, we should still track the connection but not interfere
412
+ // Mark the connection as using network proxy so it's cleaned up properly
413
+ record.usingNetworkProxy = true;
414
+
415
+ // We don't close the socket - just let it remain open
416
+ // The kernel-level NFTables rules will handle the actual forwarding
417
+ return;
413
418
  }
414
419
 
415
420
  // We should have a target configuration for forwarding
@@ -547,52 +552,74 @@ export class RouteConnectionHandler {
547
552
  }
548
553
  }
549
554
  } else {
550
- // No TLS settings - basic forwarding
551
- if (this.settings.enableDetailedLogging) {
552
- console.log(
553
- `[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}`
555
+ // No TLS settings - check if this port should use HttpProxy
556
+ const isHttpProxyPort = this.settings.useHttpProxy?.includes(record.localPort);
557
+
558
+ if (isHttpProxyPort && this.httpProxyBridge.getHttpProxy()) {
559
+ // Forward non-TLS connections to HttpProxy if configured
560
+ if (this.settings.enableDetailedLogging) {
561
+ console.log(
562
+ `[${connectionId}] Using HttpProxy for non-TLS connection on port ${record.localPort}`
563
+ );
564
+ }
565
+
566
+ this.httpProxyBridge.forwardToHttpProxy(
567
+ connectionId,
568
+ socket,
569
+ record,
570
+ initialChunk,
571
+ this.settings.httpProxyPort || 8443,
572
+ (reason) => this.connectionManager.initiateCleanupOnce(record, reason)
554
573
  );
555
- }
574
+ return;
575
+ } else {
576
+ // Basic forwarding
577
+ if (this.settings.enableDetailedLogging) {
578
+ console.log(
579
+ `[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}`
580
+ );
581
+ }
556
582
 
557
- // Get the appropriate host value
558
- let targetHost: string;
583
+ // Get the appropriate host value
584
+ let targetHost: string;
585
+
586
+ if (typeof action.target.host === 'function') {
587
+ // For function-based host, use the same routeContext created earlier
588
+ const hostResult = action.target.host(routeContext);
589
+ targetHost = Array.isArray(hostResult)
590
+ ? hostResult[Math.floor(Math.random() * hostResult.length)]
591
+ : hostResult;
592
+ } else {
593
+ // For static host value
594
+ targetHost = Array.isArray(action.target.host)
595
+ ? action.target.host[Math.floor(Math.random() * action.target.host.length)]
596
+ : action.target.host;
597
+ }
559
598
 
560
- if (typeof action.target.host === 'function') {
561
- // For function-based host, use the same routeContext created earlier
562
- const hostResult = action.target.host(routeContext);
563
- targetHost = Array.isArray(hostResult)
564
- ? hostResult[Math.floor(Math.random() * hostResult.length)]
565
- : hostResult;
566
- } else {
567
- // For static host value
568
- targetHost = Array.isArray(action.target.host)
569
- ? action.target.host[Math.floor(Math.random() * action.target.host.length)]
570
- : action.target.host;
571
- }
599
+ // Determine port - either function-based, static, or preserve incoming port
600
+ let targetPort: number;
601
+ if (typeof action.target.port === 'function') {
602
+ targetPort = action.target.port(routeContext);
603
+ } else if (action.target.port === 'preserve') {
604
+ targetPort = record.localPort;
605
+ } else {
606
+ targetPort = action.target.port;
607
+ }
572
608
 
573
- // Determine port - either function-based, static, or preserve incoming port
574
- let targetPort: number;
575
- if (typeof action.target.port === 'function') {
576
- targetPort = action.target.port(routeContext);
577
- } else if (action.target.port === 'preserve') {
578
- targetPort = record.localPort;
579
- } else {
580
- targetPort = action.target.port;
581
- }
609
+ // Update the connection record and context with resolved values
610
+ record.targetHost = targetHost;
611
+ record.targetPort = targetPort;
582
612
 
583
- // Update the connection record and context with resolved values
584
- record.targetHost = targetHost;
585
- record.targetPort = targetPort;
586
-
587
- return this.setupDirectConnection(
588
- socket,
589
- record,
590
- record.lockedDomain,
591
- initialChunk,
592
- undefined,
593
- targetHost,
594
- targetPort
595
- );
613
+ return this.setupDirectConnection(
614
+ socket,
615
+ record,
616
+ record.lockedDomain,
617
+ initialChunk,
618
+ undefined,
619
+ targetHost,
620
+ targetPort
621
+ );
622
+ }
596
623
  }
597
624
  }
598
625
 
@@ -657,6 +684,71 @@ export class RouteConnectionHandler {
657
684
  }, record);
658
685
  }
659
686
 
687
+ /**
688
+ * Setup improved error handling for the outgoing connection
689
+ */
690
+ private setupOutgoingErrorHandler(
691
+ connectionId: string,
692
+ targetSocket: plugins.net.Socket,
693
+ record: IConnectionRecord,
694
+ socket: plugins.net.Socket,
695
+ finalTargetHost: string,
696
+ finalTargetPort: number
697
+ ): void {
698
+ targetSocket.once('error', (err) => {
699
+ // This handler runs only once during the initial connection phase
700
+ const code = (err as any).code;
701
+ console.log(
702
+ `[${connectionId}] Connection setup error to ${finalTargetHost}:${finalTargetPort}: ${err.message} (${code})`
703
+ );
704
+
705
+ // Resume the incoming socket to prevent it from hanging
706
+ socket.resume();
707
+
708
+ // Log specific error types for easier debugging
709
+ if (code === 'ECONNREFUSED') {
710
+ console.log(
711
+ `[${connectionId}] Target ${finalTargetHost}:${finalTargetPort} refused connection. ` +
712
+ `Check if the target service is running and listening on that port.`
713
+ );
714
+ } else if (code === 'ETIMEDOUT') {
715
+ console.log(
716
+ `[${connectionId}] Connection to ${finalTargetHost}:${finalTargetPort} timed out. ` +
717
+ `Check network conditions, firewall rules, or if the target is too far away.`
718
+ );
719
+ } else if (code === 'ECONNRESET') {
720
+ console.log(
721
+ `[${connectionId}] Connection to ${finalTargetHost}:${finalTargetPort} was reset. ` +
722
+ `The target might have closed the connection abruptly.`
723
+ );
724
+ } else if (code === 'EHOSTUNREACH') {
725
+ console.log(
726
+ `[${connectionId}] Host ${finalTargetHost} is unreachable. ` +
727
+ `Check DNS settings, network routing, or firewall rules.`
728
+ );
729
+ } else if (code === 'ENOTFOUND') {
730
+ console.log(
731
+ `[${connectionId}] DNS lookup failed for ${finalTargetHost}. ` +
732
+ `Check your DNS settings or if the hostname is correct.`
733
+ );
734
+ }
735
+
736
+ // Clear any existing error handler after connection phase
737
+ targetSocket.removeAllListeners('error');
738
+
739
+ // Re-add the normal error handler for established connections
740
+ targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
741
+
742
+ if (record.outgoingTerminationReason === null) {
743
+ record.outgoingTerminationReason = 'connection_failed';
744
+ this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
745
+ }
746
+
747
+ // Clean up the connection
748
+ this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
749
+ });
750
+ }
751
+
660
752
  /**
661
753
  * Sets up a direct connection to the target
662
754
  */
@@ -702,108 +794,14 @@ export class RouteConnectionHandler {
702
794
  connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
703
795
  }
704
796
 
705
- // Create a safe queue for incoming data
706
- const dataQueue: Buffer[] = [];
707
- let queueSize = 0;
708
- let processingQueue = false;
709
- let drainPending = false;
710
- let pipingEstablished = false;
711
-
712
- // Pause the incoming socket to prevent buffer overflows
713
- socket.pause();
714
-
715
- // Function to safely process the data queue without losing events
716
- const processDataQueue = () => {
717
- if (processingQueue || dataQueue.length === 0 || pipingEstablished) return;
718
-
719
- processingQueue = true;
720
-
721
- try {
722
- // Process all queued chunks with the current active handler
723
- while (dataQueue.length > 0) {
724
- const chunk = dataQueue.shift()!;
725
- queueSize -= chunk.length;
726
-
727
- // Once piping is established, we shouldn't get here,
728
- // but just in case, pass to the outgoing socket directly
729
- if (pipingEstablished && record.outgoing) {
730
- record.outgoing.write(chunk);
731
- continue;
732
- }
733
-
734
- // Track bytes received
735
- record.bytesReceived += chunk.length;
736
-
737
- // Check for TLS handshake
738
- if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) {
739
- record.isTLS = true;
740
-
741
- if (this.settings.enableTlsDebugLogging) {
742
- console.log(
743
- `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
744
- );
745
- }
746
- }
747
-
748
- // Check if adding this chunk would exceed the buffer limit
749
- const newSize = record.pendingDataSize + chunk.length;
750
-
751
- if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
752
- console.log(
753
- `[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
754
- );
755
- socket.end(); // Gracefully close the socket
756
- this.connectionManager.initiateCleanupOnce(record, 'buffer_limit_exceeded');
757
- return;
758
- }
759
-
760
- // Buffer the chunk and update the size counter
761
- record.pendingData.push(Buffer.from(chunk));
762
- record.pendingDataSize = newSize;
763
- this.timeoutManager.updateActivity(record);
764
- }
765
- } finally {
766
- processingQueue = false;
767
-
768
- // If there's a pending drain and we've processed everything,
769
- // signal we're ready for more data if we haven't established piping yet
770
- if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
771
- drainPending = false;
772
- socket.resume();
773
- }
774
- }
775
- };
776
-
777
- // Unified data handler that safely queues incoming data
778
- const safeDataHandler = (chunk: Buffer) => {
779
- // If piping is already established, just let the pipe handle it
780
- if (pipingEstablished) return;
781
-
782
- // Add to our queue for orderly processing
783
- dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
784
- queueSize += chunk.length;
785
-
786
- // If queue is getting large, pause socket until we catch up
787
- if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
788
- socket.pause();
789
- drainPending = true;
790
- }
791
-
792
- // Process the queue
793
- processDataQueue();
794
- };
795
-
796
- // Add our safe data handler
797
- socket.on('data', safeDataHandler);
798
-
799
- // Add initial chunk to pending data if present
797
+ // Store initial data if provided
800
798
  if (initialChunk) {
801
799
  record.bytesReceived += initialChunk.length;
802
800
  record.pendingData.push(Buffer.from(initialChunk));
803
801
  record.pendingDataSize = initialChunk.length;
804
802
  }
805
803
 
806
- // Create the target socket but don't set up piping immediately
804
+ // Create the target socket
807
805
  const targetSocket = plugins.net.connect(connectionOptions);
808
806
  record.outgoing = targetSocket;
809
807
  record.outgoingStartTime = Date.now();
@@ -811,7 +809,7 @@ export class RouteConnectionHandler {
811
809
  // Apply socket optimizations
812
810
  targetSocket.setNoDelay(this.settings.noDelay);
813
811
 
814
- // Apply keep-alive settings to the outgoing connection as well
812
+ // Apply keep-alive settings if enabled
815
813
  if (this.settings.keepAlive) {
816
814
  targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
817
815
 
@@ -835,53 +833,15 @@ export class RouteConnectionHandler {
835
833
  }
836
834
  }
837
835
 
838
- // Setup specific error handler for connection phase
839
- targetSocket.once('error', (err) => {
840
- // This handler runs only once during the initial connection phase
841
- const code = (err as any).code;
842
- console.log(
843
- `[${connectionId}] Connection setup error to ${finalTargetHost}:${connectionOptions.port}: ${err.message} (${code})`
844
- );
845
-
846
- // Resume the incoming socket to prevent it from hanging
847
- socket.resume();
836
+ // Setup improved error handling for outgoing connection
837
+ this.setupOutgoingErrorHandler(connectionId, targetSocket, record, socket, finalTargetHost, finalTargetPort);
848
838
 
849
- if (code === 'ECONNREFUSED') {
850
- console.log(
851
- `[${connectionId}] Target ${finalTargetHost}:${connectionOptions.port} refused connection`
852
- );
853
- } else if (code === 'ETIMEDOUT') {
854
- console.log(
855
- `[${connectionId}] Connection to ${finalTargetHost}:${connectionOptions.port} timed out`
856
- );
857
- } else if (code === 'ECONNRESET') {
858
- console.log(
859
- `[${connectionId}] Connection to ${finalTargetHost}:${connectionOptions.port} was reset`
860
- );
861
- } else if (code === 'EHOSTUNREACH') {
862
- console.log(`[${connectionId}] Host ${finalTargetHost} is unreachable`);
863
- }
864
-
865
- // Clear any existing error handler after connection phase
866
- targetSocket.removeAllListeners('error');
867
-
868
- // Re-add the normal error handler for established connections
869
- targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
870
-
871
- if (record.outgoingTerminationReason === null) {
872
- record.outgoingTerminationReason = 'connection_failed';
873
- this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
874
- }
875
-
876
- // Route-based configuration doesn't use domain handlers
877
-
878
- // Clean up the connection
879
- this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
880
- });
881
-
882
- // Setup close handler
839
+ // Setup close handlers
883
840
  targetSocket.on('close', this.connectionManager.handleClose('outgoing', record));
884
841
  socket.on('close', this.connectionManager.handleClose('incoming', record));
842
+
843
+ // Setup error handlers for incoming socket
844
+ socket.on('error', this.connectionManager.handleError('incoming', record));
885
845
 
886
846
  // Handle timeouts with keep-alive awareness
887
847
  socket.on('timeout', () => {
@@ -947,19 +907,19 @@ export class RouteConnectionHandler {
947
907
 
948
908
  // Wait for the outgoing connection to be ready before setting up piping
949
909
  targetSocket.once('connect', () => {
910
+ if (this.settings.enableDetailedLogging) {
911
+ console.log(
912
+ `[${connectionId}] Connection established to target: ${finalTargetHost}:${finalTargetPort}`
913
+ );
914
+ }
915
+
950
916
  // Clear the initial connection error handler
951
917
  targetSocket.removeAllListeners('error');
952
918
 
953
919
  // Add the normal error handler for established connections
954
920
  targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
955
921
 
956
- // Process any remaining data in the queue before switching to piping
957
- processDataQueue();
958
-
959
- // Set up piping immediately
960
- pipingEstablished = true;
961
-
962
- // Flush all pending data to target
922
+ // Flush any pending data to target
963
923
  if (record.pendingData.length > 0) {
964
924
  const combinedData = Buffer.concat(record.pendingData);
965
925
 
@@ -982,52 +942,29 @@ export class RouteConnectionHandler {
982
942
  record.pendingDataSize = 0;
983
943
  }
984
944
 
985
- // Setup piping in both directions without any delays
945
+ // Immediately setup bidirectional piping - much simpler than manual data management
986
946
  socket.pipe(targetSocket);
987
947
  targetSocket.pipe(socket);
988
948
 
989
- // Resume the socket to ensure data flows
990
- socket.resume();
991
-
992
- // Process any data that might be queued in the interim
993
- if (dataQueue.length > 0) {
994
- // Write any remaining queued data directly to the target socket
995
- for (const chunk of dataQueue) {
996
- targetSocket.write(chunk);
997
- }
998
- // Clear the queue
999
- dataQueue.length = 0;
1000
- queueSize = 0;
1001
- }
949
+ // Track incoming data for bytes counting - do this after piping is set up
950
+ socket.on('data', (chunk: Buffer) => {
951
+ record.bytesReceived += chunk.length;
952
+ this.timeoutManager.updateActivity(record);
953
+ });
1002
954
 
1003
- if (this.settings.enableDetailedLogging) {
1004
- console.log(
1005
- `[${connectionId}] Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` +
1006
- `${
1007
- serverName
1008
- ? ` (SNI: ${serverName})`
1009
- : record.lockedDomain
1010
- ? ` (Domain: ${record.lockedDomain})`
1011
- : ''
1012
- }` +
1013
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
1014
- record.hasKeepAlive ? 'Yes' : 'No'
1015
- }`
1016
- );
1017
- } else {
1018
- console.log(
1019
- `Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` +
1020
- `${
1021
- serverName
1022
- ? ` (SNI: ${serverName})`
1023
- : record.lockedDomain
1024
- ? ` (Domain: ${record.lockedDomain})`
1025
- : ''
1026
- }`
1027
- );
1028
- }
955
+ // Log successful connection
956
+ console.log(
957
+ `Connection established: ${record.remoteIP} -> ${finalTargetHost}:${finalTargetPort}` +
958
+ `${
959
+ serverName
960
+ ? ` (SNI: ${serverName})`
961
+ : record.lockedDomain
962
+ ? ` (Domain: ${record.lockedDomain})`
963
+ : ''
964
+ }`
965
+ );
1029
966
 
1030
- // Add the renegotiation handler for SNI validation
967
+ // Add TLS renegotiation handler if needed
1031
968
  if (serverName) {
1032
969
  // Create connection info object for the existing connection
1033
970
  const connInfo = {
@@ -1055,11 +992,6 @@ export class RouteConnectionHandler {
1055
992
  console.log(
1056
993
  `[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
1057
994
  );
1058
- if (this.settings.allowSessionTicket === false) {
1059
- console.log(
1060
- `[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`
1061
- );
1062
- }
1063
995
  }
1064
996
  }
1065
997
 
@@ -1074,14 +1006,7 @@ export class RouteConnectionHandler {
1074
1006
  // Mark TLS handshake as complete for TLS connections
1075
1007
  if (record.isTLS) {
1076
1008
  record.tlsHandshakeComplete = true;
1077
-
1078
- if (this.settings.enableTlsDebugLogging) {
1079
- console.log(
1080
- `[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
1081
- );
1082
- }
1083
1009
  }
1084
1010
  });
1085
1011
  }
1086
- }
1087
-
1012
+ }
@@ -338,10 +338,19 @@ export class RouteManager extends plugins.EventEmitter {
338
338
 
339
339
  // Find the first matching route based on priority order
340
340
  for (const route of routesForPort) {
341
- // Check domain match if specified
342
- if (domain && !this.matchRouteDomain(route, domain)) {
343
- continue;
341
+ // Check domain match
342
+ // If the route has domain restrictions and we have a domain to check
343
+ if (route.match.domains) {
344
+ // If no domain was provided (non-TLS or no SNI), this route doesn't match
345
+ if (!domain) {
346
+ continue;
347
+ }
348
+ // If domain is provided but doesn't match the route's domains, skip
349
+ if (!this.matchRouteDomain(route, domain)) {
350
+ continue;
351
+ }
344
352
  }
353
+ // If route has no domain restrictions, it matches all domains
345
354
 
346
355
  // Check path match if specified in both route and request
347
356
  if (path && route.match.path) {