From ee03224561411c3bc280b177badfdb4252e56fe0 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Fri, 21 Feb 2025 19:44:59 +0000 Subject: [PATCH] feat(PortProxy): Add optional source IP preservation support in PortProxy --- changelog.md | 6 ++++ test/test.portproxy.ts | 59 +++++++++++++++++++++++++++++--------- ts/00_commitinfo_data.ts | 2 +- ts/smartproxy.portproxy.ts | 15 +++++++--- 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/changelog.md b/changelog.md index bf5015e..ab95197 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,11 @@ # Changelog +## 2025-02-21 - 3.7.0 - feat(PortProxy) +Add optional source IP preservation support in PortProxy + +- Added a feature to optionally preserve the client's source IP when proxying connections. +- Enhanced test cases to include scenarios for source IP preservation. + ## 2025-02-21 - 3.6.0 - feat(PortProxy) Add feature to preserve original client IP through chained proxies diff --git a/test/test.portproxy.ts b/test/test.portproxy.ts index 43c2389..5ce04cf 100644 --- a/test/test.portproxy.ts +++ b/test/test.portproxy.ts @@ -175,35 +175,66 @@ tap.test('should stop port proxy', async () => { }); // Cleanup -tap.test('should preserve client IP through chained proxies', async () => { - // Create two proxies in chain - const firstProxy = new PortProxy({ +tap.test('should support optional source IP preservation in chained proxies', async () => { + // Test 1: Without IP preservation (default behavior) + const firstProxyDefault = new PortProxy({ fromPort: PROXY_PORT + 4, - toPort: PROXY_PORT + 5, // Point to second proxy + toPort: PROXY_PORT + 5, toHost: 'localhost', domains: [], sniEnabled: false, - defaultAllowedIPs: ['127.0.0.1'] + defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'] }); - const secondProxy = new PortProxy({ + const secondProxyDefault = new PortProxy({ fromPort: PROXY_PORT + 5, toPort: TEST_SERVER_PORT, toHost: 'localhost', domains: [], sniEnabled: false, - defaultAllowedIPs: ['127.0.0.1'] + defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'] }); - await secondProxy.start(); - await firstProxy.start(); + await secondProxyDefault.start(); + await firstProxyDefault.start(); - // Connect through the chain - const response = await createTestClient(PROXY_PORT + 4, TEST_DATA); - expect(response).toEqual(`Echo: ${TEST_DATA}`); + // 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 firstProxy.stop(); - await secondProxy.stop(); + await firstProxyDefault.stop(); + await secondProxyDefault.stop(); + + // Test 2: With IP preservation + const firstProxyPreserved = new PortProxy({ + fromPort: PROXY_PORT + 6, + toPort: PROXY_PORT + 7, + toHost: 'localhost', + domains: [], + sniEnabled: false, + defaultAllowedIPs: ['127.0.0.1'], + preserveSourceIP: true + }); + + const secondProxyPreserved = new PortProxy({ + fromPort: PROXY_PORT + 7, + toPort: TEST_SERVER_PORT, + toHost: 'localhost', + domains: [], + sniEnabled: false, + defaultAllowedIPs: ['127.0.0.1'], + preserveSourceIP: true + }); + + 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(); }); tap.test('cleanup port proxy test environment', async () => { diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 8219335..45329ca 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '3.6.0', + version: '3.7.0', description: 'a proxy for handling high workloads of proxying' } diff --git a/ts/smartproxy.portproxy.ts b/ts/smartproxy.portproxy.ts index 30f1a35..94074a2 100644 --- a/ts/smartproxy.portproxy.ts +++ b/ts/smartproxy.portproxy.ts @@ -17,6 +17,7 @@ export interface ProxySettings extends plugins.tls.TlsOptions { domains: DomainConfig[]; sniEnabled?: boolean; defaultAllowedIPs?: string[]; // Optional default IP patterns if no matching domain found + preserveSourceIP?: boolean; // Whether to preserve the client's source IP when proxying } export class PortProxy { @@ -123,12 +124,18 @@ export class PortProxy { const domainConfig = serverName ? findMatchingDomain(serverName) : undefined; const targetHost = domainConfig?.targetIP || this.settings.toHost!; - // Create connection with IP binding to preserve original client IP - const to = plugins.net.createConnection({ + // Create connection, optionally preserving the client's source IP + const connectionOptions: plugins.net.NetConnectOpts = { host: targetHost, port: this.settings.toPort, - localAddress: remoteIP.replace('::ffff:', ''), // Remove IPv6 mapping if present - }); + }; + + // Only set localAddress if preserveSourceIP is enabled + if (this.settings.preserveSourceIP) { + connectionOptions.localAddress = remoteIP.replace('::ffff:', ''); // Remove IPv6 mapping if present + } + + const to = plugins.net.createConnection(connectionOptions); console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`); from.setTimeout(120000); from.pipe(to);