Compare commits

...

52 Commits

Author SHA1 Message Date
12de96a7d5 3.22.1 2025-03-03 01:57:52 +00:00
296e1fcdc7 fix(PortProxy): Fix connection timeout and IP validation handling for PortProxy 2025-03-03 01:57:52 +00:00
8459e4013c 3.22.0 2025-03-03 01:50:30 +00:00
191c8ac0e6 feat(classes.portproxy): Enhanced PortProxy to support initial data timeout and improved IP handling 2025-03-03 01:50:30 +00:00
3ab483d164 3.21.0 2025-03-03 01:42:16 +00:00
fcd80dc56b feat(PortProxy): Enhancements to connection management in PortProxy 2025-03-03 01:42:16 +00:00
8ddffcd6e5 3.20.2 2025-03-01 20:31:50 +00:00
a5a7781c17 fix(PortProxy): Enhance connection cleanup handling in PortProxy 2025-03-01 20:31:50 +00:00
d647e77cdf 3.20.1 2025-03-01 17:32:31 +00:00
9161336197 fix(PortProxy): Improve IP allowance check for forced domains 2025-03-01 17:32:31 +00:00
2e63d13dd4 3.20.0 2025-03-01 17:19:27 +00:00
af6ed735d5 feat(PortProxy): Enhance PortProxy with advanced connection cleanup and logging 2025-03-01 17:19:27 +00:00
7d38f29ef3 3.19.0 2025-03-01 13:17:05 +00:00
0df26d4367 feat(PortProxy): Enhance PortProxy with default blocked IPs 2025-03-01 13:17:05 +00:00
f9a6e2d748 3.18.2 2025-02-27 21:25:03 +00:00
1cb6302750 fix(portproxy): Fixed typographical errors in comments within PortProxy class. 2025-02-27 21:25:03 +00:00
f336f25535 3.18.1 2025-02-27 21:19:34 +00:00
5d6b707440 fix(PortProxy): Refactor and enhance PortProxy test cases and handling 2025-02-27 21:19:34 +00:00
622ad2ff20 3.18.0 2025-02-27 20:59:29 +00:00
dd23efd28d feat(PortProxy): Add SNI-based renegotiation handling in PortProxy 2025-02-27 20:59:29 +00:00
0ddf68a919 3.17.1 2025-02-27 20:10:26 +00:00
ec08ca51f5 fix(PortProxy): Fix handling of SNI re-negotiation in PortProxy 2025-02-27 20:10:26 +00:00
29688d1379 3.17.0 2025-02-27 19:57:28 +00:00
c83f6fa278 feat(smartproxy): Enhance description clarity and improve SNI handling with domain locking. 2025-02-27 19:57:27 +00:00
60333b0a59 3.16.9 2025-02-27 15:46:14 +00:00
1aa409907b fix(portproxy): Extend domain input validation to support string arrays in port proxy configurations. 2025-02-27 15:46:14 +00:00
adee6afc76 3.16.8 2025-02-27 15:41:03 +00:00
4a0792142f fix(PortProxy): Fix IP filtering for domain and global default allowed lists and improve port-based routing logic. 2025-02-27 15:41:03 +00:00
f1b810a4fa 3.16.7 2025-02-27 15:32:06 +00:00
96b5877c5f fix(PortProxy): Improved IP validation logic in PortProxy to ensure correct domain matching and fallback 2025-02-27 15:32:06 +00:00
6d627f67f7 3.16.6 2025-02-27 15:30:20 +00:00
9af968b8e7 fix(PortProxy): Optimize connection cleanup logic in PortProxy by removing unnecessary delays. 2025-02-27 15:30:20 +00:00
b3ba0c21e8 3.16.5 2025-02-27 15:05:38 +00:00
ef707a5870 fix(PortProxy): Improved connection cleanup process with added asynchronous delays 2025-02-27 15:05:38 +00:00
6ca14edb38 3.16.4 2025-02-27 14:23:44 +00:00
5a5686b6b9 fix(PortProxy): Fix and enhance port proxy handling 2025-02-27 14:23:44 +00:00
2080f419cb 3.16.3 2025-02-27 13:04:01 +00:00
659aae297b fix(PortProxy): Refactored PortProxy to support multiple listening ports and improved modularity. 2025-02-27 13:04:01 +00:00
fcd0f61b5c 3.16.2 2025-02-27 12:54:15 +00:00
7ee35a98e3 fix(PortProxy): Fix port-based routing logic in PortProxy 2025-02-27 12:54:14 +00:00
ea0f6d2270 3.16.1 2025-02-27 12:42:50 +00:00
621ad9e681 fix(core): Updated minor version numbers in dependencies for patch release. 2025-02-27 12:42:50 +00:00
7cea5773ee 3.16.0 2025-02-27 12:41:20 +00:00
a2cb56ba65 feat(PortProxy): Enhancements made to PortProxy settings and capabilities 2025-02-27 12:41:20 +00:00
408b793149 3.15.0 2025-02-27 12:25:48 +00:00
f6c3d2d3d0 feat(classes.portproxy): Add support for port range-based routing with enhanced IP and port validation. 2025-02-27 12:25:48 +00:00
422eb5ec40 3.14.2 2025-02-26 19:00:09 +00:00
45390c4389 fix(PortProxy): Fix cleanup timer reset for PortProxy 2025-02-26 19:00:09 +00:00
0f2e6d688c 3.14.1 2025-02-26 12:56:00 +00:00
3bd7b70c19 fix(PortProxy): Increased default maxConnectionLifetime for PortProxy to 600000 ms 2025-02-26 12:56:00 +00:00
07a82a09be 3.14.0 2025-02-26 10:29:21 +00:00
23253a2731 feat(PortProxy): Introduce max connection lifetime feature 2025-02-26 10:29:21 +00:00
7 changed files with 977 additions and 293 deletions

View File

