Compare commits

...

89 Commits

Author SHA1 Message Date
156abbf5b4 3.41.3
Some checks failed
Default (tags) / security (push) Failing after 10m42s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-12 09:56:21 +00:00
1a90566622 fix(TLS/SNI): Improve TLS session resumption handling and logging. Now, session resumption attempts are always logged with details, and connections without a proper SNI are rejected when allowSessionTicket is disabled. In addition, empty SNI extensions are explicitly treated as missing, ensuring stricter and more consistent TLS handshake validation. 2025-03-12 09:56:21 +00:00
b48b90d613 3.41.2
Some checks failed
Default (tags) / security (push) Successful in 28s
Default (tags) / test (push) Failing after 1m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 19:41:04 +00:00
124f8d48b7 fix(SniHandler): Refactor hasSessionResumption to return detailed session resumption info 2025-03-11 19:41:04 +00:00
b2a57ada5d 3.41.1
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 19:38:41 +00:00
62a3e1f4b7 fix(SniHandler): Improve TLS SNI session resumption handling: connections containing a session ticket are now only rejected when no SNI is present and allowSessionTicket is disabled. Updated return values and logging for clearer resumption detection. 2025-03-11 19:38:41 +00:00
3a1485213a 3.41.0
Some checks failed
Default (tags) / security (push) Failing after 10m42s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 19:31:20 +00:00
9dbf6fdeb5 feat(PortProxy/TLS): Add allowSessionTicket option to control TLS session ticket handling 2025-03-11 19:31:20 +00:00
9496dd5336 3.40.0
Some checks failed
Default (tags) / security (push) Failing after 11m44s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 18:05:20 +00:00
29d28fba93 feat(SniHandler): Add session cache support and tab reactivation detection to improve SNI extraction in TLS handshakes 2025-03-11 18:05:20 +00:00
8196de4fa3 3.39.0
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:50:57 +00:00
6fddafe9fd feat(PortProxy): Add domain-specific NetworkProxy integration support to PortProxy 2025-03-11 17:50:56 +00:00
1e89062167 3.38.2
Some checks failed
Default (tags) / security (push) Successful in 22s
Default (tags) / test (push) Failing after 1m11s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:38:32 +00:00
21a24fd95b fix(core): No code changes detected; bumping patch version for consistency. 2025-03-11 17:38:32 +00:00
03ef5e7f6e 3.38.1
Some checks failed
Default (tags) / security (push) Successful in 21s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:37:43 +00:00
415b82a84a fix(PortProxy): Improve SNI extraction handling in PortProxy by passing explicit connection info to extractSNIWithResumptionSupport for better TLS renegotiation and debug logging. 2025-03-11 17:37:43 +00:00
f304cc67b4 3.38.0
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:33:31 +00:00
0e12706176 feat(SniHandler): Enhance SNI extraction to support fragmented ClientHello messages, TLS 1.3 early data, and improved PSK parsing 2025-03-11 17:33:31 +00:00
6daf4c914d 3.37.3
Some checks failed
Default (tags) / security (push) Failing after 13m6s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 17:23:57 +00:00
36e4341315 fix(snihandler): Enhance SNI extraction to support TLS 1.3 PSK-based session resumption by adding a dedicated extractSNIFromPSKExtension method and improved logging for session resumption indicators. 2025-03-11 17:23:57 +00:00
474134d29c 3.37.2
Some checks failed
Default (tags) / security (push) Successful in 20s
Default (tags) / test (push) Failing after 1m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:05:15 +00:00
43378becd2 fix(PortProxy): Improve buffering and data handling during connection setup in PortProxy to prevent data loss 2025-03-11 17:05:15 +00:00
5ba8eb778f 3.37.1
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 17:01:07 +00:00
87d26c86a1 fix(PortProxy/SNI): Refactor SNI extraction in PortProxy to use the dedicated SniHandler class 2025-03-11 17:01:07 +00:00
d81cf94876 3.37.0
Some checks failed
Default (tags) / security (push) Failing after 10m56s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 12:56:04 +00:00
8d06f1533e feat(portproxy): Add ACME certificate management options to PortProxy, update ACME settings handling, and bump dependency versions 2025-03-11 12:56:03 +00:00
223be61c8d 3.35.0 2025-03-11 12:45:55 +00:00
6a693f4d86 feat(NetworkProxy): Integrate Port80Handler for automatic ACME certificate management
- Add ACME certificate management capabilities to NetworkProxy
- Implement automatic certificate issuance and renewal
- Add SNI support for serving the correct certificates
- Create certificate storage and caching system
- Enable dynamic certificate issuance for new domains
- Support automatic HTTP-to-HTTPS redirects for secured domains

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 12:45:22 +00:00
27a2bcb556 feat(NetworkProxy): Add support for array-based destinations and integration with PortProxy
- Update NetworkProxy to support new IReverseProxyConfig interface with destinationIps[] and destinationPorts[]
- Add load balancing with round-robin selection of destination endpoints
- Create automatic conversion of PortProxy domain configs to NetworkProxy configs
- Implement backward compatibility to ensure tests continue to work

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 12:34:24 +00:00
0674ca7163 3.34.0
Some checks failed
Default (tags) / security (push) Failing after 12m28s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 11:34:29 +00:00
e31c84493f feat(core): Improve wildcard domain matching and enhance NetworkProxy integration in PortProxy. Added support for TLD wildcards and complex wildcard patterns in the router, and refactored TLS renegotiation handling for stricter SNI enforcement. 2025-03-11 11:34:29 +00:00
d2ad659d37 3.33.0
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 14m16s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 09:57:06 +00:00
df7a12041e feat(portproxy): Add browser-friendly mode and SNI renegotiation configuration options to PortProxy 2025-03-11 09:57:06 +00:00
2b69150545 3.32.2
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 09:12:40 +00:00
85cc57ae10 fix(PortProxy): Simplify TLS handshake SNI extraction and update timeout settings in PortProxy for improved maintainability and reliability. 2025-03-11 09:12:40 +00:00
e021b66898 3.32.1
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 1m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 04:39:17 +00:00
865d21b36a fix(portproxy): Relax TLS handshake and connection timeout settings for improved stability in chained proxy scenarios; update TLS session cache defaults and add keep-alive flags to connection records. 2025-03-11 04:39:17 +00:00
58ba0d9362 3.32.0
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 1m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 04:24:29 +00:00
ccccc5b8c8 feat(PortProxy): Enhance TLS session cache, SNI extraction, and chained proxy support in PortProxy. Improve handling of multiple and fragmented TLS records, and add new configuration options (isChainedProxy, chainPosition, aggressiveTlsRefresh, tlsSessionCache) for robust TLS certificate refresh. 2025-03-11 04:24:29 +00:00
d8466a866c 3.31.2
Some checks failed
Default (tags) / security (push) Successful in 28s
Default (tags) / test (push) Failing after 1m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 03:56:09 +00:00
119b643690 fix(PortProxy): Improve SNI renegotiation handling by adding flexible domain configuration matching on rehandshake and session resumption events. 2025-03-11 03:56:09 +00:00
98f1e0df4c 3.31.1
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 1m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 03:48:10 +00:00
d6022c8f8a fix(PortProxy): Improve TLS handshake buffering and enhance debug logging for SNI forwarding in PortProxy 2025-03-11 03:48:10 +00:00
0ea0f02428 fix(PortProxy): Improve connection reliability for initial and resumed TLS sessions
Added enhanced connection handling to fix issues with both initial connections and TLS session resumption:

1. Improved debugging for connection setup with detailed logging
2. Added explicit timeout for backend connections to prevent hanging connections
3. Enhanced error recovery for connection failures with faster client notification
4. Added detailed session tracking to maintain domain context across TLS sessions
5. Fixed handling of TLS renegotiation with improved activity timestamp updates

This should address the issue where initial connections may fail but subsequent retries succeed,
as well as ensuring proper certificate selection for resumed TLS sessions.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 03:33:03 +00:00
e452f55203 3.31.0
Some checks failed
Default (tags) / security (push) Successful in 35s
Default (tags) / test (push) Failing after 1m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 03:16:04 +00:00
55f25f1976 feat(PortProxy): Improve TLS handshake SNI extraction and add session resumption tracking in PortProxy 2025-03-11 03:16:04 +00:00
98b7f3ed7f 3.30.8
Some checks failed
Default (tags) / security (push) Failing after 11m56s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-11 02:50:01 +00:00
cb83caeafd fix(core): No changes in this commit. 2025-03-11 02:50:01 +00:00
7850a80452 fix(PortProxy): Fix TypeScript errors by using correct variable names
Fixed TypeScript errors caused by using 'connectionRecord' instead of 'record' in TLS renegotiation handlers.
The variable name mistake occurred when moving and restructuring the TLS handshake detection code.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 02:47:57 +00:00
ef8f583a90 fix(PortProxy): Move TLS renegotiation detection before socket piping
Fundamentally restructured TLS renegotiation handling to ensure handshake packets are properly detected. The previous implementation attached event handlers after pipe() was established, which might have caused handshake packets to bypass detection. Key changes:

1. Moved renegotiation detection before pipe() to ensure all TLS handshake packets are detected
2. Added explicit lockedDomain setting for all SNI connections
3. Simplified the NetworkProxy TLS handshake detection
4. Removed redundant data handlers that could interfere with each other

These changes should make renegotiation detection more reliable regardless of how Node.js internal pipe() implementation handles data events.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 02:45:51 +00:00
2bdd6f8c1f fix(PortProxy): Update activity timestamp during TLS renegotiation to prevent connection timeouts
Ensures that TLS renegotiation packets properly update the connection's activity timestamp even when no SNI is present or when there are errors processing the renegotiation. This prevents connections from being closed due to inactivity during legitimate TLS renegotiation.

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 02:40:08 +00:00
99d28eafd1 3.30.7
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 02:25:59 +00:00
788b444fcc fix(PortProxy): Improve TLS renegotiation SNI handling by first checking if the new SNI is allowed under the existing domain config. If not, attempt to find an alternative domain config and update the locked domain accordingly; otherwise, terminate the connection on SNI mismatch. 2025-03-11 02:25:58 +00:00
4225abe3c4 3.30.6
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-11 02:18:56 +00:00
74fdb58f84 fix(PortProxy): Improve TLS renegotiation handling in PortProxy by validating the new SNI against allowed domain configurations. If the new SNI is permitted based on existing IP rules, update the locked domain to allow connection reuse; otherwise, terminate the connection to prevent misrouting. 2025-03-11 02:18:56 +00:00
bffdaffe39 3.30.5
Some checks failed
Default (tags) / security (push) Successful in 20s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-10 22:36:28 +00:00
67a4228518 fix(internal): No uncommitted changes detected; project files and tests remain unchanged. 2025-03-10 22:36:28 +00:00
681209f2e1 3.30.4
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 1m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-10 22:35:34 +00:00
c415a6c361 fix(PortProxy): Fix TLS renegotiation handling and adjust TLS keep-alive timeouts in PortProxy implementation 2025-03-10 22:35:34 +00:00
009e3c4f0e 3.30.3
Some checks failed
Default (tags) / security (push) Failing after 14m48s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-10 22:07:12 +00:00
f9c42975dc fix(classes.portproxy.ts): Simplify timeout management in PortProxy and fix chained proxy certificate refresh issues 2025-03-10 22:07:12 +00:00
feef949afe 3.30.2
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 1m10s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-10 14:15:03 +00:00
8d3b07b1e6 fix(classes.portproxy.ts): Adjust TLS keep-alive timeout to refresh certificate context. 2025-03-10 14:15:03 +00:00
51fe935f1f 3.30.1
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Failing after 1m9s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-10 14:13:57 +00:00
146fac73cf fix(PortProxy): Improve TLS keep-alive management and fix whitespace formatting 2025-03-10 14:13:56 +00:00
4465cac807 3.30.0
Some checks failed
Default (tags) / security (push) Failing after 16m2s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-08 12:40:55 +00:00
9d7ed21cba feat(PortProxy): Add advanced TLS keep-alive handling and system sleep detection 2025-03-08 12:40:55 +00:00
54fbe5beac 3.29.3
Some checks failed
Default (tags) / security (push) Successful in 19s
Default (tags) / test (push) Failing after 1m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 15:50:25 +00:00
0704853fa2 fix(core): Fix functional errors in the proxy setup and enhance pnpm configuration 2025-03-07 15:50:25 +00:00
8cf22ee38b 3.29.2
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 48s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 15:46:34 +00:00
f28e68e487 fix(PortProxy): Fix test for PortProxy handling of custom IPs in Docker/CI environments. 2025-03-07 15:46:34 +00:00
499aed19f6 3.29.1
Some checks failed
Default (tags) / security (push) Successful in 29s
Default (tags) / test (push) Failing after 50s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 14:34:49 +00:00
618b6fe2d1 fix(readme): Update readme for IPTablesProxy options 2025-03-07 14:34:49 +00:00
d6027c11c1 3.29.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 49s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 14:30:38 +00:00
bbdea52677 feat(IPTablesProxy): Enhanced IPTablesProxy with multi-port and IPv6 support 2025-03-07 14:30:38 +00:00
d8585975a8 3.28.6
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 49s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-07 11:16:45 +00:00
98c61cccbb fix(PortProxy): Adjust default timeout settings and enhance keep-alive connection handling in PortProxy. 2025-03-07 11:16:44 +00:00
b3dcc0ae22 3.28.5 2025-03-07 02:55:19 +00:00
b96d7dec98 fix(core): Ensure proper resource cleanup during server shutdown. 2025-03-07 02:55:19 +00:00
0d0a1c740b 3.28.4 2025-03-07 02:54:34 +00:00
9bd87b8437 fix(router): Improve path pattern matching and hostname prioritization in router 2025-03-07 02:54:34 +00:00
0e281b3243 3.28.3 2025-03-06 23:08:57 +00:00
a14b7802c4 fix(PortProxy): Ensure timeout values are within Node.js safe limits 2025-03-06 23:08:57 +00:00
138900ca8b 3.28.2 2025-03-06 23:00:24 +00:00
cb6c2503e2 fix(portproxy): Adjust safe timeout defaults in PortProxy to prevent overflow issues. 2025-03-06 23:00:24 +00:00
f3fd903231 3.28.1 2025-03-06 22:56:19 +00:00
0e605d9a9d fix(PortProxy): Improved code formatting and readability in PortProxy class by adjusting spacing and comments. 2025-03-06 22:56:18 +00:00
1718a3b2f2 3.28.0 2025-03-06 08:36:19 +00:00
568f77e65b feat(router): Add detailed routing tests and refactor ProxyRouter for improved path matching 2025-03-06 08:36:19 +00:00
13 changed files with 6945 additions and 2000 deletions

