2025-02-04 01:24:37 +01:00
|
|
|
import { expect, tap } from '@push.rocks/tapbundle';
|
|
|
|
import * as net from 'net';
|
2025-02-24 09:53:39 +00:00
|
|
|
import { PortProxy } from '../ts/classes.portproxy.js';
|
2025-02-04 01:24:37 +01:00
|
|
|
|
|
|
|
let testServer: net.Server;
|
|
|
|
let portProxy: PortProxy;
|
|
|
|
const TEST_SERVER_PORT = 4000;
|
|
|
|
const PROXY_PORT = 4001;
|
|
|
|
const TEST_DATA = 'Hello through port proxy!';
|
|
|
|
|
2025-03-05 17:06:51 +00:00
|
|
|
// Track all created servers and proxies for proper cleanup
|
|
|
|
const allServers: net.Server[] = [];
|
|
|
|
const allProxies: PortProxy[] = [];
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// Helper: Creates a test TCP server that listens on a given port and host.
|
|
|
|
function createTestServer(port: number, host: string = 'localhost'): Promise<net.Server> {
|
2025-02-04 01:24:37 +01:00
|
|
|
return new Promise((resolve) => {
|
|
|
|
const server = net.createServer((socket) => {
|
|
|
|
socket.on('data', (data) => {
|
2025-02-27 21:19:34 +00:00
|
|
|
// Echo the received data back with a prefix.
|
2025-02-04 01:24:37 +01:00
|
|
|
socket.write(`Echo: ${data.toString()}`);
|
|
|
|
});
|
|
|
|
socket.on('error', (error) => {
|
2025-02-27 21:19:34 +00:00
|
|
|
console.error(`[Test Server] Socket error on ${host}:${port}:`, error);
|
2025-02-04 01:24:37 +01:00
|
|
|
});
|
|
|
|
});
|
2025-02-27 21:19:34 +00:00
|
|
|
server.listen(port, host, () => {
|
|
|
|
console.log(`[Test Server] Listening on ${host}:${port}`);
|
2025-03-05 17:06:51 +00:00
|
|
|
allServers.push(server); // Track this server
|
2025-02-04 01:24:37 +01:00
|
|
|
resolve(server);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// Helper: Creates a test client connection.
|
2025-02-04 01:24:37 +01:00
|
|
|
function createTestClient(port: number, data: string): Promise<string> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const client = new net.Socket();
|
|
|
|
let response = '';
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
client.destroy();
|
|
|
|
reject(new Error(`Client connection timeout to port ${port}`));
|
|
|
|
}, 5000);
|
|
|
|
|
2025-02-04 01:24:37 +01:00
|
|
|
client.connect(port, 'localhost', () => {
|
|
|
|
console.log('[Test Client] Connected to server');
|
|
|
|
client.write(data);
|
|
|
|
});
|
|
|
|
client.on('data', (chunk) => {
|
|
|
|
response += chunk.toString();
|
|
|
|
client.end();
|
|
|
|
});
|
2025-03-05 17:06:51 +00:00
|
|
|
client.on('end', () => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
resolve(response);
|
|
|
|
});
|
|
|
|
client.on('error', (error) => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
reject(error);
|
|
|
|
});
|
2025-02-04 01:24:37 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// SETUP: Create a test server and a PortProxy instance.
|
2025-02-04 01:24:37 +01:00
|
|
|
tap.test('setup port proxy test environment', async () => {
|
|
|
|
testServer = await createTestServer(TEST_SERVER_PORT);
|
2025-02-21 17:01:02 +00:00
|
|
|
portProxy = new PortProxy({
|
|
|
|
fromPort: PROXY_PORT,
|
|
|
|
toPort: TEST_SERVER_PORT,
|
2025-02-27 14:23:44 +00:00
|
|
|
targetIP: 'localhost',
|
2025-02-27 21:19:34 +00:00
|
|
|
domainConfigs: [],
|
2025-02-21 15:14:02 +00:00
|
|
|
sniEnabled: false,
|
2025-02-27 14:23:44 +00:00
|
|
|
defaultAllowedIPs: ['127.0.0.1'],
|
|
|
|
globalPortRanges: []
|
2025-02-21 15:14:02 +00:00
|
|
|
});
|
2025-03-05 17:06:51 +00:00
|
|
|
allProxies.push(portProxy); // Track this proxy
|
2025-02-04 01:24:37 +01:00
|
|
|
});
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// Test that the proxy starts and its servers are listening.
|
2025-02-04 01:24:37 +01:00
|
|
|
tap.test('should start port proxy', async () => {
|
|
|
|
await portProxy.start();
|
2025-02-27 14:23:44 +00:00
|
|
|
expect((portProxy as any).netServers.every((server: net.Server) => server.listening)).toBeTrue();
|
2025-02-04 01:24:37 +01:00
|
|
|
});
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// Test basic TCP forwarding.
|
2025-02-21 17:01:02 +00:00
|
|
|
tap.test('should forward TCP connections and data to localhost', async () => {
|
2025-02-04 01:24:37 +01:00
|
|
|
const response = await createTestClient(PROXY_PORT, TEST_DATA);
|
|
|
|
expect(response).toEqual(`Echo: ${TEST_DATA}`);
|
|
|
|
});
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// Test proxy with a custom target host.
|
2025-02-21 17:01:02 +00:00
|
|
|
tap.test('should forward TCP connections to custom host', async () => {
|
|
|
|
const customHostProxy = new PortProxy({
|
|
|
|
fromPort: PROXY_PORT + 1,
|
|
|
|
toPort: TEST_SERVER_PORT,
|
2025-02-27 14:23:44 +00:00
|
|
|
targetIP: '127.0.0.1',
|
2025-02-27 21:19:34 +00:00
|
|
|
domainConfigs: [],
|
2025-02-21 17:01:02 +00:00
|
|
|
sniEnabled: false,
|
2025-02-27 14:23:44 +00:00
|
|
|
defaultAllowedIPs: ['127.0.0.1'],
|
|
|
|
globalPortRanges: []
|
2025-02-21 17:01:02 +00:00
|
|
|
});
|
2025-03-05 17:06:51 +00:00
|
|
|
allProxies.push(customHostProxy); // Track this proxy
|
2025-02-21 17:01:02 +00:00
|
|
|
|
|
|
|
await customHostProxy.start();
|
|
|
|
const response = await createTestClient(PROXY_PORT + 1, TEST_DATA);
|
|
|
|
expect(response).toEqual(`Echo: ${TEST_DATA}`);
|
|
|
|
await customHostProxy.stop();
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
// Remove from tracking after stopping
|
|
|
|
const index = allProxies.indexOf(customHostProxy);
|
|
|
|
if (index !== -1) allProxies.splice(index, 1);
|
2025-02-21 17:01:02 +00:00
|
|
|
});
|
|
|
|
|
2025-03-05 17:06:51 +00:00
|
|
|
// Test custom IP forwarding
|
2025-03-07 15:46:34 +00:00
|
|
|
// Modified to work in Docker/CI environments without needing 127.0.0.2
|
2025-03-05 17:06:51 +00:00
|
|
|
tap.test('should forward connections to custom IP', async () => {
|
|
|
|
// Set up ports that are FAR apart to avoid any possible confusion
|
2025-03-07 15:46:34 +00:00
|
|
|
const forcedProxyPort = PROXY_PORT + 2; // 4003 - The port that our proxy listens on
|
|
|
|
const targetServerPort = TEST_SERVER_PORT + 200; // 4200 - Target test server on different port
|
2025-03-05 17:06:51 +00:00
|
|
|
|
2025-03-07 15:46:34 +00:00
|
|
|
// Create a test server listening on a unique port on 127.0.0.1 (works in all environments)
|
|
|
|
const testServer2 = await createTestServer(targetServerPort, '127.0.0.1');
|
2025-02-21 19:34:11 +00:00
|
|
|
|
2025-03-07 15:46:34 +00:00
|
|
|
// We're simulating routing to a different IP by using a different port
|
|
|
|
// This tests the core functionality without requiring multiple IPs
|
2025-02-21 19:34:11 +00:00
|
|
|
const domainProxy = new PortProxy({
|
2025-03-05 17:06:51 +00:00
|
|
|
fromPort: forcedProxyPort, // 4003 - Listen on this port
|
2025-03-07 15:46:34 +00:00
|
|
|
toPort: targetServerPort, // 4200 - Forward to this port
|
|
|
|
targetIP: '127.0.0.1', // Always use localhost (works in Docker)
|
2025-03-05 17:06:51 +00:00
|
|
|
domainConfigs: [], // No domain configs to confuse things
|
2025-02-27 14:23:44 +00:00
|
|
|
sniEnabled: false,
|
2025-03-05 17:06:51 +00:00
|
|
|
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], // Allow localhost
|
|
|
|
// We'll test the functionality WITHOUT port ranges this time
|
|
|
|
globalPortRanges: []
|
2025-02-21 19:34:11 +00:00
|
|
|
});
|
2025-03-05 17:06:51 +00:00
|
|
|
allProxies.push(domainProxy); // Track this proxy
|
2025-02-21 19:34:11 +00:00
|
|
|
|
|
|
|
await domainProxy.start();
|
|
|
|
|
2025-03-05 17:06:51 +00:00
|
|
|
// Send a single test connection
|
2025-02-27 21:19:34 +00:00
|
|
|
const response = await createTestClient(forcedProxyPort, TEST_DATA);
|
|
|
|
expect(response).toEqual(`Echo: ${TEST_DATA}`);
|
2025-02-21 19:34:11 +00:00
|
|
|
|
|
|
|
await domainProxy.stop();
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
// Remove from tracking after stopping
|
|
|
|
const proxyIndex = allProxies.indexOf(domainProxy);
|
|
|
|
if (proxyIndex !== -1) allProxies.splice(proxyIndex, 1);
|
|
|
|
|
|
|
|
// Close the test server
|
2025-02-21 19:34:11 +00:00
|
|
|
await new Promise<void>((resolve) => testServer2.close(() => resolve()));
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
// Remove from tracking
|
|
|
|
const serverIndex = allServers.indexOf(testServer2);
|
|
|
|
if (serverIndex !== -1) allServers.splice(serverIndex, 1);
|
2025-02-21 19:34:11 +00:00
|
|
|
});
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// Test handling of multiple concurrent connections.
|
2025-02-04 01:24:37 +01:00
|
|
|
tap.test('should handle multiple concurrent connections', async () => {
|
|
|
|
const concurrentRequests = 5;
|
2025-02-27 21:19:34 +00:00
|
|
|
const requests = Array(concurrentRequests).fill(null).map((_, i) =>
|
2025-02-04 01:24:37 +01:00
|
|
|
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}`);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// Test connection timeout handling.
|
2025-02-04 01:24:37 +01:00
|
|
|
tap.test('should handle connection timeouts', async () => {
|
|
|
|
const client = new net.Socket();
|
|
|
|
await new Promise<void>((resolve) => {
|
2025-03-05 17:06:51 +00:00
|
|
|
// Add a timeout to ensure we don't hang here
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
client.destroy();
|
|
|
|
resolve();
|
|
|
|
}, 3000);
|
|
|
|
|
2025-02-04 01:24:37 +01:00
|
|
|
client.connect(PROXY_PORT, 'localhost', () => {
|
2025-02-27 21:19:34 +00:00
|
|
|
// Do not send any data to trigger a timeout.
|
2025-03-05 17:06:51 +00:00
|
|
|
client.on('close', () => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
resolve();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
client.on('error', () => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
client.destroy();
|
|
|
|
resolve();
|
2025-02-04 01:24:37 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// Test stopping the port proxy.
|
2025-02-04 01:24:37 +01:00
|
|
|
tap.test('should stop port proxy', async () => {
|
|
|
|
await portProxy.stop();
|
2025-02-27 14:23:44 +00:00
|
|
|
expect((portProxy as any).netServers.every((server: net.Server) => !server.listening)).toBeTrue();
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
// Remove from tracking
|
|
|
|
const index = allProxies.indexOf(portProxy);
|
|
|
|
if (index !== -1) allProxies.splice(index, 1);
|
2025-02-04 01:24:37 +01:00
|
|
|
});
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// Test chained proxies with and without source IP preservation.
|
2025-02-21 19:44:59 +00:00
|
|
|
tap.test('should support optional source IP preservation in chained proxies', async () => {
|
2025-02-27 21:19:34 +00:00
|
|
|
// Chained proxies without IP preservation.
|
2025-02-21 19:44:59 +00:00
|
|
|
const firstProxyDefault = new PortProxy({
|
2025-02-21 19:39:52 +00:00
|
|
|
fromPort: PROXY_PORT + 4,
|
2025-02-21 19:44:59 +00:00
|
|
|
toPort: PROXY_PORT + 5,
|
2025-02-27 14:23:44 +00:00
|
|
|
targetIP: 'localhost',
|
2025-02-27 21:19:34 +00:00
|
|
|
domainConfigs: [],
|
2025-02-21 19:39:52 +00:00
|
|
|
sniEnabled: false,
|
2025-02-27 14:23:44 +00:00
|
|
|
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
|
|
|
|
globalPortRanges: []
|
2025-02-21 19:39:52 +00:00
|
|
|
});
|
2025-02-21 19:44:59 +00:00
|
|
|
const secondProxyDefault = new PortProxy({
|
2025-02-21 19:39:52 +00:00
|
|
|
fromPort: PROXY_PORT + 5,
|
|
|
|
toPort: TEST_SERVER_PORT,
|
2025-02-27 14:23:44 +00:00
|
|
|
targetIP: 'localhost',
|
2025-02-27 21:19:34 +00:00
|
|
|
domainConfigs: [],
|
2025-02-21 19:39:52 +00:00
|
|
|
sniEnabled: false,
|
2025-02-27 14:23:44 +00:00
|
|
|
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'],
|
|
|
|
globalPortRanges: []
|
2025-02-21 19:39:52 +00:00
|
|
|
});
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
allProxies.push(firstProxyDefault, secondProxyDefault); // Track these proxies
|
|
|
|
|
2025-02-21 19:44:59 +00:00
|
|
|
await secondProxyDefault.start();
|
|
|
|
await firstProxyDefault.start();
|
|
|
|
const response1 = await createTestClient(PROXY_PORT + 4, TEST_DATA);
|
|
|
|
expect(response1).toEqual(`Echo: ${TEST_DATA}`);
|
|
|
|
await firstProxyDefault.stop();
|
|
|
|
await secondProxyDefault.stop();
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
// Remove from tracking
|
|
|
|
const index1 = allProxies.indexOf(firstProxyDefault);
|
|
|
|
if (index1 !== -1) allProxies.splice(index1, 1);
|
|
|
|
const index2 = allProxies.indexOf(secondProxyDefault);
|
|
|
|
if (index2 !== -1) allProxies.splice(index2, 1);
|
2025-02-21 19:44:59 +00:00
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// Chained proxies with IP preservation.
|
2025-02-21 19:44:59 +00:00
|
|
|
const firstProxyPreserved = new PortProxy({
|
|
|
|
fromPort: PROXY_PORT + 6,
|
|
|
|
toPort: PROXY_PORT + 7,
|
2025-02-27 14:23:44 +00:00
|
|
|
targetIP: 'localhost',
|
2025-02-27 21:19:34 +00:00
|
|
|
domainConfigs: [],
|
2025-02-21 19:44:59 +00:00
|
|
|
sniEnabled: false,
|
|
|
|
defaultAllowedIPs: ['127.0.0.1'],
|
2025-02-27 14:23:44 +00:00
|
|
|
preserveSourceIP: true,
|
|
|
|
globalPortRanges: []
|
2025-02-21 19:44:59 +00:00
|
|
|
});
|
|
|
|
const secondProxyPreserved = new PortProxy({
|
|
|
|
fromPort: PROXY_PORT + 7,
|
|
|
|
toPort: TEST_SERVER_PORT,
|
2025-02-27 14:23:44 +00:00
|
|
|
targetIP: 'localhost',
|
2025-02-27 21:19:34 +00:00
|
|
|
domainConfigs: [],
|
2025-02-21 19:44:59 +00:00
|
|
|
sniEnabled: false,
|
|
|
|
defaultAllowedIPs: ['127.0.0.1'],
|
2025-02-27 14:23:44 +00:00
|
|
|
preserveSourceIP: true,
|
|
|
|
globalPortRanges: []
|
2025-02-21 19:44:59 +00:00
|
|
|
});
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
allProxies.push(firstProxyPreserved, secondProxyPreserved); // Track these proxies
|
|
|
|
|
2025-02-21 19:44:59 +00:00
|
|
|
await secondProxyPreserved.start();
|
|
|
|
await firstProxyPreserved.start();
|
|
|
|
const response2 = await createTestClient(PROXY_PORT + 6, TEST_DATA);
|
|
|
|
expect(response2).toEqual(`Echo: ${TEST_DATA}`);
|
|
|
|
await firstProxyPreserved.stop();
|
|
|
|
await secondProxyPreserved.stop();
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
// Remove from tracking
|
|
|
|
const index3 = allProxies.indexOf(firstProxyPreserved);
|
|
|
|
if (index3 !== -1) allProxies.splice(index3, 1);
|
|
|
|
const index4 = allProxies.indexOf(secondProxyPreserved);
|
|
|
|
if (index4 !== -1) allProxies.splice(index4, 1);
|
2025-02-21 19:39:52 +00:00
|
|
|
});
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
// 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: []
|
|
|
|
});
|
|
|
|
|
2025-03-05 17:06:51 +00:00
|
|
|
// Don't track this proxy as it doesn't actually start or listen
|
|
|
|
|
2025-02-27 21:19:34 +00:00
|
|
|
const firstTarget = (proxyInstance as any).getTargetIP(domainConfig);
|
|
|
|
const secondTarget = (proxyInstance as any).getTargetIP(domainConfig);
|
|
|
|
expect(firstTarget).toEqual('hostA');
|
|
|
|
expect(secondTarget).toEqual('hostB');
|
|
|
|
});
|
|
|
|
|
2025-03-05 17:06:51 +00:00
|
|
|
// CLEANUP: Tear down all servers and proxies
|
2025-02-04 01:24:37 +01:00
|
|
|
tap.test('cleanup port proxy test environment', async () => {
|
2025-03-05 17:06:51 +00:00
|
|
|
// Stop all remaining proxies
|
|
|
|
for (const proxy of [...allProxies]) {
|
|
|
|
try {
|
|
|
|
await proxy.stop();
|
|
|
|
const index = allProxies.indexOf(proxy);
|
|
|
|
if (index !== -1) allProxies.splice(index, 1);
|
|
|
|
} catch (err) {
|
|
|
|
console.error(`Error stopping proxy: ${err}`);
|
|
|
|
}
|
2025-02-04 01:24:37 +01:00
|
|
|
}
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
// Close all remaining servers
|
|
|
|
for (const server of [...allServers]) {
|
|
|
|
try {
|
|
|
|
await new Promise<void>((resolve) => {
|
|
|
|
if (server.listening) {
|
|
|
|
server.close(() => resolve());
|
|
|
|
} else {
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
const index = allServers.indexOf(server);
|
|
|
|
if (index !== -1) allServers.splice(index, 1);
|
|
|
|
} catch (err) {
|
|
|
|
console.error(`Error closing server: ${err}`);
|
|
|
|
}
|
2025-02-04 01:24:37 +01:00
|
|
|
}
|
2025-03-05 17:06:51 +00:00
|
|
|
|
|
|
|
// Verify all resources are cleaned up
|
|
|
|
expect(allProxies.length).toEqual(0);
|
|
|
|
expect(allServers.length).toEqual(0);
|
2025-02-04 01:24:37 +01:00
|
|
|
});
|
|
|
|
|
2025-02-27 14:23:44 +00:00
|
|
|
export default tap.start();
|