@ -1,5 +1,177 @@
# Changelog
## 2025-03-03 - 3.22.1 - fix(PortProxy)
Fix connection timeout and IP validation handling for PortProxy
- Adjusted initial data timeout setting for SNI-enabled connections in PortProxy.
- Restored IP validation logic to original behavior, ensuring compatibility with domain configurations.
## 2025-03-03 - 3.22.0 - feat(classes.portproxy)
Enhanced PortProxy to support initial data timeout and improved IP handling
- Added `initialDataTimeout` to PortProxy settings for handling data flow in chained proxies.
- Improved IP validation by allowing relaxed checks in chained proxy setups.
- Introduced dynamic logging for connection lifecycle and proxy configurations.
- Enhanced timeout handling for better proxy resilience.
## 2025-03-03 - 3.21.0 - feat(PortProxy)
Enhancements to connection management in PortProxy
- Introduced a unique ID for each connection record for improved tracking.
- Enhanced cleanup mechanism for connections with dual states: initiated and executed.
- Implemented shutdown process handling to ensure graceful connection closure.
- Added logging for better tracing of connection activities and states.
- Improved connection setup with explicit timeouts and data flow management.
- Integrated inactivity and parity checks to monitor connection health.
## 2025-03-01 - 3.20.2 - fix(PortProxy)
Enhance connection cleanup handling in PortProxy
- Add checks to ensure timers are reset only if outgoing socket is active
- Prevent setting outgoingActive if the connection is already closed
## 2025-03-01 - 3.20.1 - fix(PortProxy)
Improve IP allowance check for forced domains
- Enhanced IP allowance check logic by incorporating blocked IPs and default allowed IPs for forced domains within port proxy configurations.
## 2025-03-01 - 3.20.0 - feat(PortProxy)
Enhance PortProxy with advanced connection cleanup and logging
- Introduced `cleanupConnection` method for improved connection management.
- Added logging for connection cleanup including special conditions.
- Implemented parity check to clean up connections when outgoing side closes but incoming remains active.
- Improved logging during interval checks for active connections and their durations.
## 2025-03-01 - 3.19.0 - feat(PortProxy)
Enhance PortProxy with default blocked IPs
- Introduced defaultBlockedIPs in IPortProxySettings to handle globally blocked IPs.
- Added logic for merging domain-specific and default allowed and blocked IPs for effective IP filtering.
- Refactored helper functions for IP and port range checks to improve modularity in PortProxy.
## 2025-02-27 - 3.18.2 - fix(portproxy)
Fixed typographical errors in comments within PortProxy class.
- Corrected typographical errors in comments within the PortProxy class.
## 2025-02-27 - 3.18.1 - fix(PortProxy)
Refactor and enhance PortProxy test cases and handling
- Refactored test cases in test/test.portproxy.ts for clarity and added coverage.
- Improved TCP server helper functions for better flexibility.
- Fixed issues with domain handling in PortProxy configuration.
- Introduced round-robin logic for multi-IP domains in PortProxy.
- Ensured proper cleanup and stopping of test servers in the test suite.
## 2025-02-27 - 3.18.0 - feat(PortProxy)
Add SNI-based renegotiation handling in PortProxy
- Introduced a new field 'lockedDomain' in IConnectionRecord to store initial SNI.
- Enhanced connection management by enforcing termination if rehandshake is detected with different SNI.
## 2025-02-27 - 3.17.1 - fix(PortProxy)
Fix handling of SNI re-negotiation in PortProxy
- Removed connection locking to the initially negotiated SNI
- Improved handling of SNI during renegotiation in PortProxy
## 2025-02-27 - 3.17.0 - feat(smartproxy)
Enhance description clarity and improve SNI handling with domain locking.
- Improved package description in package.json, readme.md, and npmextra.json for better clarity and keyword optimization.
- Enhanced SNI handling in PortProxy by adding domain locking and extra checks to terminate connections if a different SNI is detected post-handshake.
- Refactored readme.md to better explain the usage and functionalities of the proxy features including SSL redirection, WebSocket handling, and dynamic routing.
## 2025-02-27 - 3.16.9 - fix(portproxy)
Extend domain input validation to support string arrays in port proxy configurations.
- Modify IDomainConfig interface to allow domain specification as string array.
- Update connection setup logic to handle multiple domain patterns.
- Enhance domain rejection logging to include all domain patterns.
## 2025-02-27 - 3.16.8 - fix(PortProxy)
Fix IP filtering for domain and global default allowed lists and improve port-based routing logic.
- Improved logic to prioritize domain-specific allowed IPs over global defaults.
- Fixed port-based rules application to handle global port ranges more effectively.
- Enhanced rejection handling for unauthorized IP addresses in both domain-specific and default global lists.
## 2025-02-27 - 3.16.7 - fix(PortProxy)
Improved IP validation logic in PortProxy to ensure correct domain matching and fallback
- Refactored the setupConnection function inside PortProxy to enhance IP address validation.
- Domain-specific allowed IP preference is applied before default list lookup.
- Removed redundant condition checks to streamline connection rejection paths.
## 2025-02-27 - 3.16.6 - fix(PortProxy)
Optimize connection cleanup logic in PortProxy by removing unnecessary delays.
- Removed multiple await plugins.smartdelay.delayFor(0) calls.
- Improved performance by ensuring timely resource release during connection termination.
## 2025-02-27 - 3.16.5 - fix(PortProxy)
Improved connection cleanup process with added asynchronous delays
- Connection cleanup now includes asynchronous delays for reliable order of operations.
## 2025-02-27 - 3.16.4 - fix(PortProxy)
Fix and enhance port proxy handling
- Ensure that all created proxy servers are correctly checked for listening state.
- Corrected the handling of ports and domain configurations within port proxy setups.
- Expanded test coverage for handling multiple concurrent and chained proxy connections.
## 2025-02-27 - 3.16.3 - fix(PortProxy)
Refactored PortProxy to support multiple listening ports and improved modularity.
- Updated PortProxy to allow multiple listening ports with flexible configuration.
- Moved helper functions for IP and port range checks outside the class for cleaner code structure.
## 2025-02-27 - 3.16.2 - fix(PortProxy)
Fix port-based routing logic in PortProxy
- Optimized the handling and checking of local ports in the global port range.
- Fixed the logic for rejecting or accepting connections based on predefined port ranges.
- Improved handling of the default and specific domain configurations during port-based connections.
## 2025-02-27 - 3.16.1 - fix(core)
Updated minor version numbers in dependencies for patch release.
- No specific file changes detected.
- Dependencies versioning adjusted for stability.
## 2025-02-27 - 3.16.0 - feat(PortProxy)
Enhancements made to PortProxy settings and capabilities
- Added 'forwardAllGlobalRanges' and 'targetIP' to IPortProxySettings.
- Improved PortProxy to forward connections based on domain-specific configurations.
- Added comprehensive handling for global port-range based connection forwarding.
- Enabled forwarding of all connections on global port ranges directly to global target IP.
## 2025-02-27 - 3.15.0 - feat(classes.portproxy)
Add support for port range-based routing with enhanced IP and port validation.
- Introduced globalPortRanges in IPortProxySettings for routing based on port ranges.
- Improved connection handling with port range and domain configuration validations.
- Updated connection logging to include the local port information.
## 2025-02-26 - 3.14.2 - fix(PortProxy)
Fix cleanup timer reset for PortProxy
- Resolved an issue where the cleanup timer in the PortProxy class did not reset correctly if both incoming and outgoing data events were triggered without clearing flags.
## 2025-02-26 - 3.14.1 - fix(PortProxy)
Increased default maxConnectionLifetime for PortProxy to 600000 ms
- Updated PortProxy settings to extend default maxConnectionLifetime to 10 minutes.
## 2025-02-26 - 3.14.0 - feat(PortProxy)
Introduce max connection lifetime feature
- Added an optional maxConnectionLifetime setting for PortProxy.
- Forces cleanup of long-lived connections based on inactivity or lifetime limit.
## 2025-02-25 - 3.13.0 - feat(core)
Add support for tagging iptables rules with comments and cleaning them up on process exit

View File

@ -5,26 +5,26 @@
"githost": "code.foss.global",
"gitscope": "push.rocks",
"gitrepo": "smartproxy",
"description": "A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.",
"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.",
"npmPackagename": "@push.rocks/smartproxy",
"license": "MIT",
"projectDomain": "push.rocks",
"keywords": [
"proxy",
"network traffic",
"high workload",
"http",
"https",
"websocket",
"network routing",
"ssl redirect",
"port mapping",
"reverse proxy",
"authentication",
"network",
"traffic management",
"SSL",
"TLS",
"WebSocket",
"port proxying",
"dynamic routing",
"sni",
"port forwarding",
"real-time applications"
"authentication",
"real-time applications",
"high workload",
"HTTPS",
"reverse proxy",
"server",
"network security"
]
}
},

View File