View File

@ -1,5 +1,312 @@
# Changelog
## 2025-03-12 - 3.41.3 - fix(TLS/SNI)
Improve TLS session resumption handling and logging. Now, session resumption attempts are always logged with details, and connections without a proper SNI are rejected when allowSessionTicket is disabled. In addition, empty SNI extensions are explicitly treated as missing, ensuring stricter and more consistent TLS handshake validation.
- Always log session resumption in both renegotiation and initial ClientHello processing.
- Terminate connections that attempt session resumption without SNI when allowSessionTicket is false.
- Treat empty SNI extensions as absence of SNI to improve consistency in TLS handshake processing.
## 2025-03-11 - 3.41.2 - fix(SniHandler)
Refactor hasSessionResumption to return detailed session resumption info
- Changed the return type of hasSessionResumption from boolean to an object with properties isResumption and hasSNI
- Updated early return conditions to return { isResumption: false, hasSNI: false } when buffer is too short or invalid
- Modified corresponding documentation to reflect the new return type
## 2025-03-11 - 3.41.1 - fix(SniHandler)
Improve TLS SNI session resumption handling: connections containing a session ticket are now only rejected when no SNI is present and allowSessionTicket is disabled. Updated return values and logging for clearer resumption detection.
- Changed SniHandler.hasSessionResumption to return an object with 'isResumption' and 'hasSNI' flags.
- Adjusted PortProxy logic to only terminate connections when a session ticket is detected without an accompanying SNI (when allowSessionTicket is false).
- Enhanced debug logging to clearly differentiate between session resumption scenarios with and without SNI.
## 2025-03-11 - 3.41.0 - feat(PortProxy/TLS)
Add allowSessionTicket option to control TLS session ticket handling
- Introduce 'allowSessionTicket' flag (default true) in PortProxy settings to enable or disable TLS session resumption via session tickets.
- Update SniHandler with a new hasSessionResumption method to detect session ticket and PSK extensions in ClientHello messages.
- Force connection cleanup during renegotiation and initial handshake when allowSessionTicket is set to false and a session ticket is detected.
## 2025-03-11 - 3.40.0 - feat(SniHandler)
Add session cache support and tab reactivation detection to improve SNI extraction in TLS handshakes
- Introduce a session cache mechanism to store and retrieve cached SNI values based on client IP (and optionally client random) to better handle tab reactivation scenarios.
- Implement functions to initialize, update, and clean up the session cache for TLS ClientHello messages.
- Enhance SNI extraction logic to check for tab reactivation handshakes and to return cached SNI for resumed connections or 0-RTT scenarios.
- Update PSK extension handling to safely skip over obfuscated ticket age bytes.
## 2025-03-11 - 3.39.0 - feat(PortProxy)
Add domain-specific NetworkProxy integration support to PortProxy
- Introduced new properties 'useNetworkProxy' and 'networkProxyPort' in domain configurations.
- Updated forwardToNetworkProxy to accept an optional custom proxy port parameter.
- Enhanced TLS handshake processing to extract SNI and, if a matching domain config specifies NetworkProxy usage, forward the connection using the domain-specific port.
- Refined connection routing logic to check for domain-specific NetworkProxy settings before falling back to default behavior.
## 2025-03-11 - 3.38.2 - fix(core)
No code changes detected; bumping patch version for consistency.
## 2025-03-11 - 3.38.1 - fix(PortProxy)
Improve SNI extraction handling in PortProxy by passing explicit connection info to extractSNIWithResumptionSupport for better TLS renegotiation and debug logging.
- In the renegotiation handler, create and pass a connection info object (sourceIp, sourcePort, destIp, destPort) instead of a boolean flag.
- Update the TLS handshake processing to construct a connection info object for detailed SNI extraction and logging.
- Enhance consistency by using processTlsPacket with cached SNI hints during fallback.
## 2025-03-11 - 3.38.0 - feat(SniHandler)
Enhance SNI extraction to support fragmented ClientHello messages, TLS 1.3 early data, and improved PSK parsing
- Added isTlsApplicationData method for detecting TLS application data packets
- Implemented handleFragmentedClientHello to buffer and reassemble fragmented ClientHello messages
- Extended extractSNIWithResumptionSupport to accept connection information and use reassembled data
- Added detection for TLS 1.3 early data (0-RTT) in the ClientHello, supporting session resumption scenarios
- Improved logging and heuristics for handling potential connection racing in modern browsers
## 2025-03-11 - 3.37.3 - fix(snihandler)
Enhance SNI extraction to support TLS 1.3 PSK-based session resumption by adding a dedicated extractSNIFromPSKExtension method and improved logging for session resumption indicators.
- Defined TLS_PSK_EXTENSION_TYPE and TLS_PSK_KE_MODES_EXTENSION_TYPE constants.
- Added extractSNIFromPSKExtension method to handle ClientHello messages containing PSK identities.
- Improved logging to indicate when session resumption indicators (ticket or PSK) are present but no standard SNI is found.
- Enhanced extractSNIWithResumptionSupport to attempt PSK extraction if standard SNI extraction fails.
## 2025-03-11 - 3.37.2 - fix(PortProxy)
Improve buffering and data handling during connection setup in PortProxy to prevent data loss
- Added a safeDataHandler and processDataQueue to buffer incoming data reliably during the TLS handshake phase
- Introduced a queue with pause/resume logic to avoid exceeding maxPendingDataSize and ensure all pending data is flushed before piping begins
- Refactored the piping setup to install the renegotiation handler only after proper data flushing
## 2025-03-11 - 3.37.1 - fix(PortProxy/SNI)
Refactor SNI extraction in PortProxy to use the dedicated SniHandler class
- Removed local SNI extraction and handshake detection functions from classes.portproxy.ts
- Introduced a standalone SniHandler class in ts/classes.snihandler.ts for robust SNI extraction and improved logging
- Replaced inlined calls to isTlsHandshake and extractSNI with calls to SniHandler methods
- Ensured consistency in handling TLS ClientHello messages across the codebase
## 2025-03-11 - 3.37.0 - feat(portproxy)
Add ACME certificate management options to PortProxy, update ACME settings handling, and bump dependency versions
- Bumped version in package.json from 3.34.0 to 3.36.0 and updated commitinfo accordingly
- Updated dependencies: @push.rocks/tapbundle to ^5.5.10, @types/node to ^22.13.10, and @tsclass/tsclass to ^5.0.0
- Added ACME certificate management configuration to PortProxy settings (acme options, updateAcmeSettings, requestCertificate)
- Enhanced sync of domain configs to NetworkProxy with fallback for missing default certificates
## 2025-03-11 - 3.34.0 - feat(core)
Improve wildcard domain matching and enhance NetworkProxy integration in PortProxy. Added support for TLD wildcards and complex wildcard patterns in the router, and refactored TLS renegotiation handling for stricter SNI enforcement.
- Added support for TLD wildcard matching (e.g., 'example.*') to improve domain routing.
- Implemented complex wildcard pattern matching (e.g., '*.lossless*') in the router.
- Enhanced NetworkProxy integration by initializing a single NetworkProxy instance and forwarding TLS connections accordingly.
- Refactored TLS renegotiation handling to terminate connections on SNI mismatch for stricter enforcement.
- Updated tests to cover the new wildcard matching scenarios.
## 2025-03-11 - 3.33.0 - feat(portproxy)
Add browser-friendly mode and SNI renegotiation configuration options to PortProxy
- Introduce new properties: browserFriendlyMode (default true) to optimize handling for browser connections.
- Add allowRenegotiationWithDifferentSNI (default false) to enable or disable SNI changes during renegotiation.
- Include relatedDomainPatterns to define patterns for related domains that can share connections.
- Update TypeScript interfaces and internal renegotiation logic to support these options.
## 2025-03-11 - 3.32.2 - fix(PortProxy)
Simplify TLS handshake SNI extraction and update timeout settings in PortProxy for improved maintainability and reliability.
- Removed legacy and deprecated fields related to chained proxy configurations (isChainedProxy, chainPosition, aggressiveTlsRefresh).
- Refactored the extractSNI functions to use a simpler, more robust approach for TLS ClientHello processing.
- Adjusted default timeout and keep-alive settings to more standard values (e.g. initialDataTimeout set to 60s, socketTimeout to 1h).
- Eliminated redundant TLS session cache and deep TLS refresh logic.
- Improved logging and error handling during connection setup and renegotiation phases.
## 2025-03-11 - 3.32.1 - fix(portproxy)
Relax TLS handshake and connection timeout settings for improved stability in chained proxy scenarios; update TLS session cache defaults and add keep-alive flags to connection records.
- Increased TLS session cache maximum entries from 10,000 to 20,000, expiry time from 24 hours to 7 days, and cleanup interval from 10 minutes to 30 minutes
- Relaxed socket timeouts: standalone connections now use up to 6 hours, with chained proxies adjusted for 56 hours based on proxy position
- Updated inactivity, connection, and initial handshake timeouts to provide a more relaxed behavior under high-traffic chained proxy scenarios
- Increased keepAliveInitialDelay from 10 seconds to 30 seconds and introduced separate incoming and outgoing keep-alive flags
- Enhanced TLS renegotiation handling with more detailed logging and temporary processing flags to avoid duplicate processing
- Updated NetworkProxy integration to use optimized connection settings and more aggressive application-level keep-alive probes
## 2025-03-11 - 3.32.0 - feat(PortProxy)
Enhance TLS session cache, SNI extraction, and chained proxy support in PortProxy. Improve handling of multiple and fragmented TLS records, and add new configuration options (isChainedProxy, chainPosition, aggressiveTlsRefresh, tlsSessionCache) for robust TLS certificate refresh.
- Implement TlsSessionCache with configurable cleanup, eviction, and statistics.
- Improve extractSNIInfo to process multiple TLS records and partial handshake data.
- Add new settings to detect chained proxy scenarios and adjust timeouts accordingly.
- Enhance TLS state refresh with aggressive probing and deep refresh sequence.
## 2025-03-11 - 3.31.2 - fix(PortProxy)
Improve SNI renegotiation handling by adding flexible domain configuration matching on rehandshake and session resumption events.
- When a rehandshake is detected with a changed SNI, first check existing domain config rules and log if allowed.
- If the exact domain config is not found, additionally attempt flexible matching using parent domain and wildcard patterns.
- For resumed sessions, try an exact match first and then use fallback logic to select a similar domain config based on matching target IP.
- Enhanced logging added to help diagnose missing or mismatched domain configurations.
## 2025-03-11 - 3.31.1 - fix(PortProxy)
Improve TLS handshake buffering and enhance debug logging for SNI forwarding in PortProxy
- Explicitly copy the initial TLS handshake data to prevent mutation before buffering
- Log buffered TLS handshake data with SNI information for better diagnostics
- Add detailed error logs on TLS connection failures, including server and domain config status
- Output additional debug messages during ClientHello forwarding to verify proper TLS handshake processing
## 2025-03-11 - 3.31.0 - feat(PortProxy)
Improve TLS handshake SNI extraction and add session resumption tracking in PortProxy
- Added ITlsSessionInfo interface and a global tlsSessionCache to track TLS session IDs for session resumption
- Implemented a cleanup timer for the TLS session cache with startSessionCleanupTimer and stopSessionCleanupTimer
- Enhanced extractSNIInfo to return detailed SNI information including session IDs, ticket details, and resumption status
- Updated renegotiation handlers to use extractSNIInfo for proper SNI extraction during TLS rehandshake
## 2025-03-11 - 3.30.8 - fix(core)
No changes in this commit.
## 2025-03-11 - 3.30.7 - fix(PortProxy)
Improve TLS renegotiation SNI handling by first checking if the new SNI is allowed under the existing domain config. If not, attempt to find an alternative domain config and update the locked domain accordingly; otherwise, terminate the connection on SNI mismatch.
- Added a preliminary check against the original domain config to allow re-handshakes if the new SNI matches allowed patterns.
- If the original config does not allow, search for an alternative domain config and validate IP rules.
- Update the locked domain when allowed, ensuring connection reuse with valid certificate context.
- Terminate the connection if no suitable domain config is found or IP restrictions are violated.
## 2025-03-11 - 3.30.6 - fix(PortProxy)
Improve TLS renegotiation handling in PortProxy by validating the new SNI against allowed domain configurations. If the new SNI is permitted based on existing IP rules, update the locked domain to allow connection reuse; otherwise, terminate the connection to prevent misrouting.
- Added logic to check if a new SNI during renegotiation is allowed by comparing IP rules from the matching domain configuration.
- Updated detailed logging to indicate when a valid SNI change is accepted and when it results in a mismatch termination.
## 2025-03-10 - 3.30.5 - fix(internal)
No uncommitted changes detected; project files and tests remain unchanged.
## 2025-03-10 - 3.30.4 - fix(PortProxy)
Fix TLS renegotiation handling and adjust TLS keep-alive timeouts in PortProxy implementation
- Allow TLS renegotiation data without an explicit SNI extraction to pass through, ensuring valid renegotiations are not dropped (critical for Chrome).
- Update TLS keep-alive timeout from an aggressive 30 minutes to a more generous 4 hours to reduce unnecessary reconnections.
- Increase inactivity thresholds for TLS connections from 20 minutes to 2 hours with an additional verification interval extended from 5 to 15 minutes.
- Adjust long-lived TLS connection timeout from 45 minutes to 8 hours for improved certificate context refresh in chained proxy scenarios.
## 2025-03-10 - 3.30.3 - fix(classes.portproxy.ts)
Simplify timeout management in PortProxy and fix chained proxy certificate refresh issues
- Reduced TLS keep-alive timeout from 8 hours to 30 minutes to ensure frequent certificate refresh
- Added aggressive TLS state refresh after 20 minutes of inactivity and secondary verification checks
- Lowered long-lived TLS connection lifetime from 12 hours to 45 minutes to prevent stale certificates
- Removed configurable timeout settings from the public API in favor of hardcoded sensible defaults
- Simplified internal timeout management to reduce code complexity and improve certificate handling in chained proxies
## 2025-03-10 - 3.31.0 - fix(classes.portproxy.ts)
Simplified timeout management and fixed certificate issues in chained proxy scenarios
- Dramatically reduced TLS keep-alive timeout from 8 hours to 30 minutes to ensure fresh certificates
- Added aggressive certificate refresh after 20 minutes of inactivity (down from 4 hours)
- Added secondary verification checks for TLS refresh operations
- Reduced long-lived TLS connection lifetime from 12 hours to 45 minutes
- Removed configurable timeouts completely from the public API in favor of hardcoded sensible defaults
- Simplified interface by removing no-longer-configurable settings while maintaining internal compatibility
- Reduced overall code complexity by eliminating complex timeout management
- Fixed chained proxy certificate issues by ensuring more frequent certificate refreshes in all deployment scenarios
## 2025-03-10 - 3.30.2 - fix(classes.portproxy.ts)
Adjust TLS keep-alive timeout to refresh certificate context.
- Modified TLS keep-alive timeout for connections to 8 hours to refresh certificate context.
- Updated timeout log messages for clarity on TLS certificate refresh.
## 2025-03-10 - 3.30.1 - fix(PortProxy)
Improve TLS keep-alive management and fix whitespace formatting
- Implemented better handling for TLS keep-alive connections after sleep or long inactivity.
- Reformatted whitespace for better readability and consistency.
## 2025-03-08 - 3.30.0 - feat(PortProxy)
Add advanced TLS keep-alive handling and system sleep detection
- Implemented system sleep detection to maintain keep-alive connections.
- Enhanced TLS keep-alive connections with extended timeout and sleep detection mechanisms.
- Introduced automatic TLS state refresh after system wake-up to prevent connection drops.
## 2025-03-07 - 3.29.3 - fix(core)
Fix functional errors in the proxy setup and enhance pnpm configuration
- Corrected pnpm configuration to include specific dependencies as 'onlyBuiltDependencies'.
## 2025-03-07 - 3.29.2 - fix(PortProxy)
Fix test for PortProxy handling of custom IPs in Docker/CI environments.
- Ensure compatibility with Docker/CI environments by standardizing on 127.0.0.1 for test server setup.
- Simplify test configuration by using a unique port rather than different IPs.
## 2025-03-07 - 3.29.1 - fix(readme)
Update readme for IPTablesProxy options
- Add comprehensive examples for IPTablesProxy usage.
- Expand IPTablesProxy settings with IPv6, logging, and advanced features.
- Clarify option defaults and descriptions for IPTablesProxy.
- Enhance 'Troubleshooting' section with IPTables tips.
## 2025-03-07 - 3.29.0 - feat(IPTablesProxy)
Enhanced IPTablesProxy with multi-port and IPv6 support
- Added support for specifying multiple ports and port ranges, allowing for more complex network proxy configurations.
- Introduced IPv6 support to allow handling of IPv6 addressed networks.
- Implemented more detailed logging and error handling features to improve debugging capabilities.
- Enhanced integration options with NetworkProxy, allowing for a more seamless routing and termination process.
- Restructured the initialization and validation process to ensure robust handling of configuration settings.
## 2025-03-07 - 3.28.6 - fix(PortProxy)
Adjust default timeout settings and enhance keep-alive connection handling in PortProxy.
- Updated default value for maxConnectionLifetime to 24 hours and inactivityTimeout to 4 hours.
- Introduced enhanced settings for treating keep-alive connections as 'extended' or 'immortal'.
- Modified logic to avoid closing keep-alive connections unnecessarily by adding inactivity warnings and grace periods.
## 2025-03-07 - 3.28.5 - fix(core)
Ensure proper resource cleanup during server shutdown.
- Fixed potential hanging of server shutdown due to improper cleanup in promise handling.
- Corrected potential memory leaks by ensuring all pending and active connections are properly closed during shutdown.
## 2025-03-07 - 3.28.4 - fix(router)
Improve path pattern matching and hostname prioritization in router
- Enhance path pattern matching capabilities
- Ensure hostname prioritization in routing logic
## 2025-03-06 - 3.28.3 - fix(PortProxy)
Ensure timeout values are within Node.js safe limits
- Implemented `ensureSafeTimeout` to keep timeout values under the maximum safe integer for Node.js.
- Updated timeout configurations in `PortProxy` to include safety checks.
## 2025-03-06 - 3.28.2 - fix(portproxy)
Adjust safe timeout defaults in PortProxy to prevent overflow issues.
- Adjusted socketTimeout to maximum safe limit (~24.8 days) for PortProxy.
- Adjusted maxConnectionLifetime to maximum safe limit (~24.8 days) for PortProxy.
- Ensured enhanced default timeout settings in PortProxy.
## 2025-03-06 - 3.28.1 - fix(PortProxy)
Improved code formatting and readability in PortProxy class by adjusting spacing and comments.
- Adjusted comment and spacing for better code readability.
- No functional changes made in the PortProxy class.
## 2025-03-06 - 3.28.0 - feat(router)
Add detailed routing tests and refactor ProxyRouter for improved path matching
- Implemented a comprehensive test suite for the ProxyRouter class to ensure accurate routing based on hostnames and path patterns.
- Refactored the ProxyRouter to enhance path matching logic with improvements in wildcard and parameter handling.
- Improved logging capabilities within the ProxyRouter for enhanced debugging and info level insights.
- Optimized the data structures for storing and accessing proxy configurations to reduce overhead in routing operations.
## 2025-03-06 - 3.27.0 - feat(AcmeCertManager)
Introduce AcmeCertManager for enhanced ACME certificate management

