udphole

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

commit 1ea8f9d276fd616b593629fe1d65d7ca7e6af389
parent 9e59b28efb7cac8516bb6039c1b20866cd10c780
Author: Robin Bron <robin.bron@yourhosting.nl>
Date:   Sun,  1 Mar 2026 14:06:55 +0100

Implement unix control socket

Diffstat:
MMakefile | 10+++++++---
Msrc/common/socket_util.c | 48++++++++++++++++++++++++++++++++++++++++++++----
Msrc/common/socket_util.h | 3++-
Msrc/interface/api/server.c | 12++++++++++++
Atest/basic-forwarding-tcp.js | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/basic-forwarding-unix.js | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/basic-forwarding.js | 99-------------------------------------------------------------------------------
Rtest/config.ini -> test/config-tcp.ini | 0
Atest/config-unix.ini | 9+++++++++
Mtest/helpers.js | 20++++++++++++++++++++
Atest/listen-relearn-tcp.js | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/listen-relearn-unix.js | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/listen-relearn.js | 148-------------------------------------------------------------------------------
13 files changed, 586 insertions(+), 255 deletions(-)

diff --git a/Makefile b/Makefile @@ -108,9 +108,13 @@ $(BIN): $(OBJ) .PHONY: test test: $(BIN) - @node test/basic-forwarding.js - @sleep 2 - @node test/listen-relearn.js + @node test/basic-forwarding-tcp.js + @sleep 1 + @node test/basic-forwarding-unix.js + @sleep 1 + @node test/listen-relearn-tcp.js + @sleep 1 + @node test/listen-relearn-unix.js .PHONY: clean clean: diff --git a/src/common/socket_util.c b/src/common/socket_util.c @@ -12,6 +12,8 @@ #include <errno.h> #include <sys/un.h> #include <sys/stat.h> +#include <pwd.h> +#include <grp.h> int set_socket_nonblocking(int fd, int nonblock) { int flags = fcntl(fd, F_GETFL, 0); @@ -277,7 +279,7 @@ int *udp_recv(const char *addr, const char *default_host, const char *default_po return fds; } -int *unix_listen(const char *path, int sock_type) { +int *unix_listen(const char *path, int sock_type, const char *owner) { if (!path || !path[0]) { log_error("unix_listen: empty path"); return NULL; @@ -336,9 +338,47 @@ int *unix_listen(const char *path, int sock_type) { return NULL; } - if (sock_type == SOCK_DGRAM) { - chmod(path_copy, 0777); - } else if (sock_type == SOCK_STREAM) { + if (owner && owner[0]) { + uid_t uid = -1; + gid_t gid = -1; + char *owner_copy = strdup(owner); + if (owner_copy) { + char *colon = strchr(owner_copy, ':'); + if (colon) { + *colon = '\0'; + colon++; + if (colon[0]) { + struct passwd *pw = getpwnam(owner_copy); + if (pw) { + uid = pw->pw_uid; + struct group *gr = getgrnam(colon); + if (gr) { + gid = gr->gr_gid; + } + } + } + } else { + struct passwd *pw = getpwnam(owner_copy); + if (pw) { + uid = pw->pw_uid; + gid = pw->pw_gid; + } + } + free(owner_copy); + } + if (uid != (uid_t)-1 || gid != (gid_t)-1) { + if (fchown(fd, uid, gid) < 0) { + log_error("unix_listen: fchown failed: %s", strerror(errno)); + close(fd); + unlink(path_copy); + free(path_copy); + free(fds); + return NULL; + } + } + } + + if (sock_type == SOCK_STREAM) { if (listen(fd, 8) < 0) { log_error("unix_listen: listen failed: %s", strerror(errno)); close(fd); diff --git a/src/common/socket_util.h b/src/common/socket_util.h @@ -15,8 +15,9 @@ int *tcp_listen(const char *addr, const char *default_host, const char *default_ int *udp_recv(const char *addr, const char *default_host, const char *default_port); /* Create Unix domain socket. path is the socket path, sock_type is SOCK_DGRAM or SOCK_STREAM. + * owner is optional and can be "user" or "user:group" to set socket ownership. * Returns int array: index 0 = count, index 1 = socket fd. Caller must free. * On error returns NULL. */ -int *unix_listen(const char *path, int sock_type); +int *unix_listen(const char *path, int sock_type, const char *owner); #endif diff --git a/src/interface/api/server.c b/src/interface/api/server.c @@ -429,6 +429,18 @@ static int *create_listen_socket(const char *listen_addr) { const char *cfg_port = resp_map_get_string(api_sec, "port"); if (cfg_port && cfg_port[0]) default_port = cfg_port; } + + if (listen_addr && strncmp(listen_addr, "unix://", 7) == 0) { + const char *socket_path = listen_addr + 7; + const char *socket_owner = api_sec ? resp_map_get_string(api_sec, "socket_owner") : NULL; + int *fds = unix_listen(socket_path, SOCK_STREAM, socket_owner); + if (!fds) { + return NULL; + } + log_info("api: listening on %s", listen_addr); + return fds; + } + int *fds = tcp_listen(listen_addr, NULL, default_port); if (!fds) { return NULL; diff --git a/test/basic-forwarding-tcp.js b/test/basic-forwarding-tcp.js @@ -0,0 +1,98 @@ +const path = require('path'); +const { + spawnDaemon, + killDaemon, + connectApi, + apiCommand, + createUdpEchoServer, + sendUdp, + TIMEOUT +} = require('./helpers'); + +const CONFIG_PATH = path.join(__dirname, 'config-tcp.ini'); +const API_PORT = 9123; + +async function runTest() { + let daemon = null; + let apiSock = null; + let echoServer = null; + + console.log('=== Basic Forwarding Test ==='); + console.log('Testing: UDP packets are forwarded from listen socket to connect socket\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-session', '60'); + console.log(` Session create: ${resp}`); + + console.log('5. Creating listen socket...'); + resp = await apiCommand(apiSock, 'session.socket.create.listen', 'test-session', 'client-a'); + 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-session', 'relay', '127.0.0.1', echoServer.port); + console.log(` Connect socket: ${resp}`); + + console.log('8. Creating forward: client-a -> relay...'); + resp = await apiCommand(apiSock, 'session.forward.create', 'test-session', 'client-a', 'relay'); + console.log(` Forward create: ${resp}`); + + console.log(' Waiting for session to initialize...'); + await new Promise(r => setTimeout(r, 100)); + + console.log('9. Sending UDP packet to listen socket...'); + await sendUdp(listenPort, '127.0.0.1', 'hello'); + console.log(' Sent "hello"'); + + console.log('10. Waiting for echo response...'); + const messages = echoServer.getMessages(); + const start = Date.now(); + while (messages.length === 0 && Date.now() - start < TIMEOUT) { + await new Promise(r => setTimeout(r, 50)); + } + + if (messages.length === 0) { + throw new Error('Timeout: no message received by echo server'); + } + + const msg = messages[0]; + console.log(` Received: "${msg.data}" from ${msg.rinfo.address}:${msg.rinfo.port}`); + + if (msg.data === 'hello') { + console.log('\n✓ PASS: UDP forwarding works correctly'); + console.log(' Packet was forwarded from listen socket to connect socket'); + console.log(' and echoed back successfully.'); + process.exit(0); + } else { + throw new Error(`Expected "hello", got "${msg.data}"`); + } + + } catch (err) { + console.error(`\n✗ FAIL: ${err.message}`); + process.exit(1); + } finally { + if (echoServer) echoServer.socket.close(); + if (apiSock) apiSock.end(); + if (daemon) await killDaemon(daemon); + } +} + +runTest(); +\ No newline at end of file diff --git a/test/basic-forwarding-unix.js b/test/basic-forwarding-unix.js @@ -0,0 +1,98 @@ +const path = require('path'); +const { + spawnDaemon, + killDaemon, + connectUnixApi, + apiCommand, + createUdpEchoServer, + sendUdp, + TIMEOUT +} = require('./helpers'); + +const CONFIG_PATH = path.join(__dirname, 'config-unix.ini'); +const API_SOCKET = '/tmp/udphole-test.sock'; + +async function runTest() { + let daemon = null; + let apiSock = null; + let echoServer = null; + + console.log('=== Basic Forwarding Test (Unix Socket) ==='); + console.log('Testing: UDP packets are forwarded from listen socket to connect socket\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 via Unix socket...'); + apiSock = await connectUnixApi(API_SOCKET); + 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-session', '60'); + console.log(` Session create: ${resp}`); + + console.log('5. Creating listen socket...'); + resp = await apiCommand(apiSock, 'session.socket.create.listen', 'test-session', 'client-a'); + 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-session', 'relay', '127.0.0.1', echoServer.port); + console.log(` Connect socket: ${resp}`); + + console.log('8. Creating forward: client-a -> relay...'); + resp = await apiCommand(apiSock, 'session.forward.create', 'test-session', 'client-a', 'relay'); + console.log(` Forward create: ${resp}`); + + console.log(' Waiting for session to initialize...'); + await new Promise(r => setTimeout(r, 100)); + + console.log('9. Sending UDP packet to listen socket...'); + await sendUdp(listenPort, '127.0.0.1', 'hello'); + console.log(' Sent "hello"'); + + console.log('10. Waiting for echo response...'); + const messages = echoServer.getMessages(); + const start = Date.now(); + while (messages.length === 0 && Date.now() - start < TIMEOUT) { + await new Promise(r => setTimeout(r, 50)); + } + + if (messages.length === 0) { + throw new Error('Timeout: no message received by echo server'); + } + + const msg = messages[0]; + console.log(` Received: "${msg.data}" from ${msg.rinfo.address}:${msg.rinfo.port}`); + + if (msg.data === 'hello') { + console.log('\n✓ PASS: UDP forwarding works correctly (Unix socket)'); + console.log(' Packet was forwarded from listen socket to connect socket'); + console.log(' and echoed back successfully.'); + process.exit(0); + } else { + throw new Error(`Expected "hello", got "${msg.data}"`); + } + + } catch (err) { + console.error(`\n✗ FAIL: ${err.message}`); + process.exit(1); + } finally { + if (echoServer) echoServer.socket.close(); + if (apiSock) apiSock.end(); + if (daemon) await killDaemon(daemon); + } +} + +runTest(); diff --git a/test/basic-forwarding.js b/test/basic-forwarding.js @@ -1,98 +0,0 @@ -const path = require('path'); -const { - spawnDaemon, - killDaemon, - connectApi, - apiCommand, - createUdpEchoServer, - sendUdp, - TIMEOUT -} = require('./helpers'); - -const CONFIG_PATH = path.join(__dirname, 'config.ini'); -const API_PORT = 9123; - -async function runTest() { - let daemon = null; - let apiSock = null; - let echoServer = null; - - console.log('=== Basic Forwarding Test ==='); - console.log('Testing: UDP packets are forwarded from listen socket to connect socket\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-session', '60'); - console.log(` Session create: ${resp}`); - - console.log('5. Creating listen socket...'); - resp = await apiCommand(apiSock, 'session.socket.create.listen', 'test-session', 'client-a'); - 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-session', 'relay', '127.0.0.1', echoServer.port); - console.log(` Connect socket: ${resp}`); - - console.log('8. Creating forward: client-a -> relay...'); - resp = await apiCommand(apiSock, 'session.forward.create', 'test-session', 'client-a', 'relay'); - console.log(` Forward create: ${resp}`); - - console.log(' Waiting for session to initialize...'); - await new Promise(r => setTimeout(r, 100)); - - console.log('9. Sending UDP packet to listen socket...'); - await sendUdp(listenPort, '127.0.0.1', 'hello'); - console.log(' Sent "hello"'); - - console.log('10. Waiting for echo response...'); - const messages = echoServer.getMessages(); - const start = Date.now(); - while (messages.length === 0 && Date.now() - start < TIMEOUT) { - await new Promise(r => setTimeout(r, 50)); - } - - if (messages.length === 0) { - throw new Error('Timeout: no message received by echo server'); - } - - const msg = messages[0]; - console.log(` Received: "${msg.data}" from ${msg.rinfo.address}:${msg.rinfo.port}`); - - if (msg.data === 'hello') { - console.log('\n✓ PASS: UDP forwarding works correctly'); - console.log(' Packet was forwarded from listen socket to connect socket'); - console.log(' and echoed back successfully.'); - process.exit(0); - } else { - throw new Error(`Expected "hello", got "${msg.data}"`); - } - - } catch (err) { - console.error(`\n✗ FAIL: ${err.message}`); - process.exit(1); - } finally { - if (echoServer) echoServer.socket.close(); - if (apiSock) apiSock.end(); - if (daemon) await killDaemon(daemon); - } -} - -runTest(); -\ No newline at end of file diff --git a/test/config.ini b/test/config-tcp.ini diff --git a/test/config-unix.ini b/test/config-unix.ini @@ -0,0 +1,9 @@ +[udphole] +mode = builtin +ports = 9100-9199 +listen = unix:///tmp/udphole-test.sock +socket_owner = + +[user:finwo] +permit = * +secret = testsecret diff --git a/test/helpers.js b/test/helpers.js @@ -88,6 +88,25 @@ function connectApi(port) { }); } +function connectUnixApi(socketPath) { + return new Promise((resolve, reject) => { + const sock = net.createConnection({ path: socketPath, noDelay: true }); + sock.setEncoding('utf8'); + + const timeout = setTimeout(() => { + sock.destroy(); + reject(new Error('Connection timeout')); + }, TIMEOUT); + + sock.on('connect', () => { + clearTimeout(timeout); + resolve(sock); + }); + + sock.on('error', reject); + }); +} + function encodeResp(...args) { const n = args.length; let cmd = `*${n}\r\n`; @@ -217,6 +236,7 @@ module.exports = { spawnDaemon, killDaemon, connectApi, + connectUnixApi, apiCommand, createUdpEchoServer, sendUdp, diff --git a/test/listen-relearn-tcp.js b/test/listen-relearn-tcp.js @@ -0,0 +1,147 @@ +const path = require('path'); +const dgram = require('dgram'); +const { + spawnDaemon, + killDaemon, + connectApi, + apiCommand, + createUdpEchoServer, + TIMEOUT +} = require('./helpers'); + +const CONFIG_PATH = path.join(__dirname, 'config-tcp.ini'); +const API_PORT = 9123; + +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; + + console.log('=== Listen Socket Re-learn Test ==='); + console.log('Testing: listen socket re-learns remote address when different client sends\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-relearn', '60'); + console.log(` Session create: ${resp}`); + + console.log('5. Creating listen socket...'); + resp = await apiCommand(apiSock, 'session.socket.create.listen', 'test-relearn', 'listener'); + 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-relearn', 'relay', '127.0.0.1', echoServer.port); + console.log(` Connect socket: ${resp}`); + + console.log('8. Creating forward: listener -> relay...'); + resp = await apiCommand(apiSock, 'session.forward.create', 'test-relearn', 'listener', 'relay'); + console.log(` Forward create: ${resp}`); + + console.log('9. First client sending "from-A" from port 50001...'); + await sendUdpFromPort(50001, listenPort, 'from-A'); + console.log(' Sent "from-A"'); + + 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 from first client'); + } + + const msgA = messages[0]; + console.log(` Received: "${msgA.data}" from ${msgA.rinfo.address}:${msgA.rinfo.port}`); + + if (msgA.data !== 'from-A') { + throw new Error(`Expected "from-A", got "${msgA.data}"`); + } + console.log(' ✓ First client connection established, listen socket learned address'); + + echoServer.clearMessages(); + + console.log('10. Second client sending "from-B" from port 50002...'); + await sendUdpFromPort(50002, listenPort, 'from-B'); + console.log(' Sent "from-B"'); + + 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 from second client'); + } + + const msgB = messages[0]; + console.log(` Received: "${msgB.data}" from ${msgB.rinfo.address}:${msgB.rinfo.port}`); + + if (msgB.data === 'from-B') { + console.log('\n✓ PASS: Listen socket correctly re-learned new remote address'); + console.log(' Second client (from port 50002) was able to communicate'); + console.log(' through the same listen socket after the first client.'); + process.exit(0); + } else if (msgB.data === 'from-A') { + console.log('\n✗ FAIL: Listen socket did NOT re-learn new remote address'); + console.log(' The second client\'s packet was sent back to the first client'); + console.log(' instead of the second client. This is the bug to fix.'); + console.log(` Expected to receive from port 50002, but responses went to port ${msgA.rinfo.port}`); + process.exit(1); + } else { + throw new Error(`Unexpected message: "${msgB.data}"`); + } + + } catch (err) { + console.error(`\n✗ FAIL: ${err.message}`); + process.exit(1); + } finally { + if (echoServer) echoServer.socket.close(); + if (apiSock) apiSock.end(); + if (daemon) await killDaemon(daemon); + } +} + +runTest(); +\ No newline at end of file diff --git a/test/listen-relearn-unix.js b/test/listen-relearn-unix.js @@ -0,0 +1,147 @@ +const path = require('path'); +const dgram = require('dgram'); +const { + spawnDaemon, + killDaemon, + connectUnixApi, + apiCommand, + createUdpEchoServer, + TIMEOUT +} = require('./helpers'); + +const CONFIG_PATH = path.join(__dirname, 'config-unix.ini'); +const API_SOCKET = '/tmp/udphole-test.sock'; + +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; + + console.log('=== Listen Socket Re-learn Test (Unix Socket) ==='); + console.log('Testing: listen socket re-learns remote address when different client sends\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 via Unix socket...'); + apiSock = await connectUnixApi(API_SOCKET); + 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-relearn', '60'); + console.log(` Session create: ${resp}`); + + console.log('5. Creating listen socket...'); + resp = await apiCommand(apiSock, 'session.socket.create.listen', 'test-relearn', 'listener'); + 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-relearn', 'relay', '127.0.0.1', echoServer.port); + console.log(` Connect socket: ${resp}`); + + console.log('8. Creating forward: listener -> relay...'); + resp = await apiCommand(apiSock, 'session.forward.create', 'test-relearn', 'listener', 'relay'); + console.log(` Forward create: ${resp}`); + + console.log('9. First client sending "from-A" from port 50001...'); + await sendUdpFromPort(50001, listenPort, 'from-A'); + console.log(' Sent "from-A"'); + + 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 from first client'); + } + + const msgA = messages[0]; + console.log(` Received: "${msgA.data}" from ${msgA.rinfo.address}:${msgA.rinfo.port}`); + + if (msgA.data !== 'from-A') { + throw new Error(`Expected "from-A", got "${msgA.data}"`); + } + console.log(' ✓ First client connection established, listen socket learned address'); + + echoServer.clearMessages(); + + console.log('10. Second client sending "from-B" from port 50002...'); + await sendUdpFromPort(50002, listenPort, 'from-B'); + console.log(' Sent "from-B"'); + + 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 from second client'); + } + + const msgB = messages[0]; + console.log(` Received: "${msgB.data}" from ${msgB.rinfo.address}:${msgB.rinfo.port}`); + + if (msgB.data === 'from-B') { + console.log('\n✓ PASS: Listen socket correctly re-learned new remote address (Unix socket)'); + console.log(' Second client (from port 50002) was able to communicate'); + console.log(' through the same listen socket after the first client.'); + process.exit(0); + } else if (msgB.data === 'from-A') { + console.log('\n✗ FAIL: Listen socket did NOT re-learn new remote address'); + console.log(' The second client\'s packet was sent back to the first client'); + console.log(' instead of the second client. This is the bug to fix.'); + console.log(` Expected to receive from port 50002, but responses went to port ${msgA.rinfo.port}`); + process.exit(1); + } else { + throw new Error(`Unexpected message: "${msgB.data}"`); + } + + } catch (err) { + console.error(`\n✗ FAIL: ${err.message}`); + process.exit(1); + } finally { + if (echoServer) echoServer.socket.close(); + if (apiSock) apiSock.end(); + if (daemon) await killDaemon(daemon); + } +} + +runTest(); diff --git a/test/listen-relearn.js b/test/listen-relearn.js @@ -1,147 +0,0 @@ -const path = require('path'); -const dgram = require('dgram'); -const { - spawnDaemon, - killDaemon, - connectApi, - apiCommand, - createUdpEchoServer, - TIMEOUT -} = require('./helpers'); - -const CONFIG_PATH = path.join(__dirname, 'config.ini'); -const API_PORT = 9123; - -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; - - console.log('=== Listen Socket Re-learn Test ==='); - console.log('Testing: listen socket re-learns remote address when different client sends\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-relearn', '60'); - console.log(` Session create: ${resp}`); - - console.log('5. Creating listen socket...'); - resp = await apiCommand(apiSock, 'session.socket.create.listen', 'test-relearn', 'listener'); - 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-relearn', 'relay', '127.0.0.1', echoServer.port); - console.log(` Connect socket: ${resp}`); - - console.log('8. Creating forward: listener -> relay...'); - resp = await apiCommand(apiSock, 'session.forward.create', 'test-relearn', 'listener', 'relay'); - console.log(` Forward create: ${resp}`); - - console.log('9. First client sending "from-A" from port 50001...'); - await sendUdpFromPort(50001, listenPort, 'from-A'); - console.log(' Sent "from-A"'); - - 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 from first client'); - } - - const msgA = messages[0]; - console.log(` Received: "${msgA.data}" from ${msgA.rinfo.address}:${msgA.rinfo.port}`); - - if (msgA.data !== 'from-A') { - throw new Error(`Expected "from-A", got "${msgA.data}"`); - } - console.log(' ✓ First client connection established, listen socket learned address'); - - echoServer.clearMessages(); - - console.log('10. Second client sending "from-B" from port 50002...'); - await sendUdpFromPort(50002, listenPort, 'from-B'); - console.log(' Sent "from-B"'); - - 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 from second client'); - } - - const msgB = messages[0]; - console.log(` Received: "${msgB.data}" from ${msgB.rinfo.address}:${msgB.rinfo.port}`); - - if (msgB.data === 'from-B') { - console.log('\n✓ PASS: Listen socket correctly re-learned new remote address'); - console.log(' Second client (from port 50002) was able to communicate'); - console.log(' through the same listen socket after the first client.'); - process.exit(0); - } else if (msgB.data === 'from-A') { - console.log('\n✗ FAIL: Listen socket did NOT re-learn new remote address'); - console.log(' The second client\'s packet was sent back to the first client'); - console.log(' instead of the second client. This is the bug to fix.'); - console.log(` Expected to receive from port 50002, but responses went to port ${msgA.rinfo.port}`); - process.exit(1); - } else { - throw new Error(`Unexpected message: "${msgB.data}"`); - } - - } catch (err) { - console.error(`\n✗ FAIL: ${err.message}`); - process.exit(1); - } finally { - if (echoServer) echoServer.socket.close(); - if (apiSock) apiSock.end(); - if (daemon) await killDaemon(daemon); - } -} - -runTest(); -\ No newline at end of file