@ -1,8 +1,8 @@
{
"name": "@push.rocks/smartproxy",
"version": "3.13.0",
"version": "3.22.1",
"private": false,
"description": "A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.",
"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.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
@ -53,20 +53,20 @@
],
"keywords": [
"proxy",
"network traffic",
"high workload",
"http",
"https",
"websocket",
"network routing",
"ssl redirect",
"port mapping",
"reverse proxy",
"authentication",
"network",
"traffic management",
"SSL",
"TLS",
"WebSocket",
"port proxying",
"dynamic routing",
"sni",
"port forwarding",
"real-time applications"
"authentication",
"real-time applications",
"high workload",
"HTTPS",
"reverse proxy",
"server",
"network security"
],
"homepage": "https://code.foss.global/push.rocks/smartproxy#readme",
"repository": {

127
readme.md
View File

@ -1,6 +1,6 @@
# @push.rocks/smartproxy
A proxy for handling high workloads of proxying.
A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.
## Install
@ -14,19 +14,19 @@ This will add `@push.rocks/smartproxy` to your project's dependencies.
## Usage
`@push.rocks/smartproxy` is a comprehensive and versatile package designed to handle complex and high-volume proxying tasks efficiently. It includes features such as SSL redirection, port proxying, WebSocket support, and customizable routing and authentication mechanisms. This guide will provide a detailed walkthrough of how to harness these capabilities effectively.
`@push.rocks/smartproxy` is a comprehensive package that provides advanced functionalities for handling proxy tasks efficiently, including SSL redirection, port proxying, WebSocket support, and dynamic routing with authentication capabilities. Here is an extensive guide on how to utilize these features effectively, ensuring robust and secure proxy operations.
### Initial Setup
Before diving into specific features, let's start by configuring and setting up our basic proxy server:
Before exploring the advanced features of `smartproxy`, you need to set up a basic proxy server. This setup serves as the foundation for incorporating additional functionalities later on:
```typescript
import { NetworkProxy } from '@push.rocks/smartproxy';
// Instantiate the NetworkProxy with desired options
// Create an instance of NetworkProxy with the desired configuration
const myNetworkProxy = new NetworkProxy({ port: 443 });
// Define reverse proxy configurations
// Define reverse proxy configurations for the domains you wish to proxy
const proxyConfigs = [
{
destinationIp: '127.0.0.1',
@ -39,16 +39,16 @@ PRIVATE_KEY_CONTENT
CERTIFICATE_CONTENT
-----END CERTIFICATE-----`,
},
// More configurations can be added here
// Additional configurations can be added here
];
// Start the network proxy
// Start the network proxy to enable forwarding
await myNetworkProxy.start();
// Apply proxy configurations
// Apply the configurations you defined earlier
await myNetworkProxy.updateProxyConfigs(proxyConfigs);
// Optionally add default headers to all responses
// Optionally, you can set default headers to be included in all responses
await myNetworkProxy.addDefaultHeaders({
'X-Powered-By': 'smartproxy',
});
@ -56,44 +56,45 @@ await myNetworkProxy.addDefaultHeaders({
### Configuring SSL Redirection
One essential capability of a robust proxy server is ensuring that all HTTP traffic is redirected to secure HTTPS endpoints. This can be effortlessly accomplished using the `SslRedirect` class within `smartproxy`. This class listens on port 80 (HTTP) and redirects all incoming requests to HTTPS:
A critical feature of modern proxy servers is the ability to redirect HTTP traffic to secure HTTPS endpoints. The `SslRedirect` class in `smartproxy` simplifies this process by automatically redirecting requests from HTTP port 80 to HTTPS:
```typescript
import { SslRedirect } from '@push.rocks/smartproxy';
// Instantiate the SslRedirect for listening on port 80
// Create an SslRedirect instance to listen on port 80
const mySslRedirect = new SslRedirect(80);
// Start listening and redirect HTTP traffic to HTTPS
// Start the redirect to enforce HTTPS
await mySslRedirect.start();
// To stop redirection, you can use the following command:
// To stop HTTP redirection, use the following command:
await mySslRedirect.stop();
```
### Handling Complex Networking with Port Proxy
### Managing Port Proxying
Port proxying allows redirection of traffic from one port to another. This capability is crucial when dealing with services that need dynamic port forwarding, or when adapting to infrastructure changes without downtime. Smartproxy's `PortProxy` class handles this efficiently:
Port proxying is essential for forwarding traffic from one port to another, an important feature for services that require dynamic port changes without downtime. Smartproxy's `PortProxy` class efficiently handles these scenarios:
```typescript
import { PortProxy } from '@push.rocks/smartproxy';
// Create a PortProxy to directly forward traffic from port 5000 to 3000
// Set up a PortProxy to forward traffic from port 5000 to 3000
const myPortProxy = new PortProxy(5000, 3000);
// Initiate the port proxy
await myPortProxy.start();
// To stop the port proxy mechanism:
// To halt the port proxy, execute:
await myPortProxy.stop();
```
Additionally, smartproxy's port proxying can support intricate scenarios where different forwarding rules are configured based on domain names or allowed IPs:
For more intricate setups—such as forwarding based on specific domain rules or IP allowances—smartproxy allows detailed configurations:
```typescript
import { PortProxy } from '@push.rocks/smartproxy';
const myComplexPortProxy = new PortProxy({
// Configure complex port proxy rules
const advancedPortProxy = new PortProxy({
fromPort: 6000,
toPort: 3000,
domains: [
@ -102,57 +103,60 @@ const myComplexPortProxy = new PortProxy({
allowedIPs: ['192.168.0.*', '127.0.0.1'],
targetIP: '192.168.1.100'
}
// Define more domain-specific rules if needed
// Additional domain rules can be added as needed
],
sniEnabled: true, // if SNI (Server Name Indication) is desired
defaultAllowedIPs: ['*']);
sniEnabled: true, // Server Name Indication (SNI) support
defaultAllowedIPs: ['*'],
});
// Start listening for complex routing requests
await myComplexPortProxy.start();
// Activate the proxy with conditional rules
await advancedPortProxy.start();
```
### WebSocket Support and Load Handling
### WebSocket Handling
With the advent of real-time applications, efficient WebSocket handling in proxies is crucial. Smartproxy integrates WebSocket support seamlessly, enabling it to proxy WebSocket traffic while maintaining security and performance:
With real-time applications becoming more prevalent, effective WebSocket handling is crucial in a proxy server. Smartproxy natively incorporates WebSocket support to manage WebSocket traffic securely and efficiently:
```typescript
import { NetworkProxy } from '@push.rocks/smartproxy';
const wsProxy = new NetworkProxy({ port: 443 });
// Create a NetworkProxy instance for WebSocket traffic
const wsNetworkProxy = new NetworkProxy({ port: 443 });
// Assume reverse proxy configurations with WebSocket intentions
const wsProxyConfigs = [
// Define proxy configurations targeted for WebSocket traffic
const websocketConfig = [
{
destinationIp: '127.0.0.1',
destinationPort: '8080',
hostName: 'socket.example.com',
// Add further options such as keys for SSL if needed
// Include SSL details if necessary
}
];
// Start the network proxy with WebSocket capabilities
await wsProxy.start();
await wsProxy.updateProxyConfigs(wsProxyConfigs);
// Start the proxy and apply WebSocket settings
await wsNetworkProxy.start();
await wsNetworkProxy.updateProxyConfigs(websocketConfig);
// Ensure WebSocket connections remain alive
wsProxy.heartbeatInterval = setInterval(() => {
// logic for keeping connections alive and healthy
}, 60000); // Every 60 seconds
// Set heartbeat intervals to maintain WebSocket connections
wsNetworkProxy.heartbeatInterval = setInterval(() => {
// Logic for connection health checks
}, 60000); // every minute
// Gracefully handle server or connection errors to maintain uptime
wsProxy.httpsServer.on('error', (error) => console.log('Server Error:', error));
// Capture and handle server errors for resiliency
wsNetworkProxy.httpsServer.on('error', (error) => console.log('Server Error:', error));
```
### Comprehensive Routing and Advanced Features
### Advanced Routing and Custom Features
Smartproxy supports dynamic and customizable request routing based on the incoming request's destination. This feature enables extensive use-case scenarios, from simple API endpoint redirection to elaborate B2B service integrations:
Smartproxy shines with its dynamic routing capabilities, allowing for custom and advanced request routing based on the request's destination. This enables extensive flexibility, such as directing API requests or facilitating intricate B2B integrations:
```typescript
import { NetworkProxy } from '@push.rocks/smartproxy';
const dynamicRoutingProxy = new NetworkProxy({ port: 8443 });
dynamicRoutingProxy.router.setNewProxyConfigs([
// Instantiate a proxy with dynamic routing
const routeProxy = new NetworkProxy({ port: 8443 });
routeProxy.router.setNewProxyConfigs([
{
destinationIp: '192.168.1.150',
destinationPort: '80',
@ -165,57 +169,60 @@ dynamicRoutingProxy.router.setNewProxyConfigs([
}
]);
await dynamicRoutingProxy.start();
// Activate the routing proxy
await routeProxy.start();
```
For those dealing with high volume or regulatory needs, the integration of tools like `iptables` allows broad control over network traffic:
For those who require granular traffic control, integrating tools like `iptables` offers additional power over network management:
```typescript
import { IPTablesProxy } from '@push.rocks/smartproxy';
// Setting up iptables for advanced network management
const ipTablesProxy = new IPTablesProxy({
// Set up IPTables for sophisticated network traffic management
const iptablesProxy = new IPTablesProxy({
fromPort: 8081,
toPort: 8080,
deleteOnExit: true // clean rules upon server shutdown
deleteOnExit: true // Clean up rules when the server shuts down
});
// Begin routing with IPTables
await ipTablesProxy.start();
// Enable routing through IPTables
await iptablesProxy.start();
```
### Combining with HTTP and HTTPS Credentials
### Integrating SSL and HTTP/HTTPS Credentials
When undertaking proxy configurations, handling sensitive data like SSL certificates and keys securely is imperative:
Handling sensitive data like SSL keys and certificates securely is crucial in proxy configurations:
```typescript
import { loadDefaultCertificates } from '@push.rocks/smartproxy';
try {
const { privateKey, publicKey } = loadDefaultCertificates(); // adjust path as needed
console.log('Certificates loaded.');
// Use these certificates in your SSL-based configurations
const { privateKey, publicKey } = loadDefaultCertificates(); // Adjust path if necessary
console.log('SSL certificates loaded successfully.');
// Use these credentials in your configurations
} catch (error) {
console.error('Cannot load certificates:', error);
console.error('Error loading certificates:', error);
}
```
### Testing and Validation
Given these powerful capabilities, rigorous testing of configurations and functionality using frameworks like `tap` can ensure high-quality and reliable proxy configurations. Smartproxy integrates with Typescript test setups:
Smartproxy supports extensive testing to ensure your proxy configurations operate as expected. Leveraging `tap` alongside TypeScript testing frameworks supports quality assurance:
```typescript
import { expect, tap } from '@push.rocks/tapbundle';
import { NetworkProxy } from '@push.rocks/smartproxy';
tap.test('proxied request should return status 200', async () => {
// Your test logic here
tap.test('Check proxied request returns status 200', async () => {
// Testing logic
});
tap.start();
```
In summary, `@push.rocks/smartproxy` offers a plethora of solutions tailored to both common and sophisticated proxying needs. Whether you're seeking straightforward port forwarding, secure SSL redirection, WebSocket management, or robust network routing controls, smartproxy provides the right tools for efficient and effective proxy operations. Through its integration simplicity and versatile configurations, developers can ensure high performance and secure proxying across various environments and applications.
### Conclusion
`@push.rocks/smartproxy` is designed for both simple and complex proxying demands, offering tools for high-performance and secure proxy management across diverse environments. Its efficient configurations are capable of supporting SSL redirection, WebSocket traffic, dynamic routing, and other advanced functionalities, making it indispensable for developers seeking robust and adaptable proxy solutions. By integrating these capabilities with ease of use, `smartproxy` stands out as an essential tool in modern software architecture.
## License and Legal Information

View File

@ -8,85 +8,79 @@ const TEST_SERVER_PORT = 4000;
const PROXY_PORT = 4001;
const TEST_DATA = 'Hello through port proxy!';
// Helper function to create a test TCP server
function createTestServer(port: number): Promise<net.Server> {
// Helper: Creates a test TCP server that listens on a given port and host.
function createTestServer(port: number, host: string = 'localhost'): Promise<net.Server> {
return new Promise((resolve) => {
const server = net.createServer((socket) => {
socket.on('data', (data) => {
// Echo the received data back
// Echo the received data back with a prefix.
socket.write(`Echo: ${data.toString()}`);
});
socket.on('error', (error) => {
console.error('[Test Server] Socket error:', error);
console.error(`[Test Server] Socket error on ${host}:${port}:`, error);
});
});
server.listen(port, () => {
console.log(`[Test Server] Listening on port ${port}`);
server.listen(port, host, () => {
console.log(`[Test Server] Listening on ${host}:${port}`);
resolve(server);
});
});
}
// Helper function to create a test client connection
// Helper: Creates a test client connection.
function createTestClient(port: number, data: string): Promise<string> {
return new Promise((resolve, reject) => {
const client = new net.Socket();
let response = '';
client.connect(port, 'localhost', () => {
console.log('[Test Client] Connected to server');
client.write(data);
});
client.on('data', (chunk) => {
response += chunk.toString();
client.end();
});
client.on('end', () => {
resolve(response);
});
client.on('error', (error) => {
reject(error);
});
client.on('end', () => resolve(response));
client.on('error', (error) => reject(error));
});
}
// Setup test environment
// SETUP: Create a test server and a PortProxy instance.
tap.test('setup port proxy test environment', async () => {
testServer = await createTestServer(TEST_SERVER_PORT);
portProxy = new PortProxy({
fromPort: PROXY_PORT,
toPort: TEST_SERVER_PORT,
toHost: 'localhost',
domains: [],
targetIP: 'localhost',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1']
defaultAllowedIPs: ['127.0.0.1'],
globalPortRanges: []
});
});
// Test that the proxy starts and its servers are listening.
tap.test('should start port proxy', async () => {
await portProxy.start();
expect(portProxy.netServer.listening).toBeTrue();
expect((portProxy as any).netServers.every((server: net.Server) => server.listening)).toBeTrue();
});
// Test basic TCP forwarding.
tap.test('should forward TCP connections and data to localhost', async () => {
const response = await createTestClient(PROXY_PORT, TEST_DATA);
expect(response).toEqual(`Echo: ${TEST_DATA}`);
});
// Test proxy with a custom target host.
tap.test('should forward TCP connections to custom host', async () => {
// Create a new proxy instance with a custom host
const customHostProxy = new PortProxy({
fromPort: PROXY_PORT + 1,
toPort: TEST_SERVER_PORT,
toHost: '127.0.0.1',
domains: [],
targetIP: '127.0.0.1',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1']
defaultAllowedIPs: ['127.0.0.1'],
globalPortRanges: []
});
await customHostProxy.start();
@ -95,148 +89,151 @@ tap.test('should forward TCP connections to custom host', async () => {
await customHostProxy.stop();
});
tap.test('should forward connections based on domain-specific target IP', async () => {
// Create a second test server on a different port
const TEST_SERVER_PORT_2 = TEST_SERVER_PORT + 100;
const testServer2 = await createTestServer(TEST_SERVER_PORT_2);
// Test forced domain routing via port-range configuration.
// In this test, we want to forward to a different IP (using '127.0.0.2')
// while keeping the same port. We create a test server on '127.0.0.2'.
tap.test('should forward connections based on domain-specific target IP (forced domain via port-range)', async () => {
const forcedProxyPort = PROXY_PORT + 2;
// Create a test server listening on '127.0.0.2' at forcedProxyPort.
const testServer2 = await createTestServer(forcedProxyPort, '127.0.0.2');
// Create a proxy with domain-specific target IPs
const domainProxy = new PortProxy({
fromPort: PROXY_PORT + 2,
toPort: TEST_SERVER_PORT, // default port
toHost: 'localhost', // default host
domains: [{
domain: 'domain1.test',
fromPort: forcedProxyPort,
toPort: TEST_SERVER_PORT, // default target port (unused for forced domain)
targetIP: 'localhost',
domainConfigs: [{
domains: ['forced.test'],
allowedIPs: ['127.0.0.1'],
targetIP: '127.0.0.1'
}, {
domain: 'domain2.test',
allowedIPs: ['127.0.0.1'],
targetIP: 'localhost'
targetIPs: ['127.0.0.2'], // Use a different IP than the default.
portRanges: [{ from: forcedProxyPort, to: forcedProxyPort }]
}],
sniEnabled: false, // We'll test without SNI first since this is a TCP proxy test
defaultAllowedIPs: ['127.0.0.1']
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
globalPortRanges: [{ from: forcedProxyPort, to: forcedProxyPort }]
});
await domainProxy.start();
// Test default connection (should use default host)
const response1 = await createTestClient(PROXY_PORT + 2, TEST_DATA);
expect(response1).toEqual(`Echo: ${TEST_DATA}`);
// Create another proxy with different default host
const domainProxy2 = new PortProxy({
fromPort: PROXY_PORT + 3,
toPort: TEST_SERVER_PORT,
toHost: '127.0.0.1',
domains: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1']
});
await domainProxy2.start();
const response2 = await createTestClient(PROXY_PORT + 3, TEST_DATA);
expect(response2).toEqual(`Echo: ${TEST_DATA}`);
// When connecting to forcedProxyPort, forced domain handling triggers,
// so the proxy will connect to '127.0.0.2' on the same port.
const response = await createTestClient(forcedProxyPort, TEST_DATA);
expect(response).toEqual(`Echo: ${TEST_DATA}`);
await domainProxy.stop();
await domainProxy2.stop();
await new Promise<void>((resolve) => testServer2.close(() => resolve()));
});
// Test handling of multiple concurrent connections.
tap.test('should handle multiple concurrent connections', async () => {
const concurrentRequests = 5;
const requests = Array(concurrentRequests).fill(null).map((_, i) =>
const requests = Array(concurrentRequests).fill(null).map((_, i) =>
createTestClient(PROXY_PORT, `${TEST_DATA} ${i + 1}`)
);
const responses = await Promise.all(requests);
responses.forEach((response, i) => {
expect(response).toEqual(`Echo: ${TEST_DATA} ${i + 1}`);
});
});
// Test connection timeout handling.
tap.test('should handle connection timeouts', async () => {
const client = new net.Socket();
await new Promise<void>((resolve) => {
client.connect(PROXY_PORT, 'localhost', () => {
// Don't send any data, just wait for timeout
client.on('close', () => {
resolve();
});
// Do not send any data to trigger a timeout.
client.on('close', () => resolve());
});
});
});
// Test stopping the port proxy.
tap.test('should stop port proxy', async () => {
await portProxy.stop();
expect(portProxy.netServer.listening).toBeFalse();
expect((portProxy as any).netServers.every((server: net.Server) => !server.listening)).toBeTrue();
});
// Cleanup
// Test chained proxies with and without source IP preservation.
tap.test('should support optional source IP preservation in chained proxies', async () => {
// Test 1: Without IP preservation (default behavior)
// Chained proxies without IP preservation.
const firstProxyDefault = new PortProxy({
fromPort: PROXY_PORT + 4,
toPort: PROXY_PORT + 5,
toHost: 'localhost',
domains: [],
targetIP: 'localhost',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
globalPortRanges: []
});
const secondProxyDefault = new PortProxy({
fromPort: PROXY_PORT + 5,
toPort: TEST_SERVER_PORT,
toHost: 'localhost',
domains: [],
targetIP: 'localhost',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
globalPortRanges: []
});
await secondProxyDefault.start();
await firstProxyDefault.start();
// This should work because we explicitly allow both IPv4 and IPv6 formats
const response1 = await createTestClient(PROXY_PORT + 4, TEST_DATA);
expect(response1).toEqual(`Echo: ${TEST_DATA}`);
await firstProxyDefault.stop();
await secondProxyDefault.stop();
// Test 2: With IP preservation
// Chained proxies with IP preservation.
const firstProxyPreserved = new PortProxy({
fromPort: PROXY_PORT + 6,
toPort: PROXY_PORT + 7,
toHost: 'localhost',
domains: [],
targetIP: 'localhost',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
preserveSourceIP: true
preserveSourceIP: true,
globalPortRanges: []
});
const secondProxyPreserved = new PortProxy({
fromPort: PROXY_PORT + 7,
toPort: TEST_SERVER_PORT,
toHost: 'localhost',
domains: [],
targetIP: 'localhost',
domainConfigs: [],
sniEnabled: false,
defaultAllowedIPs: ['127.0.0.1'],
preserveSourceIP: true
preserveSourceIP: true,
globalPortRanges: []
});
await secondProxyPreserved.start();
await firstProxyPreserved.start();
// This should work with just IPv4 because source IP is preserved
const response2 = await createTestClient(PROXY_PORT + 6, TEST_DATA);
expect(response2).toEqual(`Echo: ${TEST_DATA}`);
await firstProxyPreserved.stop();
await secondProxyPreserved.stop();
});
// Test round-robin behavior for multiple target IPs in a domain config.
tap.test('should use round robin for multiple target IPs in domain config', async () => {
const domainConfig = {
domains: ['rr.test'],
allowedIPs: ['127.0.0.1'],
targetIPs: ['hostA', 'hostB']
} as any;
const proxyInstance = new PortProxy({
fromPort: 0,
toPort: 0,
targetIP: 'localhost',
domainConfigs: [domainConfig],
sniEnabled: false,
defaultAllowedIPs: [],
globalPortRanges: []
});
const firstTarget = (proxyInstance as any).getTargetIP(domainConfig);
const secondTarget = (proxyInstance as any).getTargetIP(domainConfig);
expect(firstTarget).toEqual('hostA');
expect(secondTarget).toEqual('hostB');
});
// CLEANUP: Tear down the test server.
tap.test('cleanup port proxy test environment', async () => {
await new Promise<void>((resolve) => testServer.close(() => resolve()));
});
@ -245,9 +242,9 @@ process.on('exit', () => {
if (testServer) {
testServer.close();
}
if (portProxy && portProxy.netServer) {
if (portProxy && (portProxy as any).netServers) {
portProxy.stop();
}
});
export default tap.start();
export default tap.start();

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '3.13.0',
description: 'A robust and versatile proxy package designed to handle high workloads, offering features like SSL redirection, port proxying, WebSocket support, and customizable routing and authentication.'
version: '3.22.1',
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.'
}

View File

@ -1,19 +1,29 @@
import * as plugins from './plugins.js';
/** Domain configuration with per-domain allowed port ranges */
export interface IDomainConfig {
domain: string; // Glob pattern for domain
allowedIPs: string[]; // Glob patterns for allowed IPs
targetIP?: string; // Optional target IP for this domain
domains: string[]; // Glob patterns for domain(s)
allowedIPs: string[]; // Glob patterns for allowed IPs
blockedIPs?: string[]; // Glob patterns for blocked IPs
targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
}
/** Port proxy settings including global allowed port ranges */
export interface IPortProxySettings extends plugins.tls.TlsOptions {
fromPort: number;
toPort: number;
toHost?: string; // Target host to proxy to, defaults to 'localhost'
domains: IDomainConfig[];
targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
domainConfigs: IDomainConfig[];
sniEnabled?: boolean;
defaultAllowedIPs?: string[];
defaultBlockedIPs?: string[];
preserveSourceIP?: boolean;
maxConnectionLifetime?: number; // (ms) force cleanup of long-lived connections
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
initialDataTimeout?: number; // (ms) timeout for receiving initial data, useful for chained proxies
}
/**
@ -84,15 +94,60 @@ interface IConnectionRecord {
outgoing: plugins.net.Socket | null;
incomingStartTime: number;
outgoingStartTime?: number;
outgoingClosedTime?: number;
lockedDomain?: string; // Field to lock this connection to the initial SNI
connectionClosed: boolean;
cleanupTimer?: NodeJS.Timeout; // Timer to force cleanup after max lifetime/inactivity
cleanupInitiated: boolean; // Flag to track if cleanup has been initiated but not completed
id: string; // Unique identifier for the connection
lastActivity: number; // Timestamp of last activity on either socket
}
// Helper: Check if a port falls within any of the given port ranges.
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
return ranges.some(range => port >= range.from && port <= range.to);
};
// Helper: Check if a given IP matches any of the glob patterns.
const isAllowed = (ip: string, patterns: string[]): boolean => {
const normalizeIP = (ip: string): string[] => {
if (ip.startsWith('::ffff:')) {
const ipv4 = ip.slice(7);
return [ip, ipv4];
}
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
return [ip, `::ffff:${ip}`];
}
return [ip];
};
const normalizedIPVariants = normalizeIP(ip);
const expandedPatterns = patterns.flatMap(normalizeIP);
return normalizedIPVariants.some(ipVariant =>
expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
);
};
// Helper: Check if an IP is allowed considering allowed and blocked glob patterns.
const isGlobIPAllowed = (ip: string, allowed: string[], blocked: string[] = []): boolean => {
if (blocked.length > 0 && isAllowed(ip, blocked)) return false;
return isAllowed(ip, allowed);
};
// Helper: Generate a unique ID for a connection
const generateConnectionId = (): string => {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
};
export class PortProxy {
netServer: plugins.net.Server;
private netServers: plugins.net.Server[] = [];
settings: IPortProxySettings;
// Unified record tracking each connection pair.
private connectionRecords: Set<IConnectionRecord> = new Set();
private connectionRecords: Map<string, IConnectionRecord> = new Map();
private connectionLogger: NodeJS.Timeout | null = null;
private isShuttingDown: boolean = false;
// Map to track round robin indices for each domain config.
private domainTargetIndices: Map<IDomainConfig, number> = new Map();
private terminationStats: {
incoming: Record<string, number>;
@ -102,72 +157,160 @@ export class PortProxy {
outgoing: {},
};
constructor(settings: IPortProxySettings) {
constructor(settingsArg: IPortProxySettings) {
this.settings = {
...settings,
toHost: settings.toHost || 'localhost',
...settingsArg,
targetIP: settingsArg.targetIP || 'localhost',
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
};
// Debug logging for constructor settings
console.log(`PortProxy initialized with targetIP: ${this.settings.targetIP}, toPort: ${this.settings.toPort}, fromPort: ${this.settings.fromPort}, sniEnabled: ${this.settings.sniEnabled}`);
}
private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
}
/**
* Initiates the cleanup process for a connection.
* Sets the flag to prevent duplicate cleanup attempts and schedules actual cleanup.
*/
private initiateCleanup(record: IConnectionRecord, reason: string = 'normal'): void {
if (record.cleanupInitiated) return;
record.cleanupInitiated = true;
const remoteIP = record.incoming.remoteAddress || 'unknown';
console.log(`Initiating cleanup for connection ${record.id} from ${remoteIP} (reason: ${reason})`);
// Execute cleanup immediately to prevent lingering connections
this.executeCleanup(record);
}
/**
* Executes the actual cleanup of a connection.
* Destroys sockets, clears timers, and removes the record.
*/
private executeCleanup(record: IConnectionRecord): void {
if (record.connectionClosed) return;
record.connectionClosed = true;
const remoteIP = record.incoming.remoteAddress || 'unknown';
if (record.cleanupTimer) {
clearTimeout(record.cleanupTimer);
record.cleanupTimer = undefined;
}
// End the sockets first to allow for graceful closure
try {
if (!record.incoming.destroyed) {
record.incoming.end();
// Set a safety timeout to force destroy if end doesn't complete
setTimeout(() => {
if (!record.incoming.destroyed) {
console.log(`Forcing destruction of incoming socket for ${remoteIP}`);
record.incoming.destroy();
}
}, 1000);
}
} catch (err) {
console.error(`Error ending incoming socket for ${remoteIP}:`, err);
if (!record.incoming.destroyed) {
record.incoming.destroy();
}
}
try {
if (record.outgoing && !record.outgoing.destroyed) {
record.outgoing.end();
// Set a safety timeout to force destroy if end doesn't complete
setTimeout(() => {
if (record.outgoing && !record.outgoing.destroyed) {
console.log(`Forcing destruction of outgoing socket for ${remoteIP}`);
record.outgoing.destroy();
}
}, 1000);
}
} catch (err) {
console.error(`Error ending outgoing socket for ${remoteIP}:`, err);
if (record.outgoing && !record.outgoing.destroyed) {
record.outgoing.destroy();
}
}
// Remove the record after a delay to ensure all events have propagated
setTimeout(() => {
this.connectionRecords.delete(record.id);
console.log(`Connection ${record.id} from ${remoteIP} fully cleaned up. Active connections: ${this.connectionRecords.size}`);
}, 2000);
}
private getTargetIP(domainConfig: IDomainConfig): string {
if (domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
const currentIndex = this.domainTargetIndices.get(domainConfig) || 0;
const ip = domainConfig.targetIPs[currentIndex % domainConfig.targetIPs.length];
this.domainTargetIndices.set(domainConfig, currentIndex + 1);
return ip;
}
return this.settings.targetIP!;
}
/**
* Updates the last activity timestamp for a connection record
*/
private updateActivity(record: IConnectionRecord): void {
record.lastActivity = Date.now();
// Reset the inactivity timer if one is set
if (this.settings.maxConnectionLifetime && record.cleanupTimer) {
clearTimeout(record.cleanupTimer);
// Set a new cleanup timer
record.cleanupTimer = setTimeout(() => {
const now = Date.now();
const inactivityTime = now - record.lastActivity;
const remoteIP = record.incoming.remoteAddress || 'unknown';
console.log(`Connection ${record.id} from ${remoteIP} exceeded max lifetime or inactivity period (${inactivityTime}ms), forcing cleanup.`);
this.initiateCleanup(record, 'timeout');
}, this.settings.maxConnectionLifetime);
}
}
public async start() {
// Helper to forcefully destroy sockets.
const cleanUpSockets = (socketA: plugins.net.Socket, socketB?: plugins.net.Socket) => {
if (!socketA.destroyed) socketA.destroy();
if (socketB && !socketB.destroyed) socketB.destroy();
};
// Normalize an IP to include both IPv4 and IPv6 representations.
const normalizeIP = (ip: string): string[] => {
if (ip.startsWith('::ffff:')) {
const ipv4 = ip.slice(7);
return [ip, ipv4];
// Define a unified connection handler for all listening ports.
const connectionHandler = (socket: plugins.net.Socket) => {
if (this.isShuttingDown) {
socket.end();
socket.destroy();
return;
}
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
return [ip, `::ffff:${ip}`];
}
return [ip];
};
// Check if a given IP matches any of the glob patterns.
const isAllowed = (ip: string, patterns: string[]): boolean => {
const normalizedIPVariants = normalizeIP(ip);
const expandedPatterns = patterns.flatMap(normalizeIP);
return normalizedIPVariants.some(ipVariant =>
expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
);
};
// Find a matching domain config based on the SNI.
const findMatchingDomain = (serverName: string): IDomainConfig | undefined =>
this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => {
const remoteIP = socket.remoteAddress || '';
const localPort = socket.localPort; // The port on which this connection was accepted.
const connectionId = generateConnectionId();
const connectionRecord: IConnectionRecord = {
id: connectionId,
incoming: socket,
outgoing: null,
incomingStartTime: Date.now(),
lastActivity: Date.now(),
connectionClosed: false,
cleanupInitiated: false
};
this.connectionRecords.add(connectionRecord);
console.log(`New connection from ${remoteIP}. Active connections: ${this.connectionRecords.size}`);
this.connectionRecords.set(connectionId, connectionRecord);
console.log(`New connection ${connectionId} from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
let initialDataReceived = false;
let incomingTerminationReason: string | null = null;
let outgoingTerminationReason: string | null = null;
// Ensure cleanup happens only once for the entire connection record.
const cleanupOnce = () => {
if (!connectionRecord.connectionClosed) {
connectionRecord.connectionClosed = true;
cleanUpSockets(connectionRecord.incoming, connectionRecord.outgoing || undefined);
this.connectionRecords.delete(connectionRecord);
console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`);
}
// Local cleanup function that delegates to the class method.
const initiateCleanupOnce = (reason: string = 'normal') => {
this.initiateCleanup(connectionRecord, reason);
};
// Helper to reject an incoming connection.
@ -178,14 +321,70 @@ export class PortProxy {
incomingTerminationReason = reason;
this.incrementTerminationStat('incoming', reason);
}
cleanupOnce();
initiateCleanupOnce(reason);
};
// IMPORTANT: We won't set any initial timeout for a chained proxy scenario
// The code below is commented out to restore original behavior
/*
let initialTimeout: NodeJS.Timeout | null = null;
const initialTimeoutMs = this.settings.initialDataTimeout ||
(this.settings.sniEnabled ? 15000 : 0);
if (initialTimeoutMs > 0) {
console.log(`Setting initial data timeout of ${initialTimeoutMs}ms for connection from ${remoteIP}`);
initialTimeout = setTimeout(() => {
if (!initialDataReceived) {
console.log(`Initial connection timeout for ${remoteIP} (no data received after ${initialTimeoutMs}ms)`);
if (incomingTerminationReason === null) {
incomingTerminationReason = 'initial_timeout';
this.incrementTerminationStat('incoming', 'initial_timeout');
}
initiateCleanupOnce('initial_timeout');
}
}, initialTimeoutMs);
} else {
console.log(`No initial timeout set for connection from ${remoteIP} (likely chained proxy)`);
initialDataReceived = true;
}
*/
// Original behavior: only set timeout if SNI is enabled, and use a fixed 5 second timeout
let initialTimeout: NodeJS.Timeout | null = null;
if (this.settings.sniEnabled) {
console.log(`Setting 5 second initial timeout for SNI extraction from ${remoteIP}`);
initialTimeout = setTimeout(() => {
if (!initialDataReceived) {
console.log(`Initial data timeout for ${remoteIP}`);
socket.end();
initiateCleanupOnce('initial_timeout');
}
}, 5000);
} else {
initialDataReceived = true;
}
socket.on('error', (err: Error) => {
const errorMessage = initialDataReceived
? `(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`
: `(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`;
console.log(errorMessage);
// Clear the initial timeout if it exists
if (initialTimeout) {
clearTimeout(initialTimeout);
initialTimeout = null;
}
// For premature errors, we need to handle them explicitly
// since the standard error handlers might not be set up yet
if (!initialDataReceived) {
if (incomingTerminationReason === null) {
incomingTerminationReason = 'premature_error';
this.incrementTerminationStat('incoming', 'premature_error');
}
initiateCleanupOnce('premature_error');
}
});
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
@ -194,9 +393,13 @@ export class PortProxy {
if (code === 'ECONNRESET') {
reason = 'econnreset';
console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
} else if (code === 'ECONNREFUSED') {
reason = 'econnrefused';
console.log(`ECONNREFUSED on ${side} side from ${remoteIP}: ${err.message}`);
} else {
console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
}
if (side === 'incoming' && incomingTerminationReason === null) {
incomingTerminationReason = reason;
this.incrementTerminationStat('incoming', reason);
@ -204,131 +407,381 @@ export class PortProxy {
outgoingTerminationReason = reason;
this.incrementTerminationStat('outgoing', reason);
}
cleanupOnce();
initiateCleanupOnce(reason);
};
const handleClose = (side: 'incoming' | 'outgoing') => () => {
console.log(`Connection closed on ${side} side from ${remoteIP}`);
if (side === 'incoming' && incomingTerminationReason === null) {
incomingTerminationReason = 'normal';
this.incrementTerminationStat('incoming', 'normal');
} else if (side === 'outgoing' && outgoingTerminationReason === null) {
outgoingTerminationReason = 'normal';
this.incrementTerminationStat('outgoing', 'normal');
// Record the time when outgoing socket closed.
connectionRecord.outgoingClosedTime = Date.now();
// If incoming is still active but outgoing closed, set a shorter timeout
if (!connectionRecord.incoming.destroyed) {
console.log(`Outgoing socket closed but incoming still active for ${remoteIP}. Setting cleanup timeout.`);
setTimeout(() => {
if (!connectionRecord.connectionClosed && !connectionRecord.incoming.destroyed) {
console.log(`Incoming socket still active ${Date.now() - connectionRecord.outgoingClosedTime!}ms after outgoing closed for ${remoteIP}. Cleaning up.`);
initiateCleanupOnce('outgoing_closed_timeout');
}
}, 10000); // 10 second timeout instead of waiting for the next parity check
}
}
// If both sides are closed/destroyed, clean up
if ((side === 'incoming' && connectionRecord.outgoing?.destroyed) ||
(side === 'outgoing' && connectionRecord.incoming.destroyed)) {
initiateCleanupOnce('both_closed');
}
cleanupOnce();
};
const setupConnection = (serverName: string, initialChunk?: Buffer) => {
const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
if (!defaultAllowed && serverName) {
const domainConfig = findMatchingDomain(serverName);
if (!domainConfig) {
return rejectIncomingConnection('rejected', `Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
}
if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
}
} else if (!defaultAllowed && !serverName) {
return rejectIncomingConnection('rejected', `Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
} else if (defaultAllowed && !serverName) {
console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
/**
* Sets up the connection to the target host.
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
* @param initialChunk - Optional initial data chunk.
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
* @param overridePort - If provided, use this port for the outgoing connection.
*/
const setupConnection = (serverName: string, initialChunk?: Buffer, forcedDomain?: IDomainConfig, overridePort?: number) => {
// Clear the initial timeout since we've received data
if (initialTimeout) {
clearTimeout(initialTimeout);
}
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
const domainConfig = forcedDomain
? forcedDomain
: (serverName ? this.settings.domainConfigs.find(config =>
config.domains.some(d => plugins.minimatch(serverName, d))
) : undefined);
const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
const targetHost = domainConfig?.targetIP || this.settings.toHost!;
// Effective IP check: merge allowed IPs with default allowed, and remove blocked IPs.
// Use original domain configuration and IP validation logic
// This restores the behavior that was working before
if (domainConfig) {
const effectiveAllowedIPs: string[] = [
...domainConfig.allowedIPs,
...(this.settings.defaultAllowedIPs || [])
];
const effectiveBlockedIPs: string[] = [
...(domainConfig.blockedIPs || []),
...(this.settings.defaultBlockedIPs || [])
];
// Special case: if allowedIPs is empty, skip IP validation for backward compatibility
if (domainConfig.allowedIPs.length > 0 && !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
}
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`);
}
}
// If no IP validation rules, allow the connection (original behavior)
const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
const connectionOptions: plugins.net.NetConnectOpts = {
host: targetHost,
port: this.settings.toPort,
port: overridePort !== undefined ? overridePort : this.settings.toPort,
};
if (this.settings.preserveSourceIP) {
connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
}
// Add explicit connection timeout and error handling
let connectionTimeout: NodeJS.Timeout | null = null;
let connectionSucceeded = false;
// Set connection timeout - longer for chained proxies
connectionTimeout = setTimeout(() => {
if (!connectionSucceeded) {
console.log(`Connection timeout connecting to ${targetHost}:${connectionOptions.port} for ${remoteIP}`);
if (outgoingTerminationReason === null) {
outgoingTerminationReason = 'connection_timeout';
this.incrementTerminationStat('outgoing', 'connection_timeout');
}
initiateCleanupOnce('connection_timeout');
}
}, 10000); // Increased from 5s to 10s to accommodate chained proxies
console.log(`Attempting to connect to ${targetHost}:${connectionOptions.port} for client ${remoteIP}...`);
// Create the target socket
const targetSocket = plugins.net.connect(connectionOptions);
connectionRecord.outgoing = targetSocket;
connectionRecord.outgoingStartTime = Date.now();
// Handle successful connection
targetSocket.once('connect', () => {
connectionSucceeded = true;
if (connectionTimeout) {
clearTimeout(connectionTimeout);
connectionTimeout = null;
}
connectionRecord.outgoingStartTime = Date.now();
console.log(
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
);
console.log(
`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}` +
`${serverName ? ` (SNI: ${serverName})` : ''}`
);
// Setup data flow after confirmed connection
setupDataFlow(targetSocket, initialChunk);
});
// Handle connection errors early
targetSocket.once('error', (err) => {
if (!connectionSucceeded) {
// This is an initial connection error
console.log(`Failed to connect to ${targetHost}:${connectionOptions.port} for ${remoteIP}: ${err.message}`);
if (connectionTimeout) {
clearTimeout(connectionTimeout);
connectionTimeout = null;
}
if (outgoingTerminationReason === null) {
outgoingTerminationReason = 'connection_failed';
this.incrementTerminationStat('outgoing', 'connection_failed');
}
initiateCleanupOnce('connection_failed');
}
// Other errors will be handled by the main error handler
});
};
/**
* Sets up the data flow between sockets after successful connection
*/
const setupDataFlow = (targetSocket: plugins.net.Socket, initialChunk?: Buffer) => {
if (initialChunk) {
socket.unshift(initialChunk);
}
// Set appropriate timeouts for both sockets
socket.setTimeout(120000);
targetSocket.setTimeout(120000);
// Set up the pipe in both directions
socket.pipe(targetSocket);
targetSocket.pipe(socket);
// Attach error and close handlers
socket.on('error', handleError('incoming'));
targetSocket.on('error', handleError('outgoing'));
socket.on('close', handleClose('incoming'));
targetSocket.on('close', handleClose('outgoing'));
// Handle timeout events
socket.on('timeout', () => {
console.log(`Timeout on incoming side from ${remoteIP}`);
if (incomingTerminationReason === null) {
incomingTerminationReason = 'timeout';
this.incrementTerminationStat('incoming', 'timeout');
}
cleanupOnce();
initiateCleanupOnce('timeout');
});
targetSocket.on('timeout', () => {
console.log(`Timeout on outgoing side from ${remoteIP}`);
if (outgoingTerminationReason === null) {
outgoingTerminationReason = 'timeout';
this.incrementTerminationStat('outgoing', 'timeout');
}
cleanupOnce();
initiateCleanupOnce('timeout');
});
socket.on('end', handleClose('incoming'));
targetSocket.on('end', handleClose('outgoing'));
};
if (this.settings.sniEnabled) {
socket.setTimeout(5000, () => {
console.log(`Initial data timeout for ${remoteIP}`);
socket.end();
cleanupOnce();
// Track activity for both sockets to reset inactivity timers
socket.on('data', (data) => {
this.updateActivity(connectionRecord);
});
targetSocket.on('data', (data) => {
this.updateActivity(connectionRecord);
});
// Initialize a cleanup timer for max connection lifetime
if (this.settings.maxConnectionLifetime) {
connectionRecord.cleanupTimer = setTimeout(() => {
console.log(`Connection from ${remoteIP} exceeded max lifetime (${this.settings.maxConnectionLifetime}ms), forcing cleanup.`);
initiateCleanupOnce('max_lifetime');
}, this.settings.maxConnectionLifetime);
}
};
// --- PORT RANGE-BASED HANDLING ---
// Only apply port-based rules if the incoming port is within one of the global port ranges.
if (this.settings.globalPortRanges && isPortInRanges(localPort, this.settings.globalPortRanges)) {
if (this.settings.forwardAllGlobalRanges) {
if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
console.log(`Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
socket.end();
initiateCleanupOnce('rejected');
return;
}
console.log(`Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
setupConnection('', undefined, {
domains: ['global'],
allowedIPs: this.settings.defaultAllowedIPs || [],
blockedIPs: this.settings.defaultBlockedIPs || [],
targetIPs: [this.settings.targetIP!],
portRanges: []
}, localPort);
return;
} else {
// Attempt to find a matching forced domain config based on the local port.
const forcedDomain = this.settings.domainConfigs.find(
domain => domain.portRanges && domain.portRanges.length > 0 && isPortInRanges(localPort, domain.portRanges)
);
if (forcedDomain) {
const effectiveAllowedIPs: string[] = [
...forcedDomain.allowedIPs,
...(this.settings.defaultAllowedIPs || [])
];
const effectiveBlockedIPs: string[] = [
...(forcedDomain.blockedIPs || []),
...(this.settings.defaultBlockedIPs || [])
];
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
console.log(`Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`);
socket.end();
initiateCleanupOnce('rejected');
return;
}
console.log(`Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`);
setupConnection('', undefined, forcedDomain, localPort);
return;
}
// Fall through to SNI/default handling if no forced domain config is found.
}
}
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
if (this.settings.sniEnabled) {
initialDataReceived = false;
socket.once('data', (chunk: Buffer) => {
socket.setTimeout(0);
if (initialTimeout) {
clearTimeout(initialTimeout);
initialTimeout = null;
}
initialDataReceived = true;
const serverName = extractSNI(chunk) || '';
// Lock the connection to the negotiated SNI.
connectionRecord.lockedDomain = serverName;
console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
// Delay adding the renegotiation listener until the next tick,
// so the initial ClientHello is not reprocessed.
setImmediate(() => {
socket.on('data', (renegChunk: Buffer) => {
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
try {
// Try to extract SNI from potential renegotiation
const newSNI = extractSNI(renegChunk);
if (newSNI && newSNI !== connectionRecord.lockedDomain) {
console.log(`Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`);
initiateCleanupOnce('sni_mismatch');
} else if (newSNI) {
console.log(`Rehandshake detected with same SNI: ${newSNI}. Allowing.`);
}
} catch (err) {
console.log(`Error processing potential renegotiation: ${err}. Allowing connection to continue.`);
}
}
});
});
setupConnection(serverName, chunk);
});
} else {
initialDataReceived = true;
if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
if (!this.settings.defaultAllowedIPs || this.settings.defaultAllowedIPs.length === 0 || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
}
setupConnection('');
}
})
.on('error', (err: Error) => {
console.log(`Server Error: ${err.message}`);
})
.listen(this.settings.fromPort, () => {
console.log(
`PortProxy -> OK: Now listening on port ${this.settings.fromPort}` +
`${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`
);
});
};
// Every 10 seconds log active connection count and longest running durations.
// --- SETUP LISTENERS ---
// Determine which ports to listen on.
const listeningPorts = new Set<number>();
if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
// Listen on every port defined by the global ranges.
for (const range of this.settings.globalPortRanges) {
for (let port = range.from; port <= range.to; port++) {
listeningPorts.add(port);
}
}
// Also ensure the default fromPort is listened to if it isn't already in the ranges.
listeningPorts.add(this.settings.fromPort);
} else {
listeningPorts.add(this.settings.fromPort);
}
// Create a server for each port.
for (const port of listeningPorts) {
const server = plugins.net
.createServer(connectionHandler)
.on('error', (err: Error) => {
console.log(`Server Error on port ${port}: ${err.message}`);
});
server.listen(port, () => {
console.log(`PortProxy -> OK: Now listening on port ${port}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
});
this.netServers.push(server);
}
// Log active connection count, run parity checks, and check for connection issues every 10 seconds.
this.connectionLogger = setInterval(() => {
if (this.isShuttingDown) return;
const now = Date.now();
let maxIncoming = 0;
let maxOutgoing = 0;
for (const record of this.connectionRecords) {
// Create a copy of the keys to avoid modification during iteration
const connectionIds = [...this.connectionRecords.keys()];
for (const id of connectionIds) {
const record = this.connectionRecords.get(id);
if (!record) continue;
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
if (record.outgoingStartTime) {
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
}
// Parity check: if outgoing socket closed and incoming remains active for >30 seconds, trigger cleanup
if (record.outgoingClosedTime &&
!record.incoming.destroyed &&
!record.connectionClosed &&
!record.cleanupInitiated &&
(now - record.outgoingClosedTime > 30000)) {
const remoteIP = record.incoming.remoteAddress || 'unknown';
console.log(`Parity check triggered: Incoming socket for ${remoteIP} has been active >30s after outgoing closed.`);
this.initiateCleanup(record, 'parity_check');
}
// Inactivity check: if no activity for a long time but sockets still open
const inactivityTime = now - record.lastActivity;
if (inactivityTime > 180000 && // 3 minutes
!record.connectionClosed &&
!record.cleanupInitiated) {
const remoteIP = record.incoming.remoteAddress || 'unknown';
console.log(`Inactivity check triggered: No activity on connection from ${remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
this.initiateCleanup(record, 'inactivity');
}
}
console.log(
`(Interval Log) Active connections: ${this.connectionRecords.size}. ` +
`Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. ` +
@ -339,14 +792,69 @@ export class PortProxy {
}
public async stop() {
const done = plugins.smartpromise.defer();
this.netServer.close(() => {
done.resolve();
});
console.log("PortProxy shutting down...");
this.isShuttingDown = true;
// Stop accepting new connections
const closeServerPromises: Promise<void>[] = this.netServers.map(
server =>
new Promise<void>((resolve) => {
server.close(() => resolve());
})
);
// Stop the connection logger
if (this.connectionLogger) {
clearInterval(this.connectionLogger);
this.connectionLogger = null;
}
await done.promise;
// Wait for servers to close
await Promise.all(closeServerPromises);
console.log("All servers closed. Cleaning up active connections...");
// Gracefully close active connections
const connectionIds = [...this.connectionRecords.keys()];
console.log(`Cleaning up ${connectionIds.length} active connections...`);
for (const id of connectionIds) {
const record = this.connectionRecords.get(id);
if (record && !record.connectionClosed && !record.cleanupInitiated) {
this.initiateCleanup(record, 'shutdown');
}
}
// Wait for graceful shutdown or timeout
const shutdownTimeout = this.settings.gracefulShutdownTimeout || 30000;
await new Promise<void>((resolve) => {
const checkInterval = setInterval(() => {
if (this.connectionRecords.size === 0) {
clearInterval(checkInterval);
resolve();
}
}, 1000);
// Force resolve after timeout
setTimeout(() => {
clearInterval(checkInterval);
if (this.connectionRecords.size > 0) {
console.log(`Forcing shutdown with ${this.connectionRecords.size} connections still active`);
// Force destroy any remaining connections
for (const record of this.connectionRecords.values()) {
if (!record.incoming.destroyed) {
record.incoming.destroy();
}
if (record.outgoing && !record.outgoing.destroyed) {
record.outgoing.destroy();
}
}
this.connectionRecords.clear();
}
resolve();
}, shutdownTimeout);
});
console.log("PortProxy shutdown complete.");
}
}