View File

@ -1,8 +1,8 @@
{
"name": "@push.rocks/smartproxy",
"version": "3.27.0",
"version": "3.41.3",
"private": false,
"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.",
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
@ -18,8 +18,8 @@
"@git.zone/tsbuild": "^2.2.6",
"@git.zone/tsrun": "^1.2.44",
"@git.zone/tstest": "^1.0.77",
"@push.rocks/tapbundle": "^5.5.6",
"@types/node": "^22.13.9",
"@push.rocks/tapbundle": "^5.5.10",
"@types/node": "^22.13.10",
"typescript": "^5.8.2"
},
"dependencies": {
@ -28,7 +28,7 @@
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^2.0.23",
"@push.rocks/smartstring": "^4.0.15",
"@tsclass/tsclass": "^4.4.0",
"@tsclass/tsclass": "^5.0.0",
"@types/minimatch": "^5.1.2",
"@types/ws": "^8.18.0",
"acme-client": "^5.4.0",
@ -77,6 +77,11 @@
"url": "https://code.foss.global/push.rocks/smartproxy/issues"
},
"pnpm": {
"overrides": {}
"overrides": {},
"onlyBuiltDependencies": [
"esbuild",
"mongodb-memory-server",
"puppeteer"
]
}
}

1984
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -320,8 +320,8 @@ portProxy.start();
```typescript
import { IPTablesProxy } from '@push.rocks/smartproxy';
// Configure IPTables to forward from port 80 to 8080
const iptables = new IPTablesProxy({
// Basic usage - forward single port
const basicProxy = new IPTablesProxy({
fromPort: 80,
toPort: 8080,
toHost: 'localhost',
@ -329,7 +329,38 @@ const iptables = new IPTablesProxy({
deleteOnExit: true // Automatically clean up rules on process exit
});
iptables.start();
// Forward port ranges
const rangeProxy = new IPTablesProxy({
fromPort: { from: 3000, to: 3010 }, // Forward ports 3000-3010
toPort: { from: 8000, to: 8010 }, // To ports 8000-8010
protocol: 'tcp', // TCP protocol (default)
ipv6Support: true, // Enable IPv6 support
enableLogging: true // Enable detailed logging
});
// Multiple port specifications with IP filtering
const advancedProxy = new IPTablesProxy({
fromPort: [80, 443, { from: 8000, to: 8010 }], // Multiple ports/ranges
toPort: [8080, 8443, { from: 18000, to: 18010 }],
allowedSourceIPs: ['10.0.0.0/8', '192.168.1.0/24'], // Only allow these IPs
bannedSourceIPs: ['192.168.1.100'], // Explicitly block these IPs
addJumpRule: true, // Use custom chain for better management
checkExistingRules: true // Check for duplicate rules
});
// NetworkProxy integration for SSL termination
const sslProxy = new IPTablesProxy({
fromPort: 443,
toPort: 8443,
netProxyIntegration: {
enabled: true,
redirectLocalhost: true, // Redirect localhost traffic to NetworkProxy
sslTerminationPort: 8443 // Port where NetworkProxy handles SSL
}
});
// Start any of the proxies
await basicProxy.start();
```
### Automatic HTTPS Certificate Management
@ -383,13 +414,30 @@ acmeHandler.addDomain('api.example.com');
### IPTablesProxy Settings
| Option | Description | Default |
|-------------------|---------------------------------------------|-------------|
| `fromPort` | Source port to forward from | - |
| `toPort` | Destination port to forward to | - |
| `toHost` | Destination host to forward to | 'localhost' |
| `preserveSourceIP`| Preserve the original client IP | false |
| `deleteOnExit` | Remove iptables rules when process exits | false |
| Option | Description | Default |
|-----------------------|---------------------------------------------------|-------------|
| `fromPort` | Source port(s) or range(s) to forward from | - |
| `toPort` | Destination port(s) or range(s) to forward to | - |
| `toHost` | Destination host to forward to | 'localhost' |
| `preserveSourceIP` | Preserve the original client IP | false |
| `deleteOnExit` | Remove iptables rules when process exits | false |
| `protocol` | Protocol to forward ('tcp', 'udp', or 'all') | 'tcp' |
| `enableLogging` | Enable detailed logging | false |
| `ipv6Support` | Enable IPv6 support with ip6tables | false |
| `allowedSourceIPs` | Array of IP addresses/CIDR allowed to connect | - |
| `bannedSourceIPs` | Array of IP addresses/CIDR blocked from connecting | - |
| `forceCleanSlate` | Clear all IPTablesProxy rules before starting | false |
| `addJumpRule` | Add a custom chain for cleaner rule management | false |
| `checkExistingRules` | Check if rules already exist before adding | true |
| `netProxyIntegration` | NetworkProxy integration options (object) | - |
#### IPTablesProxy NetworkProxy Integration Options
| Option | Description | Default |
|----------------------|---------------------------------------------------|---------|
| `enabled` | Enable NetworkProxy integration | false |
| `redirectLocalhost` | Redirect localhost traffic to NetworkProxy | false |
| `sslTerminationPort` | Port where NetworkProxy handles SSL termination | - |
## Advanced Features
@ -442,6 +490,18 @@ The `PortProxy` class can inspect the SNI (Server Name Indication) field in TLS
- Domain-specific allowed IP ranges
- Protection against SNI renegotiation attacks
### Enhanced IPTables Management
The improved `IPTablesProxy` class offers advanced capabilities:
- Support for multiple port ranges and individual ports
- IPv6 support with ip6tables
- Source IP filtering with allow/block lists
- Custom chain creation for better rule organization
- NetworkProxy integration for SSL termination
- Automatic rule existence checking to prevent duplicates
- Comprehensive cleanup on shutdown
## Troubleshooting
### Browser Certificate Errors
@ -475,6 +535,16 @@ For improved connection stability in high-traffic environments:
4. **Monitor Connection Statistics**: Enable detailed logging to track termination reasons
5. **Fine-tune Inactivity Checks**: Adjust `inactivityCheckInterval` based on your traffic patterns
### IPTables Troubleshooting
If you're experiencing issues with IPTablesProxy:
1. **Enable Detailed Logging**: Set `enableLogging: true` to see all rule operations
2. **Force Clean Slate**: Use `forceCleanSlate: true` to remove any lingering rules
3. **Use Custom Chains**: Enable `addJumpRule: true` for cleaner rule management
4. **Check Permissions**: Ensure your process has sufficient permissions to modify iptables
5. **Verify IPv6 Support**: If using `ipv6Support: true`, ensure ip6tables is available
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.

