udphole

Basic UDP wormhole proxy
git clone git://git.finwo.net/app/udphole
Log | Files | Refs | README | LICENSE

commit 34255c71e52129921101ff1c5e1d5b80b026cb6a
parent c3f2475434cf6a45b4de73ef449acc4b8e3e08db
Author: Robin Bron <robin.bron@yourhosting.nl>
Date:   Sun,  1 Mar 2026 21:37:48 +0100

Add test to verify dropping unknown sources

Diffstat:
MMakefile | 2++
Atest/connect-drop-unknown.js | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 255 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -121,6 +121,8 @@ test: $(BIN) @node test/cluster-basic.js @sleep 1 @node test/cluster-integration.js + @sleep 1 + @node test/connect-drop-unknown.js .PHONY: clean clean: diff --git a/test/connect-drop-unknown.js b/test/connect-drop-unknown.js @@ -0,0 +1,253 @@ +const path = require('path'); +const dgram = require('dgram'); +const { + spawnDaemon, + killDaemon, + killAllDaemons, + connectApi, + apiCommand, + createUdpEchoServer, + TIMEOUT +} = require('./helpers'); + +const CONFIG_PATH = path.join(__dirname, 'config-tcp.ini'); +const API_PORT = 9123; + +function sendAndRecvUdp(srcPort, dstPort, message) { + return new Promise((resolve, reject) => { + const sock = dgram.createSocket('udp4'); + let resolved = false; + + const timer = setTimeout(() => { + if (!resolved) { + resolved = true; + sock.close(); + resolve(null); + } + }, TIMEOUT); + + sock.on('message', (msg, rinfo) => { + if (!resolved) { + resolved = true; + clearTimeout(timer); + sock.close(); + resolve({ data: msg.toString(), rinfo }); + } + }); + + sock.bind(srcPort, '127.0.0.1', () => { + const buf = Buffer.from(message); + sock.send(buf, 0, buf.length, dstPort, '127.0.0.1', (err) => { + if (err && !resolved) { + resolved = true; + clearTimeout(timer); + sock.close(); + reject(err); + } + }); + }); + + sock.on('error', (err) => { + if (!resolved) { + resolved = true; + clearTimeout(timer); + sock.close(); + reject(err); + } + }); + }); +} + +function sendUdpFromPort(srcPort, dstPort, message) { + return new Promise((resolve, reject) => { + const sock = dgram.createSocket('udp4'); + sock.bind(srcPort, '127.0.0.1', () => { + const buf = Buffer.from(message); + sock.send(buf, 0, buf.length, dstPort, '127.0.0.1', (err) => { + if (err) { + sock.close(); + reject(err); + } else { + setTimeout(() => { + sock.close(); + resolve(); + }, 50); + } + }); + }); + sock.on('error', reject); + }); +} + +async function runTest() { + let daemon = null; + let apiSock = null; + let echoServer = null; + let returnCode = 0; + + console.log('=== Connect Socket Drop Unknown Test ==='); + console.log('Testing: connect mode sockets drop traffic from unknown remote addresses\n'); + + try { + console.log('1. Spawning daemon...'); + daemon = await spawnDaemon(CONFIG_PATH); + console.log(` Daemon started (PID: ${daemon.pid})`); + + console.log('2. Connecting to API...'); + apiSock = await connectApi(API_PORT); + console.log(' Connected to API'); + + console.log('3. Authenticating...'); + let resp = await apiCommand(apiSock, 'auth', 'finwo', 'testsecret'); + console.log(` Auth response: ${resp}`); + if (resp !== 'OK') throw new Error('Authentication failed'); + + console.log('4. Creating session...'); + resp = await apiCommand(apiSock, 'session.create', 'test-connect-drop', '60'); + console.log(` Session create: ${resp}`); + + console.log('5. Creating listen socket...'); + resp = await apiCommand(apiSock, 'session.socket.create.listen', 'test-connect-drop', 'listen'); + const listenPort = resp[0]; + console.log(` Listen socket port: ${listenPort}`); + + console.log('6. Starting echo server...'); + echoServer = await createUdpEchoServer(); + console.log(` Echo server on port: ${echoServer.port}`); + + console.log('7. Creating connect socket to echo server...'); + resp = await apiCommand(apiSock, 'session.socket.create.connect', 'test-connect-drop', 'connect', '127.0.0.1', echoServer.port); + const connectPort = resp[0]; + console.log(` Connect socket local port: ${connectPort}`); + + console.log('8. Creating forward: listen -> connect...'); + resp = await apiCommand(apiSock, 'session.forward.create', 'test-connect-drop', 'listen', 'connect'); + console.log(` Forward create: ${resp}`); + + console.log('9. Creating forward: connect -> listen (for echo responses)...'); + resp = await apiCommand(apiSock, 'session.forward.create', 'test-connect-drop', 'connect', 'listen'); + console.log(` Forward create: ${resp}`); + + console.log('10. Waiting for session to initialize...'); + await new Promise(r => setTimeout(r, 100)); + + console.log('\n=== Step 1: Happy path - active to listen, through proxy, back to active ==='); + console.log('11. Sending "step1-test" from port 50001 to listen socket...'); + + const recvPromise1 = sendAndRecvUdp(50001, listenPort, 'step1-test'); + console.log(' Sent "step1-test", waiting for response...'); + + let messages = echoServer.getMessages(); + let start = Date.now(); + while (messages.length === 0 && Date.now() - start < TIMEOUT) { + await new Promise(r => setTimeout(r, 50)); + messages = echoServer.getMessages(); + } + + if (messages.length === 0) { + throw new Error('Timeout: no message received by echo server in step 1'); + } + + const msgStep1 = messages[0]; + console.log(` Echo server received: "${msgStep1.data}" from ${msgStep1.rinfo.address}:${msgStep1.rinfo.port}`); + + if (msgStep1.data !== 'step1-test') { + throw new Error(`Expected "step1-test", got "${msgStep1.data}"`); + } + console.log(' ✓ Echo server received the packet'); + + echoServer.clearMessages(); + + const responseToSender1 = await recvPromise1; + if (responseToSender1 === null) { + throw new Error('Timeout: no response received back at sender port 50001'); + } + console.log(` ✓ Sender received echo response: "${responseToSender1.data}" from ${responseToSender1.rinfo.address}:${responseToSender1.rinfo.port}`); + + if (responseToSender1.data !== 'step1-test') { + throw new Error(`Expected echo "step1-test", got "${responseToSender1.data}"`); + } + + console.log('\n=== Step 2: Verify connect socket drops unknown source ==='); + console.log('12. Sending "step2-direct" directly to connect socket from port 50002...'); + + const recvPromise2 = sendAndRecvUdp(50002, connectPort, 'step2-direct'); + console.log(' Sent "step2-direct", waiting for response...'); + + messages = echoServer.getMessages(); + start = Date.now(); + while (Date.now() - start < 500) { + await new Promise(r => setTimeout(r, 50)); + messages = echoServer.getMessages(); + } + + if (messages.length > 0) { + console.log(` ✗ FAIL: Echo server received packet from connect socket (should have been dropped)`); + console.log(` Received: "${messages[0].data}" from ${messages[0].rinfo.address}:${messages[0].rinfo.port}`); + throw new Error('Connect socket did not drop traffic from unknown source'); + } + console.log(' ✓ Echo server did NOT receive the packet (correctly dropped)'); + + const responseToSender2 = await recvPromise2; + if (responseToSender2 !== null) { + console.log(` ✗ FAIL: Sender received response from connect socket (should have been dropped)`); + console.log(` Received: "${responseToSender2.data}" from ${responseToSender2.rinfo.address}:${responseToSender2.rinfo.port}`); + throw new Error('Connect socket did not drop traffic - response sent back to sender'); + } + console.log(' ✓ Sender did NOT receive any response (correctly dropped)'); + console.log('\n✓ PASS: Connect socket correctly dropped traffic from unknown source'); + + console.log('\n=== Step 3: Verify listen->connect forward still works ==='); + echoServer.clearMessages(); + + console.log('13. Sending "step3-retry" from port 50001 to listen socket again...'); + + const recvPromise3 = sendAndRecvUdp(50001, listenPort, 'step3-retry'); + console.log(' Sent "step3-retry", waiting for response...'); + + messages = echoServer.getMessages(); + start = Date.now(); + while (messages.length === 0 && Date.now() - start < TIMEOUT) { + await new Promise(r => setTimeout(r, 50)); + messages = echoServer.getMessages(); + } + + if (messages.length === 0) { + throw new Error('Timeout: no message received by echo server in step 3'); + } + + const msgStep3 = messages[0]; + console.log(` Echo server received: "${msgStep3.data}" from ${msgStep3.rinfo.address}:${msgStep3.rinfo.port}`); + + if (msgStep3.data !== 'step3-retry') { + throw new Error(`Expected "step3-retry", got "${msgStep3.data}"`); + } + console.log(' ✓ Echo server received the packet'); + + echoServer.clearMessages(); + + const responseToSender3 = await recvPromise3; + if (responseToSender3 === null) { + throw new Error('Timeout: no response received back at sender port 50001 in step 3'); + } + console.log(` ✓ Sender received echo response: "${responseToSender3.data}" from ${responseToSender3.rinfo.address}:${responseToSender3.rinfo.port}`); + + if (responseToSender3.data !== 'step3-retry') { + throw new Error(`Expected echo "step3-retry", got "${responseToSender3.data}"`); + } + + console.log('\n✓ PASS: Connect socket drop unknown test completed successfully'); + + } catch (err) { + console.error(`\n✗ FAIL: ${err.message}`); + returnCode = 1; + } finally { + if (echoServer) echoServer.socket.close(); + if (apiSock) apiSock.end(); + if (daemon) await killDaemon(daemon); + await killAllDaemons(); + process.exit(returnCode); + } +} + +runTest();