Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
7cea5773ee | |||
a2cb56ba65 | |||
408b793149 | |||
f6c3d2d3d0 | |||
422eb5ec40 | |||
45390c4389 | |||
0f2e6d688c | |||
3bd7b70c19 | |||
07a82a09be | |||
23253a2731 | |||
be31a9b553 | |||
a1051f78e8 | |||
aa756bd698 | |||
ff4f44d6fc |
44
changelog.md
44
changelog.md
@ -1,5 +1,49 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
- Extended IPTablesProxy class to include tagging rules with unique comments.
|
||||
- Added feature to clean up iptables rules via comments during process exit.
|
||||
|
||||
## 2025-02-24 - 3.12.0 - feat(IPTablesProxy)
|
||||
Introduce IPTablesProxy class for managing iptables NAT rules
|
||||
|
||||
- Added IPTablesProxy class to facilitate basic port forwarding using iptables.
|
||||
- Introduced IIpTableProxySettings interface for configuring IPTablesProxy.
|
||||
- Implemented start and stop methods for managing iptables rules dynamically.
|
||||
|
||||
## 2025-02-24 - 3.11.0 - feat(Port80Handler)
|
||||
Add automatic certificate issuance with ACME client
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "push.rocks",
|
||||
"gitrepo": "smartproxy",
|
||||
"description": "a proxy for handling high workloads of proxying",
|
||||
"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.",
|
||||
"npmPackagename": "@push.rocks/smartproxy",
|
||||
"license": "MIT",
|
||||
"projectDomain": "push.rocks",
|
||||
@ -20,7 +20,11 @@
|
||||
"ssl redirect",
|
||||
"port mapping",
|
||||
"reverse proxy",
|
||||
"authentication"
|
||||
"authentication",
|
||||
"dynamic routing",
|
||||
"sni",
|
||||
"port forwarding",
|
||||
"real-time applications"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
10
package.json
10
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "3.11.0",
|
||||
"version": "3.16.0",
|
||||
"private": false,
|
||||
"description": "a proxy for handling high workloads of proxying",
|
||||
"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.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
@ -62,7 +62,11 @@
|
||||
"ssl redirect",
|
||||
"port mapping",
|
||||
"reverse proxy",
|
||||
"authentication"
|
||||
"authentication",
|
||||
"dynamic routing",
|
||||
"sni",
|
||||
"port forwarding",
|
||||
"real-time applications"
|
||||
],
|
||||
"homepage": "https://code.foss.global/push.rocks/smartproxy#readme",
|
||||
"repository": {
|
||||
|
189
readme.md
189
readme.md
@ -14,11 +14,11 @@ This will add `@push.rocks/smartproxy` to your project's dependencies.
|
||||
|
||||
## Usage
|
||||
|
||||
`@push.rocks/smartproxy` is a versatile package for setting up and handling proxies with various capabilities such as SSL redirection, port proxying, and creating network proxies with complex routing rules. Below is a comprehensive guide on using its features.
|
||||
`@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.
|
||||
|
||||
### Setting Up a Network Proxy
|
||||
### Initial Setup
|
||||
|
||||
Create a network proxy to route incoming HTTPS requests to different local servers based on the hostname.
|
||||
Before diving into specific features, let's start by configuring and setting up our basic proxy server:
|
||||
|
||||
```typescript
|
||||
import { NetworkProxy } from '@push.rocks/smartproxy';
|
||||
@ -26,7 +26,7 @@ import { NetworkProxy } from '@push.rocks/smartproxy';
|
||||
// Instantiate the NetworkProxy with desired options
|
||||
const myNetworkProxy = new NetworkProxy({ port: 443 });
|
||||
|
||||
// Define your reverse proxy configurations
|
||||
// Define reverse proxy configurations
|
||||
const proxyConfigs = [
|
||||
{
|
||||
destinationIp: '127.0.0.1',
|
||||
@ -39,70 +39,187 @@ PRIVATE_KEY_CONTENT
|
||||
CERTIFICATE_CONTENT
|
||||
-----END CERTIFICATE-----`,
|
||||
},
|
||||
// Add more reverse proxy configurations here
|
||||
// More configurations can be added here
|
||||
];
|
||||
|
||||
// Start the network proxy
|
||||
await myNetworkProxy.start();
|
||||
|
||||
// Update proxy configurations dynamically
|
||||
// Apply proxy configurations
|
||||
await myNetworkProxy.updateProxyConfigs(proxyConfigs);
|
||||
|
||||
// Optionally, add default headers to all responses
|
||||
// Optionally add default headers to all responses
|
||||
await myNetworkProxy.addDefaultHeaders({
|
||||
'X-Powered-By': 'smartproxy',
|
||||
});
|
||||
```
|
||||
|
||||
### Port Proxying
|
||||
### Configuring SSL Redirection
|
||||
|
||||
You can also set up a port proxy to forward traffic from one port to another, which is useful for dynamic port forwarding scenarios.
|
||||
|
||||
```typescript
|
||||
import { PortProxy } from '@push.rocks/smartproxy';
|
||||
|
||||
// Create a PortProxy to forward traffic from port 5000 to port 3000
|
||||
const myPortProxy = new PortProxy(5000, 3000);
|
||||
|
||||
// Start the port proxy
|
||||
await myPortProxy.start();
|
||||
|
||||
// To stop the port proxy, simply call
|
||||
await myPortProxy.stop();
|
||||
```
|
||||
|
||||
### Enabling SSL Redirection
|
||||
|
||||
Easily redirect HTTP traffic to HTTPS using the `SslRedirect` class. This is particularly useful when ensuring all traffic uses encryption.
|
||||
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:
|
||||
|
||||
```typescript
|
||||
import { SslRedirect } from '@push.rocks/smartproxy';
|
||||
|
||||
// Instantiate the SslRedirect on port 80 (HTTP)
|
||||
// Instantiate the SslRedirect for listening on port 80
|
||||
const mySslRedirect = new SslRedirect(80);
|
||||
|
||||
// Start listening and redirecting to HTTPS
|
||||
// Start listening and redirect HTTP traffic to HTTPS
|
||||
await mySslRedirect.start();
|
||||
|
||||
// To stop the redirection, use
|
||||
// To stop redirection, you can use the following command:
|
||||
await mySslRedirect.stop();
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
### Handling Complex Networking with Port Proxy
|
||||
|
||||
The package integrates seamlessly with TypeScript, allowing for advanced use cases, such as implementing custom routing logic, authentication mechanisms, and handling WebSocket connections through the network proxy.
|
||||
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:
|
||||
|
||||
For a more advanced setup involving WebSocket proxying and dynamic configuration reloading, refer to the network proxy example provided above. The WebSocket support demonstrates how seamless it is to work with real-time applications.
|
||||
```typescript
|
||||
import { PortProxy } from '@push.rocks/smartproxy';
|
||||
|
||||
Remember, when dealing with certificates and private keys for HTTPS configurations, always secure your keys and store them appropriately.
|
||||
// Create a PortProxy to directly forward traffic from port 5000 to 3000
|
||||
const myPortProxy = new PortProxy(5000, 3000);
|
||||
|
||||
`@push.rocks/smartproxy` provides a solid foundation for handling high workloads and complex proxying requirements with ease, whether you're implementing SSL redirections, port forwarding, or extensive routing and WebSocket support in your network.
|
||||
// Initiate the port proxy
|
||||
await myPortProxy.start();
|
||||
|
||||
For more information on how to use the features, refer to the in-depth documentation available in the package's repository or the npm package description.
|
||||
// To stop the port proxy mechanism:
|
||||
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:
|
||||
|
||||
```typescript
|
||||
import { PortProxy } from '@push.rocks/smartproxy';
|
||||
|
||||
const myComplexPortProxy = new PortProxy({
|
||||
fromPort: 6000,
|
||||
toPort: 3000,
|
||||
domains: [
|
||||
{
|
||||
domain: 'api.example.com',
|
||||
allowedIPs: ['192.168.0.*', '127.0.0.1'],
|
||||
targetIP: '192.168.1.100'
|
||||
}
|
||||
// Define more domain-specific rules if needed
|
||||
],
|
||||
sniEnabled: true, // if SNI (Server Name Indication) is desired
|
||||
defaultAllowedIPs: ['*']);
|
||||
});
|
||||
|
||||
// Start listening for complex routing requests
|
||||
await myComplexPortProxy.start();
|
||||
```
|
||||
|
||||
### WebSocket Support and Load 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:
|
||||
|
||||
```typescript
|
||||
import { NetworkProxy } from '@push.rocks/smartproxy';
|
||||
|
||||
const wsProxy = new NetworkProxy({ port: 443 });
|
||||
|
||||
// Assume reverse proxy configurations with WebSocket intentions
|
||||
const wsProxyConfigs = [
|
||||
{
|
||||
destinationIp: '127.0.0.1',
|
||||
destinationPort: '8080',
|
||||
hostName: 'socket.example.com',
|
||||
// Add further options such as keys for SSL if needed
|
||||
}
|
||||
];
|
||||
|
||||
// Start the network proxy with WebSocket capabilities
|
||||
await wsProxy.start();
|
||||
await wsProxy.updateProxyConfigs(wsProxyConfigs);
|
||||
|
||||
// Ensure WebSocket connections remain alive
|
||||
wsProxy.heartbeatInterval = setInterval(() => {
|
||||
// logic for keeping connections alive and healthy
|
||||
}, 60000); // Every 60 seconds
|
||||
|
||||
// Gracefully handle server or connection errors to maintain uptime
|
||||
wsProxy.httpsServer.on('error', (error) => console.log('Server Error:', error));
|
||||
```
|
||||
|
||||
### Comprehensive Routing and Advanced 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:
|
||||
|
||||
```typescript
|
||||
import { NetworkProxy } from '@push.rocks/smartproxy';
|
||||
|
||||
const dynamicRoutingProxy = new NetworkProxy({ port: 8443 });
|
||||
dynamicRoutingProxy.router.setNewProxyConfigs([
|
||||
{
|
||||
destinationIp: '192.168.1.150',
|
||||
destinationPort: '80',
|
||||
hostName: 'dynamic.example.com',
|
||||
authentication: {
|
||||
type: 'Basic',
|
||||
user: 'admin',
|
||||
pass: 'password123'
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
await dynamicRoutingProxy.start();
|
||||
```
|
||||
|
||||
For those dealing with high volume or regulatory needs, the integration of tools like `iptables` allows broad control over network traffic:
|
||||
|
||||
```typescript
|
||||
import { IPTablesProxy } from '@push.rocks/smartproxy';
|
||||
|
||||
// Setting up iptables for advanced network management
|
||||
const ipTablesProxy = new IPTablesProxy({
|
||||
fromPort: 8081,
|
||||
toPort: 8080,
|
||||
deleteOnExit: true // clean rules upon server shutdown
|
||||
});
|
||||
|
||||
// Begin routing with IPTables
|
||||
await ipTablesProxy.start();
|
||||
```
|
||||
|
||||
### Combining with HTTP and HTTPS Credentials
|
||||
|
||||
When undertaking proxy configurations, handling sensitive data like SSL certificates and keys securely is imperative:
|
||||
|
||||
```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
|
||||
} catch (error) {
|
||||
console.error('Cannot load 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:
|
||||
|
||||
```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.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.
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '3.11.0',
|
||||
description: 'a proxy for handling high workloads of proxying'
|
||||
version: '3.16.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.'
|
||||
}
|
||||
|
183
ts/classes.iptablesproxy.ts
Normal file
183
ts/classes.iptablesproxy.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import { exec, execSync } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Settings for IPTablesProxy.
|
||||
*/
|
||||
export interface IIpTableProxySettings {
|
||||
fromPort: number;
|
||||
toPort: number;
|
||||
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.
|
||||
}
|
||||
|
||||
/**
|
||||
* IPTablesProxy sets up iptables NAT rules to forward TCP traffic.
|
||||
* It only supports basic port forwarding and uses iptables comments to tag rules.
|
||||
*/
|
||||
export class IPTablesProxy {
|
||||
public settings: IIpTableProxySettings;
|
||||
private rulesInstalled: boolean = false;
|
||||
private ruleTag: string;
|
||||
|
||||
constructor(settings: IIpTableProxySettings) {
|
||||
this.settings = {
|
||||
...settings,
|
||||
toHost: settings.toHost || 'localhost',
|
||||
};
|
||||
// Generate a unique identifier for the rules added by this instance.
|
||||
this.ruleTag = `IPTablesProxy:${Date.now()}:${Math.random().toString(36).substr(2, 5)}`;
|
||||
|
||||
// If deleteOnExit is true, register cleanup handlers.
|
||||
if (this.settings.deleteOnExit) {
|
||||
const cleanup = () => {
|
||||
try {
|
||||
IPTablesProxy.cleanSlateSync();
|
||||
} catch (err) {
|
||||
console.error('Error cleaning iptables rules on exit:', err);
|
||||
}
|
||||
};
|
||||
process.on('exit', cleanup);
|
||||
process.on('SIGINT', () => {
|
||||
cleanup();
|
||||
process.exit();
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
cleanup();
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up iptables rules for port forwarding.
|
||||
* The rules are tagged with a unique comment so that they can be identified later.
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the iptables rules that were added in start(), by matching the unique comment.
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.rulesInstalled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously cleans up any iptables rules in the nat table that were added by this module.
|
||||
* It looks for rules with comments containing "IPTablesProxy:".
|
||||
*/
|
||||
public static async cleanSlate(): Promise<void> {
|
||||
try {
|
||||
const { stdout } = await execAsync('iptables-save -t nat');
|
||||
const lines = stdout.split('\n');
|
||||
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to run iptables-save: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously cleans up any iptables rules in the nat table that were added by this module.
|
||||
* It looks for rules with comments containing "IPTablesProxy:".
|
||||
* This method is intended for use in process exit handlers.
|
||||
*/
|
||||
public static cleanSlateSync(): void {
|
||||
try {
|
||||
const stdout = execSync('iptables-save -t nat').toString();
|
||||
const lines = stdout.split('\n');
|
||||
const proxyLines = lines.filter(line => line.includes('IPTablesProxy:'));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to run iptables-save: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,25 @@
|
||||
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
|
||||
portRanges?: Array<{ from: number; to: number }>; // Optional domain-specific allowed port ranges
|
||||
}
|
||||
|
||||
export interface IProxySettings extends plugins.tls.TlsOptions {
|
||||
/** 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'
|
||||
targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
|
||||
domains: IDomainConfig[];
|
||||
sniEnabled?: boolean;
|
||||
defaultAllowedIPs?: 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
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,11 +91,12 @@ interface IConnectionRecord {
|
||||
incomingStartTime: number;
|
||||
outgoingStartTime?: number;
|
||||
connectionClosed: boolean;
|
||||
cleanupTimer?: NodeJS.Timeout; // Timer to force cleanup after max lifetime/inactivity
|
||||
}
|
||||
|
||||
export class PortProxy {
|
||||
netServer: plugins.net.Server;
|
||||
settings: IProxySettings;
|
||||
settings: IPortProxySettings;
|
||||
// Unified record tracking each connection pair.
|
||||
private connectionRecords: Set<IConnectionRecord> = new Set();
|
||||
private connectionLogger: NodeJS.Timeout | null = null;
|
||||
@ -102,10 +109,11 @@ export class PortProxy {
|
||||
outgoing: {},
|
||||
};
|
||||
|
||||
constructor(settings: IProxySettings) {
|
||||
constructor(settingsArg: IPortProxySettings) {
|
||||
this.settings = {
|
||||
...settings,
|
||||
toHost: settings.toHost || 'localhost',
|
||||
...settingsArg,
|
||||
targetIP: settingsArg.targetIP || 'localhost',
|
||||
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
|
||||
};
|
||||
}
|
||||
|
||||
@ -141,12 +149,18 @@ export class PortProxy {
|
||||
);
|
||||
};
|
||||
|
||||
// Find a matching domain config based on the SNI.
|
||||
// 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);
|
||||
};
|
||||
|
||||
// Find a matching domain config based on SNI (fallback when port ranges aren’t used)
|
||||
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 connectionRecord: IConnectionRecord = {
|
||||
incoming: socket,
|
||||
outgoing: null,
|
||||
@ -154,7 +168,7 @@ export class PortProxy {
|
||||
connectionClosed: false,
|
||||
};
|
||||
this.connectionRecords.add(connectionRecord);
|
||||
console.log(`New connection from ${remoteIP}. Active connections: ${this.connectionRecords.size}`);
|
||||
console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
|
||||
|
||||
let initialDataReceived = false;
|
||||
let incomingTerminationReason: string | null = null;
|
||||
@ -164,6 +178,9 @@ export class PortProxy {
|
||||
const cleanupOnce = () => {
|
||||
if (!connectionRecord.connectionClosed) {
|
||||
connectionRecord.connectionClosed = true;
|
||||
if (connectionRecord.cleanupTimer) {
|
||||
clearTimeout(connectionRecord.cleanupTimer);
|
||||
}
|
||||
cleanUpSockets(connectionRecord.incoming, connectionRecord.outgoing || undefined);
|
||||
this.connectionRecords.delete(connectionRecord);
|
||||
console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`);
|
||||
@ -219,25 +236,28 @@ export class PortProxy {
|
||||
cleanupOnce();
|
||||
};
|
||||
|
||||
const setupConnection = (serverName: string, initialChunk?: Buffer) => {
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
const setupConnection = (serverName: string, initialChunk?: Buffer, forcedDomain?: IDomainConfig) => {
|
||||
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
|
||||
const domainConfig = forcedDomain ? forcedDomain : (serverName ? findMatchingDomain(serverName) : undefined);
|
||||
const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
|
||||
|
||||
if (!defaultAllowed && serverName) {
|
||||
const domainConfig = findMatchingDomain(serverName);
|
||||
if (!defaultAllowed && serverName && !forcedDomain) {
|
||||
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`);
|
||||
}
|
||||
|
||||
const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
|
||||
const targetHost = domainConfig?.targetIP || this.settings.toHost!;
|
||||
const targetHost = domainConfig?.targetIP || this.settings.targetIP!;
|
||||
const connectionOptions: plugins.net.NetConnectOpts = {
|
||||
host: targetHost,
|
||||
port: this.settings.toPort,
|
||||
@ -252,7 +272,7 @@ export class PortProxy {
|
||||
|
||||
console.log(
|
||||
`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}` +
|
||||
`${serverName ? ` (SNI: ${serverName})` : ''}`
|
||||
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domain})` : ''}`
|
||||
);
|
||||
|
||||
if (initialChunk) {
|
||||
@ -262,6 +282,7 @@ export class PortProxy {
|
||||
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'));
|
||||
@ -284,8 +305,92 @@ export class PortProxy {
|
||||
});
|
||||
socket.on('end', handleClose('incoming'));
|
||||
targetSocket.on('end', handleClose('outgoing'));
|
||||
|
||||
// Initialize a cleanup timer for max connection lifetime.
|
||||
if (this.settings.maxConnectionLifetime) {
|
||||
let incomingActive = false;
|
||||
let outgoingActive = false;
|
||||
const resetCleanupTimer = () => {
|
||||
if (this.settings.maxConnectionLifetime) {
|
||||
if (connectionRecord.cleanupTimer) {
|
||||
clearTimeout(connectionRecord.cleanupTimer);
|
||||
}
|
||||
connectionRecord.cleanupTimer = setTimeout(() => {
|
||||
console.log(`Connection from ${remoteIP} exceeded max lifetime with inactivity (${this.settings.maxConnectionLifetime}ms), forcing cleanup.`);
|
||||
cleanupOnce();
|
||||
}, this.settings.maxConnectionLifetime);
|
||||
}
|
||||
};
|
||||
|
||||
resetCleanupTimer();
|
||||
|
||||
socket.on('data', () => {
|
||||
incomingActive = true;
|
||||
if (incomingActive && outgoingActive) {
|
||||
resetCleanupTimer();
|
||||
incomingActive = false;
|
||||
outgoingActive = false;
|
||||
}
|
||||
});
|
||||
targetSocket.on('data', () => {
|
||||
outgoingActive = true;
|
||||
if (incomingActive && outgoingActive) {
|
||||
resetCleanupTimer();
|
||||
incomingActive = false;
|
||||
outgoingActive = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// --- PORT RANGE-BASED HANDLING ---
|
||||
// If global port ranges are defined, enforce port-based routing and ignore SNI.
|
||||
if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
|
||||
if (!isPortInRanges(localPort, this.settings.globalPortRanges)) {
|
||||
console.log(`Connection from ${remoteIP} rejected: port ${localPort} is not in global allowed ranges.`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
if (this.settings.forwardAllGlobalRanges) {
|
||||
// Forward connection to the global targetIP regardless of domain config.
|
||||
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();
|
||||
return;
|
||||
}
|
||||
console.log(`Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
|
||||
setupConnection('', undefined, {
|
||||
domain: 'global',
|
||||
allowedIPs: this.settings.defaultAllowedIPs || [],
|
||||
targetIP: this.settings.targetIP,
|
||||
portRanges: []
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
// Find a matching domain config based on the incoming local port.
|
||||
const forcedDomain = this.settings.domains.find(
|
||||
domain => domain.portRanges && domain.portRanges.length > 0 && isPortInRanges(localPort, domain.portRanges)
|
||||
);
|
||||
if (!forcedDomain) {
|
||||
console.log(`Connection from ${remoteIP} rejected: port ${localPort} not configured in any domain's portRanges.`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
// Check allowed IPs for the forced domain.
|
||||
const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
|
||||
if (!defaultAllowed && !isAllowed(remoteIP, forcedDomain.allowedIPs)) {
|
||||
console.log(`Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domain} on port ${localPort}.`);
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
console.log(`Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domain}.`);
|
||||
// Proceed immediately using the forced domain; ignore SNI.
|
||||
setupConnection('', undefined, forcedDomain);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// --- FALLBACK: SNI-BASED HANDLING (if no global port ranges are defined) ---
|
||||
if (this.settings.sniEnabled) {
|
||||
socket.setTimeout(5000, () => {
|
||||
console.log(`Initial data timeout for ${remoteIP}`);
|
||||
@ -318,7 +423,7 @@ export class PortProxy {
|
||||
);
|
||||
});
|
||||
|
||||
// Every 10 seconds log active connection count and longest running durations.
|
||||
// Log active connection count and longest running durations every 10 seconds.
|
||||
this.connectionLogger = setInterval(() => {
|
||||
const now = Date.now();
|
||||
let maxIncoming = 0;
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './classes.iptablesproxy.js';
|
||||
export * from './classes.networkproxy.js';
|
||||
export * from './classes.portproxy.js';
|
||||
export * from './classes.port80handler.js';
|
||||
|
Reference in New Issue
Block a user