udphole

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

commit 4882b57353a03a465ced0b4b52e03e8ee5851090
parent 0dc05c2ae0bb46242c889c4f75f3799198c0fcfa
Author: Robin Bron <robin.bron@yourhosting.nl>
Date:   Sun,  1 Mar 2026 14:35:35 +0100

Fix session_destroy memory leak ; add system.load and session.count commands

Diffstat:
MMakefile | 2++
MREADME.md | 7+++++++
Msrc/domain/session.c | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtest/helpers.js | 2++
Atest/system-commands.js | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 178 insertions(+), 1 deletion(-)

diff --git a/Makefile b/Makefile @@ -108,6 +108,8 @@ $(BIN): $(OBJ) .PHONY: test test: $(BIN) + @node test/system-commands.js + @sleep 1 @node test/basic-forwarding-tcp.js @sleep 1 @node test/basic-forwarding-unix.js diff --git a/README.md b/README.md @@ -196,6 +196,13 @@ The API uses the RESP2 (Redis) protocol. Connect with `redis-cli` or any Redis c | `session.forward.create <session_id> <src_socket_id> <dst_socket_id>` | `+OK` | | `session.forward.destroy <session_id> <src_socket_id> <dst_socket_id>` | `+OK` | +### System commands + +| Command | Response | +|---------|----------| +| `system.load` | Array: `[1min, 5min, 15min]` load averages | +| `session.count` | Integer: number of active sessions | + --- A generic session-based UDP wormhole proxy. diff --git a/src/domain/session.c b/src/domain/session.c @@ -164,9 +164,29 @@ static void free_socket(socket_t *sock) { static void destroy_session(session_t *s) { if (!s) return; s->marked_for_deletion = 1; + + for (size_t i = 0; i < s->sockets_count; i++) { + if (s->sockets[i]) { + free_socket(s->sockets[i]); + } + } + free(s->sockets); + + for (size_t i = 0; i < s->forwards_count; i++) { + free(s->forwards[i].src_socket_id); + free(s->forwards[i].dst_socket_id); + } + free(s->forwards); + + free(s->session_id); + free(s); + for (size_t i = 0; i < sessions_count; i++) { if (sessions[i] == s) { - sessions[i] = NULL; + for (size_t j = i; j < sessions_count - 1; j++) { + sessions[j] = sessions[j + 1]; + } + sessions_count--; break; } } @@ -743,6 +763,48 @@ static char cmd_forward_destroy(api_client_t *c, char **args, int nargs) { return api_write_ok(c) ? 1 : 0; } +static char cmd_system_load(api_client_t *c, char **args, int nargs) { + (void)args; + if (nargs != 1) { + return api_write_err(c, "wrong number of arguments for 'system.load'") ? 1 : 0; + } + + double loadavg[3]; + if (getloadavg(loadavg, 3) != 3) { + return api_write_err(c, "failed to get load average") ? 1 : 0; + } + + if (!api_write_array(c, 6)) return 0; + if (!api_write_bulk_cstr(c, "1min")) return 0; + char buf[64]; + snprintf(buf, sizeof(buf), "%.2f", loadavg[0]); + if (!api_write_bulk_cstr(c, buf)) return 0; + if (!api_write_bulk_cstr(c, "5min")) return 0; + snprintf(buf, sizeof(buf), "%.2f", loadavg[1]); + if (!api_write_bulk_cstr(c, buf)) return 0; + if (!api_write_bulk_cstr(c, "15min")) return 0; + snprintf(buf, sizeof(buf), "%.2f", loadavg[2]); + if (!api_write_bulk_cstr(c, buf)) return 0; + + return 1; +} + +static char cmd_session_count(api_client_t *c, char **args, int nargs) { + (void)args; + if (nargs != 1) { + return api_write_err(c, "wrong number of arguments for 'session.count'") ? 1 : 0; + } + + size_t count = 0; + for (size_t i = 0; i < sessions_count; i++) { + if (sessions[i] != NULL) { + count++; + } + } + + return api_write_int(c, (int)count) ? 1 : 0; +} + static void register_session_commands(void) { api_register_cmd("session.create", cmd_session_create); api_register_cmd("session.list", cmd_session_list); @@ -754,6 +816,8 @@ static void register_session_commands(void) { api_register_cmd("session.forward.list", cmd_forward_list); api_register_cmd("session.forward.create", cmd_forward_create); api_register_cmd("session.forward.destroy", cmd_forward_destroy); + api_register_cmd("session.count", cmd_session_count); + api_register_cmd("system.load", cmd_system_load); log_info("udphole: registered session.* commands"); } diff --git a/test/helpers.js b/test/helpers.js @@ -156,6 +156,8 @@ function parseResp(data) { result.push(parseInt(lines[i].substring(1), 10)); } else if (lines[i].startsWith('+')) { result.push(lines[i].substring(1)); + } else if (lines[i].startsWith('-')) { + throw new Error(lines[i].substring(1)); } i++; } diff --git a/test/system-commands.js b/test/system-commands.js @@ -0,0 +1,102 @@ +const path = require('path'); +const { + spawnDaemon, + killDaemon, + connectApi, + apiCommand +} = require('./helpers'); + +const CONFIG_PATH = path.join(__dirname, 'config-tcp.ini'); +const API_PORT = 9123; + +async function runTest() { + let daemon = null; + let apiSock = null; + + console.log('=== System Commands Test ==='); + console.log('Testing: system.load and session.count commands\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. Testing session.count (no sessions)...'); + resp = await apiCommand(apiSock, 'session.count'); + console.log(` session.count: ${resp}`); + if (typeof resp !== 'number' || resp !== 0) { + throw new Error(`Expected session.count = 0, got ${resp}`); + } + + console.log('5. Creating a session...'); + resp = await apiCommand(apiSock, 'session.create', 'test-session', '60'); + console.log(` Session create: ${resp}`); + + console.log('6. Testing session.count (with 1 session)...'); + resp = await apiCommand(apiSock, 'session.count'); + console.log(` session.count: ${resp}`); + if (typeof resp !== 'number' || resp !== 1) { + throw new Error(`Expected session.count = 1, got ${resp}`); + } + + console.log('7. Creating another session...'); + resp = await apiCommand(apiSock, 'session.create', 'test-session-2', '60'); + console.log(` Session create: ${resp}`); + + console.log('8. Testing session.count (with 2 sessions)...'); + resp = await apiCommand(apiSock, 'session.count'); + console.log(` session.count: ${resp}`); + if (typeof resp !== 'number' || resp !== 2) { + throw new Error(`Expected session.count = 2, got ${resp}`); + } + + console.log('9. Testing system.load...'); + resp = await apiCommand(apiSock, 'system.load'); + console.log(` system.load: ${JSON.stringify(resp)}`); + if (!Array.isArray(resp) || resp.length !== 6) { + throw new Error(`Expected array with 6 elements, got ${JSON.stringify(resp)}`); + } + if (resp[0] !== '1min' || resp[2] !== '5min' || resp[4] !== '15min') { + throw new Error(`Expected keys [1min, 5min, 15min], got ${JSON.stringify(resp)}`); + } + const load1 = parseFloat(resp[1]); + const load5 = parseFloat(resp[3]); + const load15 = parseFloat(resp[5]); + console.log(` Parsed loads: 1min=${load1}, 5min=${load5}, 15min=${load15}`); + if (isNaN(load1) || isNaN(load5) || isNaN(load15)) { + throw new Error('Load values should be valid numbers'); + } + + console.log('10. Destroying a session...'); + resp = await apiCommand(apiSock, 'session.destroy', 'test-session'); + console.log(` Session destroy: ${resp}`); + + console.log('11. Testing session.count (after destroy)...'); + resp = await apiCommand(apiSock, 'session.count'); + console.log(` session.count: ${resp}`); + if (typeof resp !== 'number' || resp !== 1) { + throw new Error(`Expected session.count = 1, got ${resp}`); + } + + console.log('\n✓ PASS: All system commands work correctly'); + process.exit(0); + + } catch (err) { + console.error(`\n✗ FAIL: ${err.message}`); + process.exit(1); + } finally { + if (apiSock) apiSock.end(); + if (daemon) await killDaemon(daemon); + } +} + +runTest();