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:
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();