View File

@ -113,20 +113,21 @@ tap.test('should forward TCP connections to custom host', async () => {
});
// Test custom IP forwarding
// SIMPLIFIED: This version avoids port ranges and domain configs to prevent loops
// Modified to work in Docker/CI environments without needing 127.0.0.2
tap.test('should forward connections to custom IP', async () => {
// Set up ports that are FAR apart to avoid any possible confusion
const forcedProxyPort = PROXY_PORT + 2; // 4003 - The port that our proxy listens on
const targetServerPort = TEST_SERVER_PORT + 200; // 4200 - Target test server on another IP
const forcedProxyPort = PROXY_PORT + 2; // 4003 - The port that our proxy listens on
const targetServerPort = TEST_SERVER_PORT + 200; // 4200 - Target test server on different port
// Create a test server listening on 127.0.0.2:4200
const testServer2 = await createTestServer(targetServerPort, '127.0.0.2');
// Create a test server listening on a unique port on 127.0.0.1 (works in all environments)
const testServer2 = await createTestServer(targetServerPort, '127.0.0.1');
// Simplify the test drastically - use ONE proxy with very explicit configuration
// We're simulating routing to a different IP by using a different port
// This tests the core functionality without requiring multiple IPs
const domainProxy = new PortProxy({
fromPort: forcedProxyPort, // 4003 - Listen on this port
toPort: targetServerPort, // 4200 - Default forwarding port - MUST BE DIFFERENT from fromPort
targetIP: '127.0.0.2', // Forward to IP where test server is
toPort: targetServerPort, // 4200 - Forward to this port
targetIP: '127.0.0.1', // Always use localhost (works in Docker)
domainConfigs: [], // No domain configs to confuse things
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], // Allow localhost

392
test/test.router.ts Normal file
View File

@ -0,0 +1,392 @@
import { expect, tap } from '@push.rocks/tapbundle';
import * as tsclass from '@tsclass/tsclass';
import * as http from 'http';
import { ProxyRouter, type IRouterResult } from '../ts/classes.router.js';
// Test proxies and configurations
let router: ProxyRouter;
// Sample hostname for testing
const TEST_DOMAIN = 'example.com';
const TEST_SUBDOMAIN = 'api.example.com';
const TEST_WILDCARD = '*.example.com';
// Helper: Creates a mock HTTP request for testing
function createMockRequest(host: string, url: string = '/'): http.IncomingMessage {
const req = {
headers: { host },
url,
socket: {
remoteAddress: '127.0.0.1'
}
} as any;
return req;
}
// Helper: Creates a test proxy configuration
function createProxyConfig(
hostname: string,
destinationIp: string = '10.0.0.1',
destinationPort: number = 8080
): tsclass.network.IReverseProxyConfig {
return {
hostName: hostname,
destinationIp,
destinationPort: destinationPort.toString(), // Convert to string for IReverseProxyConfig
publicKey: 'mock-cert',
privateKey: 'mock-key'
} as tsclass.network.IReverseProxyConfig;
}
// SETUP: Create a ProxyRouter instance
tap.test('setup proxy router test environment', async () => {
router = new ProxyRouter();
// Initialize with empty config
router.setNewProxyConfigs([]);
});
// Test basic routing by hostname
tap.test('should route requests by hostname', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
const req = createMockRequest(TEST_DOMAIN);
const result = router.routeReq(req);
expect(result).toBeTruthy();
expect(result).toEqual(config);
});
// Test handling of hostname with port number
tap.test('should handle hostname with port number', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
const req = createMockRequest(`${TEST_DOMAIN}:443`);
const result = router.routeReq(req);
expect(result).toBeTruthy();
expect(result).toEqual(config);
});
// Test case-insensitive hostname matching
tap.test('should perform case-insensitive hostname matching', async () => {
const config = createProxyConfig(TEST_DOMAIN.toLowerCase());
router.setNewProxyConfigs([config]);
const req = createMockRequest(TEST_DOMAIN.toUpperCase());
const result = router.routeReq(req);
expect(result).toBeTruthy();
expect(result).toEqual(config);
});
// Test handling of unmatched hostnames
tap.test('should return undefined for unmatched hostnames', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
const req = createMockRequest('unknown.domain.com');
const result = router.routeReq(req);
expect(result).toBeUndefined();
});
// Test adding path patterns
tap.test('should match requests using path patterns', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
// Add a path pattern to the config
router.setPathPattern(config, '/api/users');
// Test that path matches
const req1 = createMockRequest(TEST_DOMAIN, '/api/users');
const result1 = router.routeReqWithDetails(req1);
expect(result1).toBeTruthy();
expect(result1.config).toEqual(config);
expect(result1.pathMatch).toEqual('/api/users');
// Test that non-matching path doesn't match
const req2 = createMockRequest(TEST_DOMAIN, '/web/users');
const result2 = router.routeReqWithDetails(req2);
expect(result2).toBeUndefined();
});
// Test handling wildcard patterns
tap.test('should support wildcard path patterns', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
router.setPathPattern(config, '/api/*');
// Test with path that matches the wildcard pattern
const req = createMockRequest(TEST_DOMAIN, '/api/users/123');
const result = router.routeReqWithDetails(req);
expect(result).toBeTruthy();
expect(result.config).toEqual(config);
expect(result.pathMatch).toEqual('/api');
// Print the actual value to diagnose issues
console.log('Path remainder value:', result.pathRemainder);
expect(result.pathRemainder).toBeTruthy();
expect(result.pathRemainder).toEqual('/users/123');
});
// Test extracting path parameters
tap.test('should extract path parameters from URL', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
router.setPathPattern(config, '/users/:id/profile');
const req = createMockRequest(TEST_DOMAIN, '/users/123/profile');
const result = router.routeReqWithDetails(req);
expect(result).toBeTruthy();
expect(result.config).toEqual(config);
expect(result.pathParams).toBeTruthy();
expect(result.pathParams.id).toEqual('123');
});
// Test multiple configs for same hostname with different paths
tap.test('should support multiple configs for same hostname with different paths', async () => {
const apiConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.1', 8001);
const webConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.2', 8002);
// Add both configs
router.setNewProxyConfigs([apiConfig, webConfig]);
// Set different path patterns
router.setPathPattern(apiConfig, '/api');
router.setPathPattern(webConfig, '/web');
// Test API path routes to API config
const apiReq = createMockRequest(TEST_DOMAIN, '/api/users');
const apiResult = router.routeReq(apiReq);
expect(apiResult).toEqual(apiConfig);
// Test web path routes to web config
const webReq = createMockRequest(TEST_DOMAIN, '/web/dashboard');
const webResult = router.routeReq(webReq);
expect(webResult).toEqual(webConfig);
// Test unknown path returns undefined
const unknownReq = createMockRequest(TEST_DOMAIN, '/unknown');
const unknownResult = router.routeReq(unknownReq);
expect(unknownResult).toBeUndefined();
});
// Test wildcard subdomains
tap.test('should match wildcard subdomains', async () => {
const wildcardConfig = createProxyConfig(TEST_WILDCARD);
router.setNewProxyConfigs([wildcardConfig]);
// Test that subdomain.example.com matches *.example.com
const req = createMockRequest('subdomain.example.com');
const result = router.routeReq(req);
expect(result).toBeTruthy();
expect(result).toEqual(wildcardConfig);
});
// Test TLD wildcards (example.*)
tap.test('should match TLD wildcards', async () => {
const tldWildcardConfig = createProxyConfig('example.*');
router.setNewProxyConfigs([tldWildcardConfig]);
// Test that example.com matches example.*
const req1 = createMockRequest('example.com');
const result1 = router.routeReq(req1);
expect(result1).toBeTruthy();
expect(result1).toEqual(tldWildcardConfig);
// Test that example.org matches example.*
const req2 = createMockRequest('example.org');
const result2 = router.routeReq(req2);
expect(result2).toBeTruthy();
expect(result2).toEqual(tldWildcardConfig);
// Test that subdomain.example.com doesn't match example.*
const req3 = createMockRequest('subdomain.example.com');
const result3 = router.routeReq(req3);
expect(result3).toBeUndefined();
});
// Test complex pattern matching (*.lossless*)
tap.test('should match complex wildcard patterns', async () => {
const complexWildcardConfig = createProxyConfig('*.lossless*');
router.setNewProxyConfigs([complexWildcardConfig]);
// Test that sub.lossless.com matches *.lossless*
const req1 = createMockRequest('sub.lossless.com');
const result1 = router.routeReq(req1);
expect(result1).toBeTruthy();
expect(result1).toEqual(complexWildcardConfig);
// Test that api.lossless.org matches *.lossless*
const req2 = createMockRequest('api.lossless.org');
const result2 = router.routeReq(req2);
expect(result2).toBeTruthy();
expect(result2).toEqual(complexWildcardConfig);
// Test that losslessapi.com matches *.lossless*
const req3 = createMockRequest('losslessapi.com');
const result3 = router.routeReq(req3);
expect(result3).toBeUndefined(); // Should not match as it doesn't have a subdomain
});
// Test default configuration fallback
tap.test('should fall back to default configuration', async () => {
const defaultConfig = createProxyConfig('*');
const specificConfig = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([defaultConfig, specificConfig]);
// Test specific domain routes to specific config
const specificReq = createMockRequest(TEST_DOMAIN);
const specificResult = router.routeReq(specificReq);
expect(specificResult).toEqual(specificConfig);
// Test unknown domain falls back to default config
const unknownReq = createMockRequest('unknown.com');
const unknownResult = router.routeReq(unknownReq);
expect(unknownResult).toEqual(defaultConfig);
});
// Test priority between exact and wildcard matches
tap.test('should prioritize exact hostname over wildcard', async () => {
const wildcardConfig = createProxyConfig(TEST_WILDCARD);
const exactConfig = createProxyConfig(TEST_SUBDOMAIN);
router.setNewProxyConfigs([wildcardConfig, exactConfig]);
// Test that exact match takes priority
const req = createMockRequest(TEST_SUBDOMAIN);
const result = router.routeReq(req);
expect(result).toEqual(exactConfig);
});
// Test adding and removing configurations
tap.test('should manage configurations correctly', async () => {
router.setNewProxyConfigs([]);
// Add a config
const config = createProxyConfig(TEST_DOMAIN);
router.addProxyConfig(config);
// Verify routing works
const req = createMockRequest(TEST_DOMAIN);
let result = router.routeReq(req);
expect(result).toEqual(config);
// Remove the config and verify it no longer routes
const removed = router.removeProxyConfig(TEST_DOMAIN);
expect(removed).toBeTrue();
result = router.routeReq(req);
expect(result).toBeUndefined();
});
// Test path pattern specificity
tap.test('should prioritize more specific path patterns', async () => {
const genericConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.1', 8001);
const specificConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.2', 8002);
router.setNewProxyConfigs([genericConfig, specificConfig]);
router.setPathPattern(genericConfig, '/api/*');
router.setPathPattern(specificConfig, '/api/users');
// The more specific '/api/users' should match before the '/api/*' wildcard
const req = createMockRequest(TEST_DOMAIN, '/api/users');
const result = router.routeReq(req);
expect(result).toEqual(specificConfig);
});
// Test getHostnames method
tap.test('should retrieve all configured hostnames', async () => {
router.setNewProxyConfigs([
createProxyConfig(TEST_DOMAIN),
createProxyConfig(TEST_SUBDOMAIN)
]);
const hostnames = router.getHostnames();
expect(hostnames.length).toEqual(2);
expect(hostnames).toContain(TEST_DOMAIN.toLowerCase());
expect(hostnames).toContain(TEST_SUBDOMAIN.toLowerCase());
});
// Test handling missing host header
tap.test('should handle missing host header', async () => {
const defaultConfig = createProxyConfig('*');
router.setNewProxyConfigs([defaultConfig]);
const req = createMockRequest('');
req.headers.host = undefined;
const result = router.routeReq(req);
expect(result).toEqual(defaultConfig);
});
// Test complex path parameters
tap.test('should handle complex path parameters', async () => {
const config = createProxyConfig(TEST_DOMAIN);
router.setNewProxyConfigs([config]);
router.setPathPattern(config, '/api/:version/users/:userId/posts/:postId');
const req = createMockRequest(TEST_DOMAIN, '/api/v1/users/123/posts/456');
const result = router.routeReqWithDetails(req);
expect(result).toBeTruthy();
expect(result.config).toEqual(config);
expect(result.pathParams).toBeTruthy();
expect(result.pathParams.version).toEqual('v1');
expect(result.pathParams.userId).toEqual('123');
expect(result.pathParams.postId).toEqual('456');
});
// Performance test
tap.test('should handle many configurations efficiently', async () => {
const configs = [];
// Create many configs with different hostnames
for (let i = 0; i < 100; i++) {
configs.push(createProxyConfig(`host-${i}.example.com`));
}
router.setNewProxyConfigs(configs);
// Test middle of the list to avoid best/worst case
const req = createMockRequest('host-50.example.com');
const result = router.routeReq(req);
expect(result).toEqual(configs[50]);
});
// Test cleanup
tap.test('cleanup proxy router test environment', async () => {
// Clear all configurations
router.setNewProxyConfigs([]);
// Verify empty state
expect(router.getHostnames().length).toEqual(0);
expect(router.getProxyConfigs().length).toEqual(0);
});
export default tap.start();

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '3.27.0',
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.'
version: '3.41.3',
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
}

