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:
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