View File

@ -3,43 +3,100 @@ import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* Represents a port range for forwarding
*/
export interface IPortRange {
from: number;
to: number;
}
/**
* Settings for IPTablesProxy.
*/
export interface IIpTableProxySettings {
fromPort: number;
toPort: number;
// Basic settings
fromPort: number | IPortRange | Array<number | IPortRange>; // Support single port, port range, or multiple ports/ranges
toPort: number | IPortRange | Array<number | IPortRange>;
toHost?: string; // Target host for proxying; defaults to 'localhost'
preserveSourceIP?: boolean; // If true, the original source IP is preserved.
deleteOnExit?: boolean; // If true, clean up marked iptables rules before process exit.
// Advanced settings
preserveSourceIP?: boolean; // If true, the original source IP is preserved
deleteOnExit?: boolean; // If true, clean up marked iptables rules before process exit
protocol?: 'tcp' | 'udp' | 'all'; // Protocol to forward, defaults to 'tcp'
enableLogging?: boolean; // Enable detailed logging
ipv6Support?: boolean; // Enable IPv6 support (ip6tables)
// Source filtering
allowedSourceIPs?: string[]; // If provided, only these IPs are allowed
bannedSourceIPs?: string[]; // If provided, these IPs are blocked
// Rule management
forceCleanSlate?: boolean; // Clear all IPTablesProxy rules before starting
addJumpRule?: boolean; // Add a custom chain for cleaner rule management
checkExistingRules?: boolean; // Check if rules already exist before adding
// Integration with PortProxy/NetworkProxy
netProxyIntegration?: {
enabled: boolean;
redirectLocalhost?: boolean; // Redirect localhost traffic to NetworkProxy
sslTerminationPort?: number; // Port where NetworkProxy handles SSL termination
};
}
/**
* Represents a rule added to iptables
*/
interface IpTablesRule {
table: string;
chain: string;
command: string;
tag: string;
added: boolean;
}
/**
* IPTablesProxy sets up iptables NAT rules to forward TCP traffic.
* It only supports basic port forwarding and uses iptables comments to tag rules.
* Enhanced with multi-port support, IPv6, and integration with PortProxy/NetworkProxy.
*/
export class IPTablesProxy {
public settings: IIpTableProxySettings;
private rulesInstalled: boolean = false;
private rules: IpTablesRule[] = [];
private ruleTag: string;
private customChain: string | null = null;
constructor(settings: IIpTableProxySettings) {
// Validate inputs to prevent command injection
this.validateSettings(settings);
// Set default settings
this.settings = {
...settings,
toHost: settings.toHost || 'localhost',
protocol: settings.protocol || 'tcp',
enableLogging: settings.enableLogging !== undefined ? settings.enableLogging : false,
ipv6Support: settings.ipv6Support !== undefined ? settings.ipv6Support : false,
checkExistingRules: settings.checkExistingRules !== undefined ? settings.checkExistingRules : true,
netProxyIntegration: settings.netProxyIntegration || { enabled: false }
};
// Generate a unique identifier for the rules added by this instance.
// Generate a unique identifier for the rules added by this instance
this.ruleTag = `IPTablesProxy:${Date.now()}:${Math.random().toString(36).substr(2, 5)}`;
if (this.settings.addJumpRule) {
this.customChain = `IPTablesProxy_${Math.random().toString(36).substr(2, 5)}`;
}
// If deleteOnExit is true, register cleanup handlers.
// Register cleanup handlers if deleteOnExit is true
if (this.settings.deleteOnExit) {
const cleanup = () => {
try {
IPTablesProxy.cleanSlateSync();
this.stopSync();
} catch (err) {
console.error('Error cleaning iptables rules on exit:', err);
}
};
process.on('exit', cleanup);
process.on('SIGINT', () => {
cleanup();
@ -53,76 +110,591 @@ export class IPTablesProxy {
}
/**
* Sets up iptables rules for port forwarding.
* The rules are tagged with a unique comment so that they can be identified later.
* Validates settings to prevent command injection and ensure valid values
*/
public async start(): Promise<void> {
const dnatCmd = `iptables -t nat -A PREROUTING -p tcp --dport ${this.settings.fromPort} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${this.settings.toPort} ` +
`-m comment --comment "${this.ruleTag}:DNAT"`;
try {
await execAsync(dnatCmd);
console.log(`Added iptables rule: ${dnatCmd}`);
this.rulesInstalled = true;
} catch (err) {
console.error(`Failed to add iptables DNAT rule: ${err}`);
throw err;
}
// If preserveSourceIP is false, add a MASQUERADE rule.
if (!this.settings.preserveSourceIP) {
const masqueradeCmd = `iptables -t nat -A POSTROUTING -p tcp -d ${this.settings.toHost} ` +
`--dport ${this.settings.toPort} -j MASQUERADE ` +
`-m comment --comment "${this.ruleTag}:MASQ"`;
try {
await execAsync(masqueradeCmd);
console.log(`Added iptables rule: ${masqueradeCmd}`);
} catch (err) {
console.error(`Failed to add iptables MASQUERADE rule: ${err}`);
// Roll back the DNAT rule if MASQUERADE fails.
try {
const rollbackCmd = `iptables -t nat -D PREROUTING -p tcp --dport ${this.settings.fromPort} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${this.settings.toPort} ` +
`-m comment --comment "${this.ruleTag}:DNAT"`;
await execAsync(rollbackCmd);
this.rulesInstalled = false;
} catch (rollbackErr) {
console.error(`Rollback failed: ${rollbackErr}`);
private validateSettings(settings: IIpTableProxySettings): void {
// Validate port numbers
const validatePorts = (port: number | IPortRange | Array<number | IPortRange>) => {
if (Array.isArray(port)) {
port.forEach(p => validatePorts(p));
return;
}
if (typeof port === 'number') {
if (port < 1 || port > 65535) {
throw new Error(`Invalid port number: ${port}`);
}
} else if (typeof port === 'object') {
if (port.from < 1 || port.from > 65535 || port.to < 1 || port.to > 65535 || port.from > port.to) {
throw new Error(`Invalid port range: ${port.from}-${port.to}`);
}
}
};
validatePorts(settings.fromPort);
validatePorts(settings.toPort);
// Define regex patterns at the method level so they're available throughout
const ipRegex = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/;
// Validate IP addresses
const validateIPs = (ips?: string[]) => {
if (!ips) return;
for (const ip of ips) {
if (!ipRegex.test(ip) && !ipv6Regex.test(ip)) {
throw new Error(`Invalid IP address format: ${ip}`);
}
}
};
validateIPs(settings.allowedSourceIPs);
validateIPs(settings.bannedSourceIPs);
// Validate toHost - only allow hostnames or IPs
if (settings.toHost) {
const hostRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
if (!hostRegex.test(settings.toHost) && !ipRegex.test(settings.toHost) && !ipv6Regex.test(settings.toHost)) {
throw new Error(`Invalid host format: ${settings.toHost}`);
}
}
}
/**
* Normalizes port specifications into an array of port ranges
*/
private normalizePortSpec(portSpec: number | IPortRange | Array<number | IPortRange>): IPortRange[] {
const result: IPortRange[] = [];
if (Array.isArray(portSpec)) {
// If it's an array, process each element
for (const spec of portSpec) {
result.push(...this.normalizePortSpec(spec));
}
} else if (typeof portSpec === 'number') {
// Single port becomes a range with the same start and end
result.push({ from: portSpec, to: portSpec });
} else {
// Already a range
result.push(portSpec);
}
return result;
}
/**
* Gets the appropriate iptables command based on settings
*/
private getIptablesCommand(isIpv6: boolean = false): string {
return isIpv6 ? 'ip6tables' : 'iptables';
}
/**
* Checks if a rule already exists in iptables
*/
private async ruleExists(table: string, command: string, isIpv6: boolean = false): Promise<boolean> {
try {
const iptablesCmd = this.getIptablesCommand(isIpv6);
const { stdout } = await execAsync(`${iptablesCmd}-save -t ${table}`);
// Convert the command to the format found in iptables-save output
// (This is a simplification - in reality, you'd need more parsing)
const rulePattern = command.replace(`${iptablesCmd} -t ${table} -A `, '-A ');
return stdout.split('\n').some(line => line.trim() === rulePattern);
} catch (err) {
this.log('error', `Failed to check if rule exists: ${err}`);
return false;
}
}
/**
* Sets up a custom chain for better rule management
*/
private async setupCustomChain(isIpv6: boolean = false): Promise<boolean> {
if (!this.customChain) return true;
const iptablesCmd = this.getIptablesCommand(isIpv6);
const table = 'nat';
try {
// Create the chain
await execAsync(`${iptablesCmd} -t ${table} -N ${this.customChain}`);
this.log('info', `Created custom chain: ${this.customChain}`);
// Add jump rule to PREROUTING chain
const jumpCommand = `${iptablesCmd} -t ${table} -A PREROUTING -j ${this.customChain} -m comment --comment "${this.ruleTag}:JUMP"`;
await execAsync(jumpCommand);
this.log('info', `Added jump rule to ${this.customChain}`);
// Store the jump rule
this.rules.push({
table,
chain: 'PREROUTING',
command: jumpCommand,
tag: `${this.ruleTag}:JUMP`,
added: true
});
return true;
} catch (err) {
this.log('error', `Failed to set up custom chain: ${err}`);
return false;
}
}
/**
* Add a source IP filter rule
*/
private async addSourceIPFilter(isIpv6: boolean = false): Promise<boolean> {
if (!this.settings.allowedSourceIPs && !this.settings.bannedSourceIPs) {
return true;
}
const iptablesCmd = this.getIptablesCommand(isIpv6);
const table = 'nat';
const chain = this.customChain || 'PREROUTING';
try {
// Add banned IPs first (explicit deny)
if (this.settings.bannedSourceIPs && this.settings.bannedSourceIPs.length > 0) {
for (const ip of this.settings.bannedSourceIPs) {
const command = `${iptablesCmd} -t ${table} -A ${chain} -s ${ip} -j DROP -m comment --comment "${this.ruleTag}:BANNED"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${command}`);
continue;
}
await execAsync(command);
this.log('info', `Added banned IP rule: ${command}`);
this.rules.push({
table,
chain,
command,
tag: `${this.ruleTag}:BANNED`,
added: true
});
}
}
// Add allowed IPs (explicit allow)
if (this.settings.allowedSourceIPs && this.settings.allowedSourceIPs.length > 0) {
// First add a default deny for all
const denyAllCommand = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} -j DROP -m comment --comment "${this.ruleTag}:DENY_ALL"`;
// Add allow rules for specific IPs
for (const ip of this.settings.allowedSourceIPs) {
const command = `${iptablesCmd} -t ${table} -A ${chain} -s ${ip} -p ${this.settings.protocol} -j ACCEPT -m comment --comment "${this.ruleTag}:ALLOWED"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${command}`);
continue;
}
await execAsync(command);
this.log('info', `Added allowed IP rule: ${command}`);
this.rules.push({
table,
chain,
command,
tag: `${this.ruleTag}:ALLOWED`,
added: true
});
}
// Now add the default deny after all allows
if (this.settings.checkExistingRules && await this.ruleExists(table, denyAllCommand, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${denyAllCommand}`);
} else {
await execAsync(denyAllCommand);
this.log('info', `Added default deny rule: ${denyAllCommand}`);
this.rules.push({
table,
chain,
command: denyAllCommand,
tag: `${this.ruleTag}:DENY_ALL`,
added: true
});
}
}
return true;
} catch (err) {
this.log('error', `Failed to add source IP filter rules: ${err}`);
return false;
}
}
/**
* Adds a port forwarding rule
*/
private async addPortForwardingRule(
fromPortRange: IPortRange,
toPortRange: IPortRange,
isIpv6: boolean = false
): Promise<boolean> {
const iptablesCmd = this.getIptablesCommand(isIpv6);
const table = 'nat';
const chain = this.customChain || 'PREROUTING';
try {
// Handle single port case
if (fromPortRange.from === fromPortRange.to && toPortRange.from === toPortRange.to) {
// Single port forward
const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPortRange.from} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${toPortRange.from} ` +
`-m comment --comment "${this.ruleTag}:DNAT"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${command}`);
} else {
await execAsync(command);
this.log('info', `Added port forwarding rule: ${command}`);
this.rules.push({
table,
chain,
command,
tag: `${this.ruleTag}:DNAT`,
added: true
});
}
} else if (fromPortRange.to - fromPortRange.from === toPortRange.to - toPortRange.from) {
// Port range forward with equal ranges
const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPortRange.from}:${fromPortRange.to} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${toPortRange.from}-${toPortRange.to} ` +
`-m comment --comment "${this.ruleTag}:DNAT_RANGE"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${command}`);
} else {
await execAsync(command);
this.log('info', `Added port range forwarding rule: ${command}`);
this.rules.push({
table,
chain,
command,
tag: `${this.ruleTag}:DNAT_RANGE`,
added: true
});
}
} else {
// Unequal port ranges need individual rules
for (let i = 0; i <= fromPortRange.to - fromPortRange.from; i++) {
const fromPort = fromPortRange.from + i;
const toPort = toPortRange.from + i % (toPortRange.to - toPortRange.from + 1);
const command = `${iptablesCmd} -t ${table} -A ${chain} -p ${this.settings.protocol} --dport ${fromPort} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${toPort} ` +
`-m comment --comment "${this.ruleTag}:DNAT_INDIVIDUAL"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, command, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${command}`);
continue;
}
await execAsync(command);
this.log('info', `Added individual port forwarding rule: ${command}`);
this.rules.push({
table,
chain,
command,
tag: `${this.ruleTag}:DNAT_INDIVIDUAL`,
added: true
});
}
}
// If preserveSourceIP is false, add a MASQUERADE rule
if (!this.settings.preserveSourceIP) {
// For port range
const masqCommand = `${iptablesCmd} -t nat -A POSTROUTING -p ${this.settings.protocol} -d ${this.settings.toHost} ` +
`--dport ${toPortRange.from}:${toPortRange.to} -j MASQUERADE ` +
`-m comment --comment "${this.ruleTag}:MASQ"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists('nat', masqCommand, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${masqCommand}`);
} else {
await execAsync(masqCommand);
this.log('info', `Added MASQUERADE rule: ${masqCommand}`);
this.rules.push({
table: 'nat',
chain: 'POSTROUTING',
command: masqCommand,
tag: `${this.ruleTag}:MASQ`,
added: true
});
}
}
return true;
} catch (err) {
this.log('error', `Failed to add port forwarding rule: ${err}`);
// Try to roll back any rules that were already added
await this.rollbackRules();
return false;
}
}
/**
* Special handling for NetworkProxy integration
*/
private async setupNetworkProxyIntegration(isIpv6: boolean = false): Promise<boolean> {
if (!this.settings.netProxyIntegration?.enabled) {
return true;
}
const netProxyConfig = this.settings.netProxyIntegration;
const iptablesCmd = this.getIptablesCommand(isIpv6);
const table = 'nat';
const chain = this.customChain || 'PREROUTING';
try {
// If redirectLocalhost is true, set up special rule to redirect localhost traffic to NetworkProxy
if (netProxyConfig.redirectLocalhost && netProxyConfig.sslTerminationPort) {
const redirectCommand = `${iptablesCmd} -t ${table} -A OUTPUT -p tcp -d 127.0.0.1 -j REDIRECT ` +
`--to-port ${netProxyConfig.sslTerminationPort} ` +
`-m comment --comment "${this.ruleTag}:NETPROXY_REDIRECT"`;
// Check if rule already exists
if (this.settings.checkExistingRules && await this.ruleExists(table, redirectCommand, isIpv6)) {
this.log('info', `Rule already exists, skipping: ${redirectCommand}`);
} else {
await execAsync(redirectCommand);
this.log('info', `Added NetworkProxy redirection rule: ${redirectCommand}`);
this.rules.push({
table,
chain: 'OUTPUT',
command: redirectCommand,
tag: `${this.ruleTag}:NETPROXY_REDIRECT`,
added: true
});
}
}
return true;
} catch (err) {
this.log('error', `Failed to set up NetworkProxy integration: ${err}`);
return false;
}
}
/**
* Rolls back rules that were added in case of error
*/
private async rollbackRules(): Promise<void> {
// Process rules in reverse order (LIFO)
for (let i = this.rules.length - 1; i >= 0; i--) {
const rule = this.rules[i];
if (rule.added) {
try {
// Convert -A (add) to -D (delete)
const deleteCommand = rule.command.replace('-A', '-D');
await execAsync(deleteCommand);
this.log('info', `Rolled back rule: ${deleteCommand}`);
rule.added = false;
} catch (err) {
this.log('error', `Failed to roll back rule: ${err}`);
}
throw err;
}
}
}
/**
* Removes the iptables rules that were added in start(), by matching the unique comment.
* Sets up iptables rules for port forwarding with enhanced features
*/
public async stop(): Promise<void> {
if (!this.rulesInstalled) return;
const dnatDelCmd = `iptables -t nat -D PREROUTING -p tcp --dport ${this.settings.fromPort} ` +
`-j DNAT --to-destination ${this.settings.toHost}:${this.settings.toPort} ` +
`-m comment --comment "${this.ruleTag}:DNAT"`;
try {
await execAsync(dnatDelCmd);
console.log(`Removed iptables rule: ${dnatDelCmd}`);
} catch (err) {
console.error(`Failed to remove iptables DNAT rule: ${err}`);
public async start(): Promise<void> {
// Optionally clean the slate first
if (this.settings.forceCleanSlate) {
await IPTablesProxy.cleanSlate();
}
if (!this.settings.preserveSourceIP) {
const masqueradeDelCmd = `iptables -t nat -D POSTROUTING -p tcp -d ${this.settings.toHost} ` +
`--dport ${this.settings.toPort} -j MASQUERADE ` +
`-m comment --comment "${this.ruleTag}:MASQ"`;
try {
await execAsync(masqueradeDelCmd);
console.log(`Removed iptables rule: ${masqueradeDelCmd}`);
} catch (err) {
console.error(`Failed to remove iptables MASQUERADE rule: ${err}`);
// First set up any custom chains
if (this.settings.addJumpRule) {
const chainSetupSuccess = await this.setupCustomChain();
if (!chainSetupSuccess) {
throw new Error('Failed to set up custom chain');
}
// For IPv6 if enabled
if (this.settings.ipv6Support) {
const chainSetupSuccessIpv6 = await this.setupCustomChain(true);
if (!chainSetupSuccessIpv6) {
this.log('warn', 'Failed to set up IPv6 custom chain, continuing with IPv4 only');
}
}
}
// Add source IP filters
await this.addSourceIPFilter();
if (this.settings.ipv6Support) {
await this.addSourceIPFilter(true);
}
// Set up NetworkProxy integration if enabled
if (this.settings.netProxyIntegration?.enabled) {
const netProxySetupSuccess = await this.setupNetworkProxyIntegration();
if (!netProxySetupSuccess) {
this.log('warn', 'Failed to set up NetworkProxy integration');
}
if (this.settings.ipv6Support) {
await this.setupNetworkProxyIntegration(true);
}
}
// Normalize port specifications
const fromPortRanges = this.normalizePortSpec(this.settings.fromPort);
const toPortRanges = this.normalizePortSpec(this.settings.toPort);
// Handle the case where fromPort and toPort counts don't match
if (fromPortRanges.length !== toPortRanges.length) {
if (toPortRanges.length === 1) {
// If there's only one toPort, use it for all fromPorts
for (const fromRange of fromPortRanges) {
await this.addPortForwardingRule(fromRange, toPortRanges[0]);
if (this.settings.ipv6Support) {
await this.addPortForwardingRule(fromRange, toPortRanges[0], true);
}
}
} else {
throw new Error('Mismatched port counts: fromPort and toPort arrays must have equal length or toPort must be a single value');
}
} else {
// Add port forwarding rules for each port specification
for (let i = 0; i < fromPortRanges.length; i++) {
await this.addPortForwardingRule(fromPortRanges[i], toPortRanges[i]);
if (this.settings.ipv6Support) {
await this.addPortForwardingRule(fromPortRanges[i], toPortRanges[i], true);
}
}
}
// Final check - ensure we have at least one rule added
if (this.rules.filter(r => r.added).length === 0) {
throw new Error('No rules were added');
}
}
this.rulesInstalled = false;
/**
* Removes all added iptables rules
*/
public async stop(): Promise<void> {
// Process rules in reverse order (LIFO)
for (let i = this.rules.length - 1; i >= 0; i--) {
const rule = this.rules[i];
if (rule.added) {
try {
// Convert -A (add) to -D (delete)
const deleteCommand = rule.command.replace('-A', '-D');
await execAsync(deleteCommand);
this.log('info', `Removed rule: ${deleteCommand}`);
rule.added = false;
} catch (err) {
this.log('error', `Failed to remove rule: ${err}`);
}
}
}
// If we created a custom chain, we need to clean it up
if (this.customChain) {
try {
// First flush the chain
await execAsync(`iptables -t nat -F ${this.customChain}`);
this.log('info', `Flushed custom chain: ${this.customChain}`);
// Then delete it
await execAsync(`iptables -t nat -X ${this.customChain}`);
this.log('info', `Deleted custom chain: ${this.customChain}`);
// Same for IPv6 if enabled
if (this.settings.ipv6Support) {
try {
await execAsync(`ip6tables -t nat -F ${this.customChain}`);
await execAsync(`ip6tables -t nat -X ${this.customChain}`);
this.log('info', `Deleted IPv6 custom chain: ${this.customChain}`);
} catch (err) {
this.log('error', `Failed to delete IPv6 custom chain: ${err}`);
}
}
} catch (err) {
this.log('error', `Failed to delete custom chain: ${err}`);
}
}
// Clear rules array
this.rules = [];
}
/**
* Synchronous version of stop, for use in exit handlers
*/
public stopSync(): void {
// Process rules in reverse order (LIFO)
for (let i = this.rules.length - 1; i >= 0; i--) {
const rule = this.rules[i];
if (rule.added) {
try {
// Convert -A (add) to -D (delete)
const deleteCommand = rule.command.replace('-A', '-D');
execSync(deleteCommand);
this.log('info', `Removed rule: ${deleteCommand}`);
rule.added = false;
} catch (err) {
this.log('error', `Failed to remove rule: ${err}`);
}
}
}
// If we created a custom chain, we need to clean it up
if (this.customChain) {
try {
// First flush the chain
execSync(`iptables -t nat -F ${this.customChain}`);
// Then delete it
execSync(`iptables -t nat -X ${this.customChain}`);
this.log('info', `Deleted custom chain: ${this.customChain}`);
// Same for IPv6 if enabled
if (this.settings.ipv6Support) {
try {
execSync(`ip6tables -t nat -F ${this.customChain}`);
execSync(`ip6tables -t nat -X ${this.customChain}`);
} catch (err) {
// IPv6 failures are non-critical
}
}
} catch (err) {
this.log('error', `Failed to delete custom chain: ${err}`);
}
}
// Clear rules array
this.rules = [];
}
/**
@ -130,26 +702,88 @@ export class IPTablesProxy {
* It looks for rules with comments containing "IPTablesProxy:".
*/
public static async cleanSlate(): Promise<void> {
await IPTablesProxy.cleanSlateInternal();
// Also clean IPv6 rules
await IPTablesProxy.cleanSlateInternal(true);
}
/**
* Internal implementation of cleanSlate with IPv6 support
*/
private static async cleanSlateInternal(isIpv6: boolean = false): Promise<void> {
const iptablesCmd = isIpv6 ? 'ip6tables' : 'iptables';
try {
const { stdout } = await execAsync('iptables-save -t nat');
const { stdout } = await execAsync(`${iptablesCmd}-save -t nat`);
const lines = stdout.split('\n');
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
// First, find and remove any custom chains
const customChains = new Set<string>();
const jumpRules: string[] = [];
for (const line of proxyLines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
// Replace the "-A" with "-D" to form a deletion command.
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `iptables -t nat ${deleteRule}`;
try {
await execAsync(cmd);
console.log(`Cleaned up iptables rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables rule: ${cmd}`, err);
if (line.includes('IPTablesProxy:JUMP')) {
// Extract chain name from jump rule
const match = line.match(/\s+-j\s+(\S+)\s+/);
if (match && match[1].startsWith('IPTablesProxy_')) {
customChains.add(match[1]);
jumpRules.push(line);
}
}
}
// Remove jump rules first
for (const line of jumpRules) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
// Replace the "-A" with "-D" to form a deletion command
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
try {
await execAsync(cmd);
console.log(`Cleaned up iptables jump rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables jump rule: ${cmd}`, err);
}
}
}
// Then remove all other rules
for (const line of proxyLines) {
if (!line.includes('IPTablesProxy:JUMP')) { // Skip jump rules we already handled
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
// Replace the "-A" with "-D" to form a deletion command
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
try {
await execAsync(cmd);
console.log(`Cleaned up iptables rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables rule: ${cmd}`, err);
}
}
}
}
// Finally clean up custom chains
for (const chain of customChains) {
try {
// Flush the chain
await execAsync(`${iptablesCmd} -t nat -F ${chain}`);
console.log(`Flushed custom chain: ${chain}`);
// Delete the chain
await execAsync(`${iptablesCmd} -t nat -X ${chain}`);
console.log(`Deleted custom chain: ${chain}`);
} catch (err) {
console.error(`Failed to delete custom chain ${chain}:`, err);
}
}
} catch (err) {
console.error(`Failed to run iptables-save: ${err}`);
console.error(`Failed to run ${iptablesCmd}-save: ${err}`);
}
}
@ -159,25 +793,109 @@ export class IPTablesProxy {
* This method is intended for use in process exit handlers.
*/
public static cleanSlateSync(): void {
IPTablesProxy.cleanSlateSyncInternal();
// Also clean IPv6 rules
IPTablesProxy.cleanSlateSyncInternal(true);
}
/**
* Internal implementation of cleanSlateSync with IPv6 support
*/
private static cleanSlateSyncInternal(isIpv6: boolean = false): void {
const iptablesCmd = isIpv6 ? 'ip6tables' : 'iptables';
try {
const stdout = execSync('iptables-save -t nat').toString();
const stdout = execSync(`${iptablesCmd}-save -t nat`).toString();
const lines = stdout.split('\n');
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
// First, find and remove any custom chains
const customChains = new Set<string>();
const jumpRules: string[] = [];
for (const line of proxyLines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `iptables -t nat ${deleteRule}`;
try {
execSync(cmd);
console.log(`Cleaned up iptables rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables rule: ${cmd}`, err);
if (line.includes('IPTablesProxy:JUMP')) {
// Extract chain name from jump rule
const match = line.match(/\s+-j\s+(\S+)\s+/);
if (match && match[1].startsWith('IPTablesProxy_')) {
customChains.add(match[1]);
jumpRules.push(line);
}
}
}
// Remove jump rules first
for (const line of jumpRules) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
// Replace the "-A" with "-D" to form a deletion command
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
try {
execSync(cmd);
console.log(`Cleaned up iptables jump rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables jump rule: ${cmd}`, err);
}
}
}
// Then remove all other rules
for (const line of proxyLines) {
if (!line.includes('IPTablesProxy:JUMP')) { // Skip jump rules we already handled
const trimmedLine = line.trim();
if (trimmedLine.startsWith('-A')) {
const deleteRule = trimmedLine.replace('-A', '-D');
const cmd = `${iptablesCmd} -t nat ${deleteRule}`;
try {
execSync(cmd);
console.log(`Cleaned up iptables rule: ${cmd}`);
} catch (err) {
console.error(`Failed to remove iptables rule: ${cmd}`, err);
}
}
}
}
// Finally clean up custom chains
for (const chain of customChains) {
try {
// Flush the chain
execSync(`${iptablesCmd} -t nat -F ${chain}`);
// Delete the chain
execSync(`${iptablesCmd} -t nat -X ${chain}`);
console.log(`Deleted custom chain: ${chain}`);
} catch (err) {
console.error(`Failed to delete custom chain ${chain}:`, err);
}
}
} catch (err) {
console.error(`Failed to run iptables-save: ${err}`);
console.error(`Failed to run ${iptablesCmd}-save: ${err}`);
}
}
/**
* Logging utility that respects the enableLogging setting
*/
private log(level: 'info' | 'warn' | 'error', message: string): void {
if (!this.settings.enableLogging && level === 'info') {
return;
}
const timestamp = new Date().toISOString();
switch (level) {
case 'info':
console.log(`[${timestamp}] [INFO] ${message}`);
break;
case 'warn':
console.warn(`[${timestamp}] [WARN] ${message}`);
break;
case 'error':
console.error(`[${timestamp}] [ERROR] ${message}`);
break;
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
import * as plugins from './plugins.js';
import * as http from 'http';
import * as url from 'url';
import * as tsclass from '@tsclass/tsclass';
/**
* Optional path pattern configuration that can be added to proxy configs
@ -11,31 +13,52 @@ export interface IPathPatternConfig {
* Interface for router result with additional metadata
*/
export interface IRouterResult {
config: plugins.tsclass.network.IReverseProxyConfig;
config: tsclass.network.IReverseProxyConfig;
pathMatch?: string;
pathParams?: Record<string, string>;
pathRemainder?: string;
}
/**
* Router for HTTP reverse proxy requests
*
* Supports the following domain matching patterns:
* - Exact matches: "example.com"
* - Wildcard subdomains: "*.example.com" (matches any subdomain of example.com)
* - TLD wildcards: "example.*" (matches example.com, example.org, etc.)
* - Complex wildcards: "*.lossless*" (matches any subdomain of any lossless domain)
* - Default fallback: "*" (matches any unmatched domain)
*
* Also supports path pattern matching for each domain:
* - Exact path: "/api/users"
* - Wildcard paths: "/api/*"
* - Path parameters: "/users/:id/profile"
*/
export class ProxyRouter {
// Using a Map for O(1) hostname lookups instead of array search
private hostMap: Map<string, plugins.tsclass.network.IReverseProxyConfig[]> = new Map();
// Store original configs for reference
private reverseProxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = [];
// Default config to use when no match is found (optional)
private defaultConfig?: plugins.tsclass.network.IReverseProxyConfig;
private defaultConfig?: tsclass.network.IReverseProxyConfig;
// Store path patterns separately since they're not in the original interface
private pathPatterns: Map<plugins.tsclass.network.IReverseProxyConfig, string> = new Map();
private pathPatterns: Map<tsclass.network.IReverseProxyConfig, string> = new Map();
// Logger interface
private logger: {
error: (message: string, data?: any) => void;
warn: (message: string, data?: any) => void;
info: (message: string, data?: any) => void;
debug: (message: string, data?: any) => void;
};
constructor(
configs?: plugins.tsclass.network.IReverseProxyConfig[],
private readonly logger: {
configs?: tsclass.network.IReverseProxyConfig[],
logger?: {
error: (message: string, data?: any) => void;
warn: (message: string, data?: any) => void;
info: (message: string, data?: any) => void;
debug: (message: string, data?: any) => void;
} = console
}
) {
this.logger = logger || console;
if (configs) {
this.setNewProxyConfigs(configs);
}
@ -45,61 +68,13 @@ export class ProxyRouter {
* Sets a new set of reverse configs to be routed to
* @param reverseCandidatesArg Array of reverse proxy configurations
*/
public setNewProxyConfigs(reverseCandidatesArg: plugins.tsclass.network.IReverseProxyConfig[]): void {
public setNewProxyConfigs(reverseCandidatesArg: tsclass.network.IReverseProxyConfig[]): void {
this.reverseProxyConfigs = [...reverseCandidatesArg];
// Reset the host map and path patterns
this.hostMap.clear();
this.pathPatterns.clear();
// Find default config if any (config with "*" as hostname)
this.defaultConfig = this.reverseProxyConfigs.find(config => config.hostName === '*');
// Group configs by hostname for faster lookups
for (const config of this.reverseProxyConfigs) {
// Skip the default config as it's stored separately
if (config.hostName === '*') continue;
const hostname = config.hostName.toLowerCase(); // Case-insensitive hostname lookup
if (!this.hostMap.has(hostname)) {
this.hostMap.set(hostname, []);
}
// Check for path pattern in extended properties
// (using any to access custom properties not in the interface)
const extendedConfig = config as any;
if (extendedConfig.pathPattern) {
this.pathPatterns.set(config, extendedConfig.pathPattern);
}
// Add to the list of configs for this hostname
this.hostMap.get(hostname).push(config);
}
// Sort configs for each hostname by specificity
// More specific path patterns should be checked first
for (const [hostname, configs] of this.hostMap.entries()) {
if (configs.length > 1) {
// Sort by pathPattern - most specific first
// (null comes last, exact paths before patterns with wildcards)
configs.sort((a, b) => {
const aPattern = this.pathPatterns.get(a);
const bPattern = this.pathPatterns.get(b);
// If one has a path and the other doesn't, the one with a path comes first
if (!aPattern && bPattern) return 1;
if (aPattern && !bPattern) return -1;
if (!aPattern && !bPattern) return 0;
// Both have path patterns - more specific (longer) first
// This is a simple heuristic; we could use a more sophisticated approach
return bPattern.length - aPattern.length;
});
}
}
this.logger.info(`Router initialized with ${this.reverseProxyConfigs.length} configs (${this.hostMap.size} unique hosts)`);
this.logger.info(`Router initialized with ${this.reverseProxyConfigs.length} configs (${this.getHostnames().length} unique hosts)`);
}
/**
@ -107,7 +82,7 @@ export class ProxyRouter {
* @param req The incoming HTTP request
* @returns The matching proxy config or undefined if no match found
*/
public routeReq(req: plugins.http.IncomingMessage): plugins.tsclass.network.IReverseProxyConfig {
public routeReq(req: http.IncomingMessage): tsclass.network.IReverseProxyConfig {
const result = this.routeReqWithDetails(req);
return result ? result.config : undefined;
}
@ -117,7 +92,7 @@ export class ProxyRouter {
* @param req The incoming HTTP request
* @returns Detailed routing result including matched config and path information
*/
public routeReqWithDetails(req: plugins.http.IncomingMessage): IRouterResult | undefined {
public routeReqWithDetails(req: http.IncomingMessage): IRouterResult | undefined {
// Extract and validate host header
const originalHost = req.headers.host;
if (!originalHost) {
@ -126,52 +101,46 @@ export class ProxyRouter {
}
// Parse URL for path matching
const urlPath = new URL(
req.url || '/',
`http://${originalHost}`
).pathname;
const parsedUrl = url.parse(req.url || '/');
const urlPath = parsedUrl.pathname || '/';
// Extract hostname without port
const hostWithoutPort = originalHost.split(':')[0].toLowerCase();
// Find configs for this hostname
const configs = this.hostMap.get(hostWithoutPort);
if (configs && configs.length > 0) {
// Check each config for path matching
for (const config of configs) {
// Get the path pattern if any
const pathPattern = this.pathPatterns.get(config);
// If no path pattern specified, this config matches all paths
if (!pathPattern) {
return { config };
}
// Check if path matches the pattern
const pathMatch = this.matchPath(urlPath, pathPattern);
if (pathMatch) {
return {
config,
pathMatch: pathMatch.matched,
pathParams: pathMatch.params,
pathRemainder: pathMatch.remainder
};
}
}
// First try exact hostname match
const exactConfig = this.findConfigForHost(hostWithoutPort, urlPath);
if (exactConfig) {
return exactConfig;
}
// Try wildcard subdomains if no direct match found
// For example, if request is for sub.example.com, try *.example.com
const domainParts = hostWithoutPort.split('.');
if (domainParts.length > 2) {
const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
const wildcardConfigs = this.hostMap.get(wildcardDomain);
// Try various wildcard patterns
if (hostWithoutPort.includes('.')) {
const domainParts = hostWithoutPort.split('.');
if (wildcardConfigs && wildcardConfigs.length > 0) {
// Use the first matching wildcard config
// Could add path matching logic here as well
return { config: wildcardConfigs[0] };
// Try wildcard subdomain (*.example.com)
if (domainParts.length > 2) {
const wildcardDomain = `*.${domainParts.slice(1).join('.')}`;
const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath);
if (wildcardConfig) {
return wildcardConfig;
}
}
// Try TLD wildcard (example.*)
const baseDomain = domainParts.slice(0, -1).join('.');
const tldWildcardDomain = `${baseDomain}.*`;
const tldWildcardConfig = this.findConfigForHost(tldWildcardDomain, urlPath);
if (tldWildcardConfig) {
return tldWildcardConfig;
}
// Try complex wildcard patterns
const wildcardPatterns = this.findWildcardMatches(hostWithoutPort);
for (const pattern of wildcardPatterns) {
const wildcardConfig = this.findConfigForHost(pattern, urlPath);
if (wildcardConfig) {
return wildcardConfig;
}
}
}
@ -186,23 +155,109 @@ export class ProxyRouter {
}
/**
* Sets a path pattern for an existing config
* @param config The existing configuration
* @param pathPattern The path pattern to set
* @returns Boolean indicating if the config was found and updated
* Find potential wildcard patterns that could match a given hostname
* Handles complex patterns like "*.lossless*" or other partial matches
* @param hostname The hostname to find wildcard matches for
* @returns Array of potential wildcard patterns that could match
*/
public setPathPattern(
config: plugins.tsclass.network.IReverseProxyConfig,
pathPattern: string
): boolean {
const exists = this.reverseProxyConfigs.includes(config);
if (exists) {
this.pathPatterns.set(config, pathPattern);
return true;
private findWildcardMatches(hostname: string): string[] {
const patterns: string[] = [];
const hostnameParts = hostname.split('.');
// Find all configured hostnames that contain wildcards
const wildcardConfigs = this.reverseProxyConfigs.filter(
config => config.hostName.includes('*')
);
// Extract unique wildcard patterns
const wildcardPatterns = [...new Set(
wildcardConfigs.map(config => config.hostName.toLowerCase())
)];
// For each wildcard pattern, check if it could match the hostname
// using simplified regex pattern matching
for (const pattern of wildcardPatterns) {
// Skip the default wildcard '*'
if (pattern === '*') continue;
// Skip already checked patterns (*.domain.com and domain.*)
if (pattern.startsWith('*.') && pattern.indexOf('*', 2) === -1) continue;
if (pattern.endsWith('.*') && pattern.indexOf('*') === pattern.length - 1) continue;
// Convert wildcard pattern to regex
const regexPattern = pattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*'); // Convert * to .* for regex
// Create regex object with case insensitive flag
const regex = new RegExp(`^${regexPattern}$`, 'i');
// If hostname matches this complex pattern, add it to the list
if (regex.test(hostname)) {
patterns.push(pattern);
}
}
return false;
return patterns;
}
/**
* Find a config for a specific host and path
*/
private findConfigForHost(hostname: string, path: string): IRouterResult | undefined {
// Find all configs for this hostname
const configs = this.reverseProxyConfigs.filter(
config => config.hostName.toLowerCase() === hostname.toLowerCase()
);
if (configs.length === 0) {
return undefined;
}
// First try configs with path patterns
const configsWithPaths = configs.filter(config => this.pathPatterns.has(config));
// Sort by path pattern specificity - more specific first
configsWithPaths.sort((a, b) => {
const aPattern = this.pathPatterns.get(a) || '';
const bPattern = this.pathPatterns.get(b) || '';
// Exact patterns come before wildcard patterns
const aHasWildcard = aPattern.includes('*');
const bHasWildcard = bPattern.includes('*');
if (aHasWildcard && !bHasWildcard) return 1;
if (!aHasWildcard && bHasWildcard) return -1;
// Longer patterns are considered more specific
return bPattern.length - aPattern.length;
});
// Check each config with path pattern
for (const config of configsWithPaths) {
const pathPattern = this.pathPatterns.get(config);
if (pathPattern) {
const pathMatch = this.matchPath(path, pathPattern);
if (pathMatch) {
return {
config,
pathMatch: pathMatch.matched,
pathParams: pathMatch.params,
pathRemainder: pathMatch.remainder
};
}
}
}
// If no path pattern matched, use the first config without a path pattern
const configWithoutPath = configs.find(config => !this.pathPatterns.has(config));
if (configWithoutPath) {
return { config: configWithoutPath };
}
return undefined;
}
/**
* Matches a URL path against a pattern
* Supports:
@ -242,62 +297,51 @@ export class ProxyRouter {
}
// Handle path parameters
const patternParts = pattern.split('/');
const pathParts = path.split('/');
const patternParts = pattern.split('/').filter(p => p);
const pathParts = path.split('/').filter(p => p);
// Check if paths are compatible length
if (
// If pattern doesn't end with wildcard, paths must have the same number of parts
(!pattern.endsWith('/*') && patternParts.length !== pathParts.length) ||
// If pattern ends with wildcard, path must have at least as many parts as the pattern
(pattern.endsWith('/*') && pathParts.length < patternParts.length - 1)
) {
// Too few path parts to match
if (pathParts.length < patternParts.length) {
return null;
}
const params: Record<string, string> = {};
const matchedParts: string[] = [];
// Compare path parts
// Compare each part
for (let i = 0; i < patternParts.length; i++) {
const patternPart = patternParts[i];
// Handle wildcard at the end
if (patternPart === '*' && i === patternParts.length - 1) {
break;
}
// If pathParts[i] doesn't exist, we've reached the end of the path
if (i >= pathParts.length) {
return null;
}
const pathPart = pathParts[i];
// Handle parameter
if (patternPart.startsWith(':')) {
const paramName = patternPart.slice(1);
params[paramName] = pathPart;
matchedParts.push(pathPart);
continue;
}
// Handle wildcard at the end
if (patternPart === '*' && i === patternParts.length - 1) {
break;
}
// Handle exact match for this part
if (patternPart !== pathPart) {
return null;
}
matchedParts.push(pathPart);
}
// Calculate the remainder
let remainder = '';
if (pattern.endsWith('/*')) {
remainder = '/' + pathParts.slice(patternParts.length - 1).join('/');
}
// Calculate the remainder - the unmatched path parts
const remainderParts = pathParts.slice(patternParts.length);
const remainder = remainderParts.length ? '/' + remainderParts.join('/') : '';
// Calculate the matched path
const matchedParts = patternParts.map((part, i) => {
return part.startsWith(':') ? pathParts[i] : part;
});
const matched = '/' + matchedParts.join('/');
return {
matched: matchedParts.join('/'),
matched,
params,
remainder
};
@ -307,7 +351,7 @@ export class ProxyRouter {
* Gets all currently active proxy configurations
* @returns Array of all active configurations
*/
public getProxyConfigs(): plugins.tsclass.network.IReverseProxyConfig[] {
public getProxyConfigs(): tsclass.network.IReverseProxyConfig[] {
return [...this.reverseProxyConfigs];
}
@ -316,7 +360,13 @@ export class ProxyRouter {
* @returns Array of hostnames
*/
public getHostnames(): string[] {
return Array.from(this.hostMap.keys());
const hostnames = new Set<string>();
for (const config of this.reverseProxyConfigs) {
if (config.hostName !== '*') {
hostnames.add(config.hostName.toLowerCase());
}
}
return Array.from(hostnames);
}
/**
@ -325,7 +375,7 @@ export class ProxyRouter {
* @param pathPattern Optional path pattern for route matching
*/
public addProxyConfig(
config: plugins.tsclass.network.IReverseProxyConfig,
config: tsclass.network.IReverseProxyConfig,
pathPattern?: string
): void {
this.reverseProxyConfigs.push(config);
@ -334,8 +384,24 @@ export class ProxyRouter {
if (pathPattern) {
this.pathPatterns.set(config, pathPattern);
}
this.setNewProxyConfigs(this.reverseProxyConfigs);
}
/**
* Sets a path pattern for an existing config
* @param config The existing configuration
* @param pathPattern The path pattern to set
* @returns Boolean indicating if the config was found and updated
*/
public setPathPattern(
config: tsclass.network.IReverseProxyConfig,
pathPattern: string
): boolean {
const exists = this.reverseProxyConfigs.includes(config);
if (exists) {
this.pathPatterns.set(config, pathPattern);
return true;
}
return false;
}
/**
@ -345,15 +411,22 @@ export class ProxyRouter {
*/
public removeProxyConfig(hostname: string): boolean {
const initialCount = this.reverseProxyConfigs.length;
// Find configs to remove
const configsToRemove = this.reverseProxyConfigs.filter(
config => config.hostName === hostname
);
// Remove them from the patterns map
for (const config of configsToRemove) {
this.pathPatterns.delete(config);
}
// Filter them out of the configs array
this.reverseProxyConfigs = this.reverseProxyConfigs.filter(
config => config.hostName !== hostname
);
if (initialCount !== this.reverseProxyConfigs.length) {
this.setNewProxyConfigs(this.reverseProxyConfigs);
return true;
}
return false;
return this.reverseProxyConfigs.length !== initialCount;
}
}

1299
ts/classes.snihandler.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,3 +3,4 @@ export * from './classes.networkproxy.js';
export * from './classes.portproxy.js';
export * from './classes.port80handler.js';
export * from './classes.sslredirect.js';
export * from './classes.snihandler.js';