commit c3f2475434cf6a45b4de73ef449acc4b8e3e08db
parent ef77b1ffb134ba340fff1cd95d4a8d79369e6e1d
Author: Robin Bron <robin.bron@yourhosting.nl>
Date: Sun, 1 Mar 2026 20:53:13 +0100
Add cluster command to set up complete networks of proxies
Diffstat:
24 files changed, 2136 insertions(+), 1066 deletions(-)
diff --git a/Makefile b/Makefile
@@ -117,6 +117,10 @@ test: $(BIN)
@node test/listen-relearn-tcp.js
@sleep 1
@node test/listen-relearn-unix.js
+ @sleep 1
+ @node test/cluster-basic.js
+ @sleep 1
+ @node test/cluster-integration.js
.PHONY: clean
clean:
diff --git a/src/domain/cluster/node.c b/src/domain/cluster/node.c
@@ -0,0 +1,270 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <ctype.h>
+#include <stdarg.h>
+#include <errno.h>
+
+#include "rxi/log.h"
+#include "domain/cluster/node.h"
+#include "common/resp.h"
+
+#define MAX_HOST_LEN 256
+#define MAX_PORT_LEN 16
+#define MAX_PATH_LEN 256
+
+static int parse_url(const char *url, char *host, size_t host_len, int *port, char *unix_path, size_t path_len) {
+ if (!url) return -1;
+
+ if (strncmp(url, "unix://", 7) == 0) {
+ const char *p = url + 7;
+ size_t path_len_calc = strlen(p);
+ if (path_len_calc >= path_len) path_len_calc = path_len - 1;
+ strncpy(unix_path, p, path_len_calc);
+ unix_path[path_len_calc] = '\0';
+ return 1;
+ }
+
+ if (strncmp(url, "tcp://", 6) != 0) {
+ return -1;
+ }
+
+ const char *p = url + 6;
+ const char *colon = strchr(p, ':');
+ if (!colon) return -1;
+
+ size_t host_len_calc = colon - p;
+ if (host_len_calc >= host_len) host_len_calc = host_len - 1;
+ strncpy(host, p, host_len_calc);
+ host[host_len_calc] = '\0';
+
+ *port = atoi(colon + 1);
+ if (*port <= 0) return -1;
+
+ return 0;
+}
+
+cluster_node_t *cluster_node_create(const char *name, const char *url, int weight, const char *user, const char *secret) {
+ cluster_node_t *node = calloc(1, sizeof(*node));
+ if (!node) return NULL;
+
+ node->name = strdup(name);
+ node->url = strdup(url);
+ node->weight = weight > 0 ? weight : 1;
+ node->user = user ? strdup(user) : NULL;
+ node->secret = secret ? strdup(secret) : NULL;
+ node->fd = -1;
+ node->session_count = 0;
+ node->available = false;
+ node->consecutive_failures = 0;
+ node->last_check = 0;
+ node->next = NULL;
+
+ char host[MAX_HOST_LEN];
+ int port;
+ char unix_path[MAX_PATH_LEN];
+ if (parse_url(url, host, sizeof(host), &port, unix_path, sizeof(unix_path)) < 0) {
+ log_error("cluster: failed to parse URL %s", url);
+ cluster_node_free(node);
+ return NULL;
+ }
+
+ return node;
+}
+
+void cluster_node_free(cluster_node_t *node) {
+ if (!node) return;
+ cluster_node_disconnect(node);
+ free(node->name);
+ free(node->url);
+ free(node->user);
+ free(node->secret);
+ free(node);
+}
+
+int cluster_node_connect(cluster_node_t *node) {
+ if (!node) return -1;
+
+ cluster_node_disconnect(node);
+
+ char host[MAX_HOST_LEN];
+ int port;
+ char unix_path[MAX_PATH_LEN];
+ int url_type = parse_url(node->url, host, sizeof(host), &port, unix_path, sizeof(unix_path));
+ if (url_type < 0) {
+ return -1;
+ }
+
+ int fd;
+ if (url_type == 1) {
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd < 0) return -1;
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, unix_path, sizeof(addr.sun_path) - 1);
+
+ int ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
+ if (ret < 0) {
+ close(fd);
+ return -1;
+ }
+
+ node->fd = fd;
+ return 0;
+ }
+
+ struct sockaddr_in addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(port);
+
+ if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) {
+ struct hostent *he = gethostbyname(host);
+ if (!he) return -1;
+ memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
+ }
+
+ fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd < 0) return -1;
+
+ int flags = fcntl(fd, F_GETFL, 0);
+ fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+
+ int ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
+ if (ret < 0 && errno != EINPROGRESS) {
+ close(fd);
+ return -1;
+ }
+
+ node->fd = fd;
+ return 0;
+}
+
+void cluster_node_disconnect(cluster_node_t *node) {
+ if (!node || node->fd < 0) return;
+ close(node->fd);
+ node->fd = -1;
+}
+
+int cluster_node_ping(cluster_node_t *node) {
+ if (!node || node->fd < 0) {
+ if (node && cluster_node_connect(node) != 0) {
+ return -1;
+ }
+ }
+
+ resp_object *resp = cluster_node_send_command(node, "ping");
+ if (!resp) return -1;
+
+ int ok = (resp->type == RESPT_SIMPLE && resp->u.s && strcmp(resp->u.s, "PONG") == 0);
+ resp_free(resp);
+
+ return ok ? 0 : -1;
+}
+
+int cluster_node_get_session_count(cluster_node_t *node) {
+ if (!node || node->fd < 0) return -1;
+
+ resp_object *resp = cluster_node_send_command(node, "session.count");
+ if (!resp) return -1;
+
+ int count = -1;
+ if (resp->type == RESPT_INT) {
+ count = (int)resp->u.i;
+ }
+ resp_free(resp);
+
+ return count;
+}
+
+resp_object *cluster_node_send_command(cluster_node_t *node, const char *cmd, ...) {
+ if (!node || node->fd < 0 || !cmd) return NULL;
+
+ char *buf = NULL;
+ size_t len = 0;
+
+ va_list args;
+ va_start(args, cmd);
+
+ int argc = 1;
+ const char *arg = cmd;
+ while (va_arg(args, const char *) != NULL) {
+ argc++;
+ }
+ va_end(args);
+
+ resp_object **argv = malloc(sizeof(resp_object *) * argc);
+ if (!argv) return NULL;
+
+ argv[0] = resp_array_init();
+ resp_array_append_bulk(argv[0], cmd);
+
+ va_start(args, cmd);
+ int i = 1;
+ while ((arg = va_arg(args, const char *)) != NULL) {
+ argv[i] = resp_array_init();
+ resp_array_append_bulk(argv[i], arg);
+ i++;
+ }
+ va_end(args);
+
+ resp_object *cmd_arr = resp_array_init();
+ for (i = 0; i < argc; i++) {
+ resp_array_append_obj(cmd_arr, argv[i]);
+ }
+
+ resp_encode_array(argc, (const resp_object *const *)argv, &buf, &len);
+
+ for (i = 0; i < argc; i++) {
+ resp_free(argv[i]);
+ }
+ free(argv);
+ resp_free(cmd_arr);
+
+ if (!buf) return NULL;
+
+ ssize_t written = send(node->fd, buf, len, 0);
+ free(buf);
+
+ if (written != (ssize_t)len) {
+ return NULL;
+ }
+
+ return resp_read(node->fd);
+}
+
+void cluster_node_set_available(cluster_node_t *node, bool available) {
+ if (!node) return;
+ node->available = available;
+ if (!available) {
+ node->consecutive_failures++;
+ } else {
+ node->consecutive_failures = 0;
+ }
+}
+
+bool cluster_node_select_weighted_lowest(cluster_node_t *node, cluster_node_t *best) {
+ if (!node || !node->available) return false;
+
+ if (!best) return true;
+
+ double node_ratio = (double)node->session_count / node->weight;
+ double best_ratio = (double)best->session_count / best->weight;
+
+ if (node_ratio < best_ratio) return true;
+ if (node_ratio > best_ratio) return false;
+
+ if (node->weight > best->weight) return true;
+ if (node->weight < best->weight) return false;
+
+ return false;
+}
diff --git a/src/domain/cluster/node.h b/src/domain/cluster/node.h
@@ -0,0 +1,43 @@
+#ifndef UDPHOLE_CLUSTER_NODE_H
+#define UDPHOLE_CLUSTER_NODE_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "common/resp.h"
+
+typedef struct cluster_node cluster_node_t;
+
+struct cluster_node {
+ char *name;
+ char *url;
+ int weight;
+ char *user;
+ char *secret;
+ int fd;
+ int session_count;
+ bool available;
+ int consecutive_failures;
+ int64_t last_check;
+ cluster_node_t *next;
+};
+
+cluster_node_t *cluster_node_create(const char *name, const char *url, int weight, const char *user, const char *secret);
+
+void cluster_node_free(cluster_node_t *node);
+
+int cluster_node_connect(cluster_node_t *node);
+
+void cluster_node_disconnect(cluster_node_t *node);
+
+resp_object *cluster_node_send_command(cluster_node_t *node, const char *cmd, ...);
+
+int cluster_node_ping(cluster_node_t *node);
+
+int cluster_node_get_session_count(cluster_node_t *node);
+
+void cluster_node_set_available(cluster_node_t *node, bool available);
+
+bool cluster_node_select_weighted_lowest(cluster_node_t *node, cluster_node_t *best);
+
+#endif
diff --git a/src/domain/daemon/session.c b/src/domain/daemon/session.c
@@ -0,0 +1,1026 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <time.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/stat.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include "rxi/log.h"
+#include "domain/protothreads.h"
+#include "domain/scheduler.h"
+#include "domain/config.h"
+#include "common/socket_util.h"
+#include "common/resp.h"
+#include "tidwall/hashmap.h"
+#include "session.h"
+
+#define SESSION_HASH_SIZE 256
+#define BUFFER_SIZE 4096
+#define DEFAULT_IDLE_EXPIRY 60
+
+typedef struct socket {
+ char *socket_id;
+ int *fds;
+ int local_port;
+ int mode;
+ struct sockaddr_storage remote_addr;
+ socklen_t remote_addrlen;
+ int learned_valid;
+ struct sockaddr_storage learned_addr;
+ socklen_t learned_addrlen;
+} socket_t;
+
+typedef struct forward {
+ char *src_socket_id;
+ char *dst_socket_id;
+} forward_t;
+
+typedef struct session {
+ char *session_id;
+ time_t idle_expiry;
+ time_t created;
+ time_t last_activity;
+ socket_t **sockets;
+ size_t sockets_count;
+ forward_t *forwards;
+ size_t forwards_count;
+ int marked_for_deletion;
+ int *ready_fds;
+ int *all_fds;
+ struct pt pt;
+ struct pt_task *task;
+} session_t;
+
+static session_t **sessions = NULL;
+static size_t sessions_count = 0;
+static int running = 0;
+
+static session_t *find_session(const char *session_id) {
+ for (size_t i = 0; i < sessions_count; i++) {
+ if (strcmp(sessions[i]->session_id, session_id) == 0) {
+ return sessions[i];
+ }
+ }
+ return NULL;
+}
+
+static uint64_t socket_hash(const void *item, uint64_t seed0, uint64_t seed1) {
+ const socket_t *s = item;
+ return hashmap_sip(s->socket_id, strlen(s->socket_id), seed0, seed1);
+}
+
+static int socket_compare(const void *a, const void *b, void *udata) {
+ (void)udata;
+ const socket_t *sa = a;
+ const socket_t *sb = b;
+ return strcmp(sa->socket_id, sb->socket_id);
+}
+
+static socket_t *find_socket(session_t *s, const char *socket_id) {
+ if (!s || !s->sockets || !socket_id) return NULL;
+ for (size_t i = 0; i < s->sockets_count; i++) {
+ if (s->sockets[i] && strcmp(s->sockets[i]->socket_id, socket_id) == 0) {
+ return s->sockets[i];
+ }
+ }
+ return NULL;
+}
+
+static int alloc_port(void) {
+ if (!domain_cfg) return 0;
+ for (int i = 0; i < domain_cfg->port_high - domain_cfg->port_low; i++) {
+ int port = domain_cfg->port_cur + i;
+ if (port > domain_cfg->port_high) port = domain_cfg->port_low;
+ domain_cfg->port_cur = port + 1;
+ if (domain_cfg->port_cur > domain_cfg->port_high) domain_cfg->port_cur = domain_cfg->port_low;
+
+ struct sockaddr_in addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = INADDR_ANY;
+ addr.sin_port = htons(port);
+
+ int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (udp_fd < 0) continue;
+ int ok = (bind(udp_fd, (struct sockaddr *)&addr, sizeof(addr)) == 0);
+ close(udp_fd);
+ if (!ok) continue;
+
+ return port;
+ }
+ return 0;
+}
+
+static int parse_ip_addr(const char *ip_str, int port, struct sockaddr_storage *addr, socklen_t *addrlen) {
+ memset(addr, 0, sizeof(*addr));
+
+ struct sockaddr_in *addr4 = (struct sockaddr_in *)addr;
+ if (inet_pton(AF_INET, ip_str, &addr4->sin_addr) == 1) {
+ addr4->sin_family = AF_INET;
+ addr4->sin_port = htons(port);
+ *addrlen = sizeof(*addr4);
+ return 0;
+ }
+
+ struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)addr;
+ if (inet_pton(AF_INET6, ip_str, &addr6->sin6_addr) == 1) {
+ addr6->sin6_family = AF_INET6;
+ addr6->sin6_port = htons(port);
+ *addrlen = sizeof(*addr6);
+ return 0;
+ }
+
+ return -1;
+}
+
+static void close_socket(socket_t *sock) {
+ if (!sock || !sock->fds) return;
+ for (int i = 1; i <= sock->fds[0]; i++) {
+ if (sock->fds[i] >= 0) {
+ close(sock->fds[i]);
+ }
+ }
+ free(sock->fds);
+ sock->fds = NULL;
+}
+
+static void free_socket(socket_t *sock) {
+ if (!sock) return;
+ close_socket(sock);
+ free(sock->socket_id);
+ free(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) {
+ for (size_t j = i; j < sessions_count - 1; j++) {
+ sessions[j] = sessions[j + 1];
+ }
+ sessions_count--;
+ break;
+ }
+ }
+}
+
+static session_t *create_session(const char *session_id, int idle_expiry) {
+ const session_t *cs = find_session(session_id);
+ if (cs) return (session_t *)cs;
+
+ session_t *s = calloc(1, sizeof(*s));
+ if (!s) return NULL;
+
+ s->session_id = strdup(session_id);
+ s->created = time(NULL);
+ s->last_activity = s->created;
+ s->idle_expiry = idle_expiry > 0 ? idle_expiry : DEFAULT_IDLE_EXPIRY;
+
+ sessions = realloc(sessions, sizeof(session_t *) * (sessions_count + 1));
+ sessions[sessions_count++] = s;
+
+ return s;
+}
+
+static void cleanup_expired_sessions(void) {
+ if (!sessions) return;
+ time_t now = time(NULL);
+
+ for (size_t i = 0; i < sessions_count; i++) {
+ session_t *s = sessions[i];
+ if (!s) continue;
+ if (now - s->last_activity > s->idle_expiry) {
+ log_debug("udphole: session %s expired (idle %ld > expiry %ld)",
+ s->session_id, (long)(now - s->last_activity), (long)s->idle_expiry);
+ destroy_session(s);
+ }
+ }
+}
+
+static int add_forward(session_t *s, const char *src_id, const char *dst_id) {
+ for (size_t i = 0; i < s->forwards_count; i++) {
+ if (strcmp(s->forwards[i].src_socket_id, src_id) == 0 &&
+ strcmp(s->forwards[i].dst_socket_id, dst_id) == 0) {
+ return 0;
+ }
+ }
+
+ forward_t *new_forwards = realloc(s->forwards, sizeof(forward_t) * (s->forwards_count + 1));
+ if (!new_forwards) return -1;
+ s->forwards = new_forwards;
+
+ s->forwards[s->forwards_count].src_socket_id = strdup(src_id);
+ s->forwards[s->forwards_count].dst_socket_id = strdup(dst_id);
+ s->forwards_count++;
+
+ return 0;
+}
+
+static int remove_forward(session_t *s, const char *src_id, const char *dst_id) {
+ for (size_t i = 0; i < s->forwards_count; i++) {
+ if (strcmp(s->forwards[i].src_socket_id, src_id) == 0 &&
+ strcmp(s->forwards[i].dst_socket_id, dst_id) == 0) {
+ free(s->forwards[i].src_socket_id);
+ free(s->forwards[i].dst_socket_id);
+ for (size_t j = i; j < s->forwards_count - 1; j++) {
+ s->forwards[j] = s->forwards[j + 1];
+ }
+ s->forwards_count--;
+ return 0;
+ }
+ }
+ return -1;
+}
+
+static socket_t *create_listen_socket(session_t *sess, const char *socket_id) {
+ socket_t *existing = find_socket(sess, socket_id);
+ if (existing) return existing;
+
+ int port = alloc_port();
+ if (!port) {
+ log_error("udphole: no ports available");
+ return NULL;
+ }
+
+ char port_str[16];
+ snprintf(port_str, sizeof(port_str), "%d", port);
+ int *fds = udp_recv(port_str, NULL, NULL);
+ if (!fds || fds[0] == 0) {
+ log_error("udphole: failed to create UDP socket on port %d", port);
+ free(fds);
+ return NULL;
+ }
+
+ socket_t *sock = calloc(1, sizeof(*sock));
+ if (!sock) {
+ free(fds);
+ return NULL;
+ }
+
+ sock->socket_id = strdup(socket_id);
+ sock->fds = fds;
+ sock->local_port = port;
+ sock->mode = 0;
+ sock->learned_valid = 0;
+
+ sess->sockets = realloc(sess->sockets, sizeof(socket_t *) * (sess->sockets_count + 1));
+ sess->sockets[sess->sockets_count++] = sock;
+
+ log_debug("udphole: created listen socket %s in session %s on port %d",
+ socket_id, sess->session_id, port);
+ return sock;
+}
+
+static socket_t *create_connect_socket(session_t *sess, const char *socket_id,
+ const char *ip, int port) {
+ socket_t *existing = find_socket(sess, socket_id);
+ if (existing) return existing;
+
+ int local_port = alloc_port();
+ if (!local_port) {
+ log_error("udphole: no ports available");
+ return NULL;
+ }
+
+ char port_str[16];
+ snprintf(port_str, sizeof(port_str), "%d", local_port);
+ int *fds = udp_recv(port_str, NULL, NULL);
+ if (!fds || fds[0] == 0) {
+ log_error("udphole: failed to create UDP socket on port %d", local_port);
+ free(fds);
+ return NULL;
+ }
+
+ struct sockaddr_storage remote_addr;
+ socklen_t remote_addrlen;
+ if (parse_ip_addr(ip, port, &remote_addr, &remote_addrlen) != 0) {
+ log_error("udphole: invalid remote address %s:%d", ip, port);
+ free(fds);
+ return NULL;
+ }
+
+ socket_t *sock = calloc(1, sizeof(*sock));
+ if (!sock) {
+ free(fds);
+ return NULL;
+ }
+
+ sock->socket_id = strdup(socket_id);
+ sock->fds = fds;
+ sock->local_port = local_port;
+ sock->mode = 1;
+ sock->remote_addr = remote_addr;
+ sock->remote_addrlen = remote_addrlen;
+ sock->learned_valid = 0;
+
+ sess->sockets = realloc(sess->sockets, sizeof(socket_t *) * (sess->sockets_count + 1));
+ sess->sockets[sess->sockets_count++] = sock;
+
+ log_debug("udphole: created connect socket %s in session %s on port %d -> %s:%d",
+ socket_id, sess->session_id, local_port, ip, port);
+ return sock;
+}
+
+static int destroy_socket(session_t *sess, const char *socket_id) {
+ socket_t *sock = find_socket(sess, socket_id);
+ if (!sock) return -1;
+
+ for (size_t i = 0; i < sess->sockets_count; i++) {
+ if (sess->sockets[i] == sock) {
+ sess->sockets[i] = NULL;
+ break;
+ }
+ }
+ free_socket(sock);
+
+ for (size_t i = 0; i < sess->forwards_count; ) {
+ if (strcmp(sess->forwards[i].src_socket_id, socket_id) == 0 ||
+ strcmp(sess->forwards[i].dst_socket_id, socket_id) == 0) {
+ free(sess->forwards[i].src_socket_id);
+ free(sess->forwards[i].dst_socket_id);
+ for (size_t j = i; j < sess->forwards_count - 1; j++) {
+ sess->forwards[j] = sess->forwards[j + 1];
+ }
+ sess->forwards_count--;
+ } else {
+ i++;
+ }
+ }
+
+ return 0;
+}
+
+static socket_t *find_socket_by_fd(session_t *s, int fd) {
+ if (!s || !s->sockets) return NULL;
+ for (size_t j = 0; j < s->sockets_count; j++) {
+ socket_t *sock = s->sockets[j];
+ if (!sock || !sock->fds) continue;
+ for (int i = 1; i <= sock->fds[0]; i++) {
+ if (sock->fds[i] == fd) {
+ return sock;
+ }
+ }
+ }
+ return NULL;
+}
+
+PT_THREAD(session_pt(struct pt *pt, int64_t timestamp, struct pt_task *task)) {
+ session_t *s = task->udata;
+
+ (void)timestamp;
+ PT_BEGIN(pt);
+
+ char buffer[BUFFER_SIZE];
+
+ for (;;) {
+ if (s->marked_for_deletion) {
+ break;
+ }
+
+ if (!s->sockets || s->sockets_count == 0) {
+ PT_YIELD(pt);
+ continue;
+ }
+
+ s->all_fds = realloc(s->all_fds, sizeof(int) * (s->sockets_count * 2 + 1));
+ if (!s->all_fds) {
+ PT_YIELD(pt);
+ continue;
+ }
+ s->all_fds[0] = 0;
+
+ for (size_t j = 0; j < s->sockets_count; j++) {
+ socket_t *sock = s->sockets[j];
+ if (!sock || !sock->fds) continue;
+ for (int i = 1; i <= sock->fds[0]; i++) {
+ s->all_fds[++s->all_fds[0]] = sock->fds[i];
+ }
+ }
+
+ if (s->all_fds[0] == 0) {
+ PT_YIELD(pt);
+ continue;
+ }
+
+ PT_WAIT_UNTIL(pt, domain_schedmod_has_data(s->all_fds, &s->ready_fds) > 0);
+
+ if (!s->ready_fds || s->ready_fds[0] == 0) {
+ PT_YIELD(pt);
+ continue;
+ }
+
+ for (int r = 1; r <= s->ready_fds[0]; r++) {
+ int ready_fd = s->ready_fds[r];
+
+ socket_t *src_sock = find_socket_by_fd(s, ready_fd);
+ if (!src_sock) continue;
+
+ struct sockaddr_storage from_addr;
+ socklen_t from_len = sizeof(from_addr);
+ ssize_t n = recvfrom(ready_fd, buffer, sizeof(buffer) - 1, 0,
+ (struct sockaddr *)&from_addr, &from_len);
+
+ if (n <= 0) {
+ if (errno != EAGAIN && errno != EWOULDBLOCK) {
+ log_warn("udphole: recvfrom error on socket %s: %s",
+ src_sock->socket_id, strerror(errno));
+ }
+ continue;
+ }
+
+ s->last_activity = time(NULL);
+
+ if (src_sock->mode == 0 && !src_sock->learned_valid) {
+ src_sock->learned_addr = from_addr;
+ src_sock->learned_addrlen = from_len;
+ src_sock->learned_valid = 1;
+ log_debug("udphole: socket %s learned remote address", src_sock->socket_id);
+ }
+
+ for (size_t i = 0; i < s->forwards_count; i++) {
+ if (strcmp(s->forwards[i].src_socket_id, src_sock->socket_id) != 0) {
+ continue;
+ }
+
+ socket_t *dst_sock = find_socket(s, s->forwards[i].dst_socket_id);
+ if (!dst_sock || !dst_sock->fds || dst_sock->fds[0] == 0) continue;
+
+ struct sockaddr *dest_addr = NULL;
+ socklen_t dest_addrlen = 0;
+
+ if (dst_sock->mode == 1) {
+ dest_addr = (struct sockaddr *)&dst_sock->remote_addr;
+ dest_addrlen = dst_sock->remote_addrlen;
+ } else if (dst_sock->learned_valid) {
+ dest_addr = (struct sockaddr *)&dst_sock->learned_addr;
+ dest_addrlen = dst_sock->learned_addrlen;
+ }
+
+ if (dest_addr && dest_addrlen > 0) {
+ int dst_fd = dst_sock->fds[1];
+ ssize_t sent = sendto(dst_fd, buffer, n, 0, dest_addr, dest_addrlen);
+ if (sent < 0) {
+ log_warn("udphole: forward failed %s -> %s: %s",
+ src_sock->socket_id, dst_sock->socket_id, strerror(errno));
+ }
+ }
+ }
+ }
+
+ }
+
+ log_debug("udphole: session %s protothread exiting", s->session_id);
+
+ if (s->all_fds) {
+ free(s->all_fds);
+ s->all_fds = NULL;
+ }
+ if (s->ready_fds) {
+ free(s->ready_fds);
+ s->ready_fds = NULL;
+ }
+
+ PT_END(pt);
+}
+
+static void spawn_session_pt(session_t *s) {
+ s->task = (struct pt_task *)(intptr_t)domain_schedmod_pt_create(session_pt, s);
+}
+
+resp_object *domain_session_create(const char *cmd, resp_object *args) {
+ (void)cmd;
+ if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 2) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.create'");
+ return err;
+ }
+
+ const char *session_id = NULL;
+ if (args->u.arr.n > 1 && args->u.arr.elem[1].type == RESPT_BULK) {
+ session_id = args->u.arr.elem[1].u.s;
+ }
+
+ if (!session_id) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.create'");
+ return err;
+ }
+
+ int idle_expiry = 0;
+ if (args->u.arr.n >= 3 && args->u.arr.elem[2].type == RESPT_BULK && args->u.arr.elem[2].u.s) {
+ idle_expiry = atoi(args->u.arr.elem[2].u.s);
+ }
+
+ session_t *s = create_session(session_id, idle_expiry);
+ if (!s) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR failed to create session");
+ return err;
+ }
+
+ spawn_session_pt(s);
+
+ resp_object *res = resp_array_init();
+ resp_array_append_simple(res, "OK");
+ return res;
+}
+
+resp_object *domain_session_list(const char *cmd, resp_object *args) {
+ (void)cmd;
+ (void)args;
+
+ resp_object *res = resp_array_init();
+ if (!res) return NULL;
+
+ for (size_t i = 0; i < sessions_count; i++) {
+ session_t *s = sessions[i];
+ if (!s) continue;
+ resp_array_append_bulk(res, s->session_id);
+ }
+
+ return res;
+}
+
+resp_object *domain_session_info(const char *cmd, resp_object *args) {
+ (void)cmd;
+ if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 2) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.info'");
+ return err;
+ }
+
+ const char *session_id = NULL;
+ if (args->u.arr.elem[1].type == RESPT_BULK) {
+ session_id = args->u.arr.elem[1].u.s;
+ }
+
+ if (!session_id) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.info'");
+ return err;
+ }
+
+ session_t *s = find_session(session_id);
+ if (!s) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ resp_object *res = resp_array_init();
+ if (!res) return NULL;
+
+ resp_array_append_bulk(res, "session_id");
+ resp_array_append_bulk(res, s->session_id);
+
+ resp_array_append_bulk(res, "created");
+ resp_array_append_int(res, (long long)s->created);
+
+ resp_array_append_bulk(res, "last_activity");
+ resp_array_append_int(res, (long long)s->last_activity);
+
+ resp_array_append_bulk(res, "idle_expiry");
+ resp_array_append_int(res, (long long)s->idle_expiry);
+
+ resp_array_append_bulk(res, "sockets");
+ resp_object *sockets_arr = resp_array_init();
+ for (size_t i = 0; i < s->sockets_count; i++) {
+ socket_t *sock = s->sockets[i];
+ if (!sock) continue;
+ resp_array_append_bulk(sockets_arr, sock->socket_id);
+ }
+ resp_array_append_obj(res, sockets_arr);
+
+ resp_array_append_bulk(res, "forwards");
+ resp_object *forwards_arr = resp_array_init();
+ for (size_t i = 0; i < s->forwards_count; i++) {
+ resp_array_append_bulk(forwards_arr, s->forwards[i].src_socket_id);
+ resp_array_append_bulk(forwards_arr, s->forwards[i].dst_socket_id);
+ }
+ resp_array_append_obj(res, forwards_arr);
+
+ resp_array_append_bulk(res, "marked_for_deletion");
+ resp_array_append_int(res, s->marked_for_deletion);
+
+ return res;
+}
+
+resp_object *domain_session_destroy(const char *cmd, resp_object *args) {
+ (void)cmd;
+ if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 2) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.destroy'");
+ return err;
+ }
+
+ const char *session_id = NULL;
+ if (args->u.arr.elem[1].type == RESPT_BULK) {
+ session_id = args->u.arr.elem[1].u.s;
+ }
+
+ if (!session_id) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.destroy'");
+ return err;
+ }
+
+ session_t *s = find_session(session_id);
+ if (!s) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ destroy_session(s);
+
+ resp_object *res = resp_array_init();
+ resp_array_append_simple(res, "OK");
+ return res;
+}
+
+resp_object *domain_socket_create_listen(const char *cmd, resp_object *args) {
+ (void)cmd;
+ if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 3) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.create.listen'");
+ return err;
+ }
+
+ const char *session_id = NULL;
+ const char *socket_id = NULL;
+
+ if (args->u.arr.elem[1].type == RESPT_BULK) {
+ session_id = args->u.arr.elem[1].u.s;
+ }
+ if (args->u.arr.elem[2].type == RESPT_BULK) {
+ socket_id = args->u.arr.elem[2].u.s;
+ }
+
+ if (!session_id || !socket_id) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.create.listen'");
+ return err;
+ }
+
+ session_t *s = find_session(session_id);
+ if (!s) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ socket_t *sock = create_listen_socket(s, socket_id);
+ if (!sock) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR failed to create socket");
+ return err;
+ }
+
+ resp_object *res = resp_array_init();
+ resp_array_append_int(res, sock->local_port);
+ resp_array_append_bulk(res, domain_cfg && domain_cfg->advertise_addr ? domain_cfg->advertise_addr : "");
+ return res;
+}
+
+resp_object *domain_socket_create_connect(const char *cmd, resp_object *args) {
+ (void)cmd;
+ if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 5) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.create.connect'");
+ return err;
+ }
+
+ const char *session_id = NULL;
+ const char *socket_id = NULL;
+ const char *ip = NULL;
+ const char *port_str = NULL;
+
+ if (args->u.arr.elem[1].type == RESPT_BULK) {
+ session_id = args->u.arr.elem[1].u.s;
+ }
+ if (args->u.arr.elem[2].type == RESPT_BULK) {
+ socket_id = args->u.arr.elem[2].u.s;
+ }
+ if (args->u.arr.elem[3].type == RESPT_BULK) {
+ ip = args->u.arr.elem[3].u.s;
+ }
+ if (args->u.arr.elem[4].type == RESPT_BULK) {
+ port_str = args->u.arr.elem[4].u.s;
+ }
+
+ if (!session_id || !socket_id || !ip || !port_str) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.create.connect'");
+ return err;
+ }
+
+ int port = atoi(port_str);
+
+ session_t *s = find_session(session_id);
+ if (!s) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ socket_t *sock = create_connect_socket(s, socket_id, ip, port);
+ if (!sock) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR failed to create socket");
+ return err;
+ }
+
+ resp_object *res = resp_array_init();
+ resp_array_append_int(res, sock->local_port);
+ resp_array_append_bulk(res, domain_cfg && domain_cfg->advertise_addr ? domain_cfg->advertise_addr : "");
+ return res;
+}
+
+resp_object *domain_socket_destroy(const char *cmd, resp_object *args) {
+ (void)cmd;
+ if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 3) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.destroy'");
+ return err;
+ }
+
+ const char *session_id = NULL;
+ const char *socket_id = NULL;
+
+ if (args->u.arr.elem[1].type == RESPT_BULK) {
+ session_id = args->u.arr.elem[1].u.s;
+ }
+ if (args->u.arr.elem[2].type == RESPT_BULK) {
+ socket_id = args->u.arr.elem[2].u.s;
+ }
+
+ if (!session_id || !socket_id) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.destroy'");
+ return err;
+ }
+
+ session_t *s = find_session(session_id);
+ if (!s) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ if (destroy_socket(s, socket_id) != 0) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR socket not found");
+ return err;
+ }
+
+ resp_object *res = resp_array_init();
+ resp_array_append_simple(res, "OK");
+ return res;
+}
+
+resp_object *domain_forward_list(const char *cmd, resp_object *args) {
+ (void)cmd;
+ if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 2) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.list'");
+ return err;
+ }
+
+ const char *session_id = NULL;
+ if (args->u.arr.elem[1].type == RESPT_BULK) {
+ session_id = args->u.arr.elem[1].u.s;
+ }
+
+ if (!session_id) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.list'");
+ return err;
+ }
+
+ session_t *s = find_session(session_id);
+ if (!s) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ resp_object *res = resp_array_init();
+ for (size_t i = 0; i < s->forwards_count; i++) {
+ resp_array_append_bulk(res, s->forwards[i].src_socket_id);
+ resp_array_append_bulk(res, s->forwards[i].dst_socket_id);
+ }
+ return res;
+}
+
+resp_object *domain_forward_create(const char *cmd, resp_object *args) {
+ (void)cmd;
+ if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 4) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.create'");
+ return err;
+ }
+
+ const char *session_id = NULL;
+ const char *src_socket_id = NULL;
+ const char *dst_socket_id = NULL;
+
+ if (args->u.arr.elem[1].type == RESPT_BULK) {
+ session_id = args->u.arr.elem[1].u.s;
+ }
+ if (args->u.arr.elem[2].type == RESPT_BULK) {
+ src_socket_id = args->u.arr.elem[2].u.s;
+ }
+ if (args->u.arr.elem[3].type == RESPT_BULK) {
+ dst_socket_id = args->u.arr.elem[3].u.s;
+ }
+
+ if (!session_id || !src_socket_id || !dst_socket_id) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.create'");
+ return err;
+ }
+
+ session_t *s = find_session(session_id);
+ if (!s) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ socket_t *src = find_socket(s, src_socket_id);
+ if (!src) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR source socket not found");
+ return err;
+ }
+
+ socket_t *dst = find_socket(s, dst_socket_id);
+ if (!dst) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR destination socket not found");
+ return err;
+ }
+
+ if (add_forward(s, src_socket_id, dst_socket_id) != 0) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR failed to add forward");
+ return err;
+ }
+
+ log_debug("udphole: created forward %s -> %s in session %s", src_socket_id, dst_socket_id, session_id);
+
+ resp_object *res = resp_array_init();
+ resp_array_append_simple(res, "OK");
+ return res;
+}
+
+resp_object *domain_forward_destroy(const char *cmd, resp_object *args) {
+ (void)cmd;
+ if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 4) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.destroy'");
+ return err;
+ }
+
+ const char *session_id = NULL;
+ const char *src_socket_id = NULL;
+ const char *dst_socket_id = NULL;
+
+ if (args->u.arr.elem[1].type == RESPT_BULK) {
+ session_id = args->u.arr.elem[1].u.s;
+ }
+ if (args->u.arr.elem[2].type == RESPT_BULK) {
+ src_socket_id = args->u.arr.elem[2].u.s;
+ }
+ if (args->u.arr.elem[3].type == RESPT_BULK) {
+ dst_socket_id = args->u.arr.elem[3].u.s;
+ }
+
+ if (!session_id || !src_socket_id || !dst_socket_id) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.destroy'");
+ return err;
+ }
+
+ session_t *s = find_session(session_id);
+ if (!s) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ if (remove_forward(s, src_socket_id, dst_socket_id) != 0) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR forward not found");
+ return err;
+ }
+
+ resp_object *res = resp_array_init();
+ resp_array_append_simple(res, "OK");
+ return res;
+}
+
+resp_object *domain_system_load(const char *cmd, resp_object *args) {
+ (void)cmd;
+ (void)args;
+
+ double loadavg[3];
+ if (getloadavg(loadavg, 3) != 3) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR failed to get load average");
+ return err;
+ }
+
+ resp_object *res = resp_array_init();
+ char buf[64];
+
+ resp_array_append_bulk(res, "1min");
+ snprintf(buf, sizeof(buf), "%.2f", loadavg[0]);
+ resp_array_append_bulk(res, buf);
+
+ resp_array_append_bulk(res, "5min");
+ snprintf(buf, sizeof(buf), "%.2f", loadavg[1]);
+ resp_array_append_bulk(res, buf);
+
+ resp_array_append_bulk(res, "15min");
+ snprintf(buf, sizeof(buf), "%.2f", loadavg[2]);
+ resp_array_append_bulk(res, buf);
+
+ return res;
+}
+
+resp_object *domain_session_count(const char *cmd, resp_object *args) {
+ (void)cmd;
+ (void)args;
+
+ size_t count = 0;
+ for (size_t i = 0; i < sessions_count; i++) {
+ if (sessions[i] != NULL) {
+ count++;
+ }
+ }
+
+ resp_object *res = malloc(sizeof(resp_object));
+ if (!res) return NULL;
+ res->type = RESPT_INT;
+ res->u.i = (long long)count;
+ return res;
+}
+
+PT_THREAD(session_manager_pt(struct pt *pt, int64_t timestamp, struct pt_task *task)) {
+ (void)timestamp;
+ log_trace("session_manager: protothread entry");
+ PT_BEGIN(pt);
+
+ PT_WAIT_UNTIL(pt, domain_cfg);
+
+ running = 1;
+ log_info("udphole: manager started with port range %d-%d", domain_cfg->port_low, domain_cfg->port_high);
+
+ int64_t last_cleanup = 0;
+
+ for (;;) {
+ int64_t now = (int64_t)(time(NULL));
+ if (now - last_cleanup >= 1) {
+ cleanup_expired_sessions();
+ last_cleanup = now;
+ }
+
+ PT_YIELD(pt);
+ }
+
+ running = 0;
+
+ for (size_t i = 0; i < sessions_count; i++) {
+ if (sessions[i]) {
+ sessions[i]->marked_for_deletion = 1;
+ }
+ }
+
+ PT_END(pt);
+}
+\ No newline at end of file
diff --git a/src/domain/daemon/session.h b/src/domain/daemon/session.h
@@ -0,0 +1,24 @@
+#ifndef UDPHOLE_SESSION_H
+#define UDPHOLE_SESSION_H
+
+#include <stdint.h>
+
+#include "domain/scheduler.h"
+#include "common/resp.h"
+
+PT_THREAD(session_manager_pt(struct pt *pt, int64_t timestamp, struct pt_task *task));
+
+resp_object *domain_session_create(const char *cmd, resp_object *args);
+resp_object *domain_session_list(const char *cmd, resp_object *args);
+resp_object *domain_session_info(const char *cmd, resp_object *args);
+resp_object *domain_session_destroy(const char *cmd, resp_object *args);
+resp_object *domain_socket_create_listen(const char *cmd, resp_object *args);
+resp_object *domain_socket_create_connect(const char *cmd, resp_object *args);
+resp_object *domain_socket_destroy(const char *cmd, resp_object *args);
+resp_object *domain_forward_list(const char *cmd, resp_object *args);
+resp_object *domain_forward_create(const char *cmd, resp_object *args);
+resp_object *domain_forward_destroy(const char *cmd, resp_object *args);
+resp_object *domain_session_count(const char *cmd, resp_object *args);
+resp_object *domain_system_load(const char *cmd, resp_object *args);
+
+#endif
diff --git a/src/domain/session.c b/src/domain/session.c
@@ -1,1014 +0,0 @@
-#include <stdlib.h>
-#include <string.h>
-#include <stdio.h>
-#include <stdarg.h>
-#include <unistd.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <time.h>
-#include <sys/socket.h>
-#include <sys/un.h>
-#include <sys/stat.h>
-#include <netinet/in.h>
-#include <arpa/inet.h>
-
-#include "rxi/log.h"
-#include "domain/protothreads.h"
-#include "domain/scheduler.h"
-#include "domain/config.h"
-#include "common/socket_util.h"
-#include "common/resp.h"
-#include "tidwall/hashmap.h"
-#include "session.h"
-
-#define SESSION_HASH_SIZE 256
-#define BUFFER_SIZE 4096
-#define DEFAULT_IDLE_EXPIRY 60
-
-typedef struct socket {
- char *socket_id;
- int *fds;
- int local_port;
- int mode;
- struct sockaddr_storage remote_addr;
- socklen_t remote_addrlen;
- int learned_valid;
- struct sockaddr_storage learned_addr;
- socklen_t learned_addrlen;
-} socket_t;
-
-typedef struct forward {
- char *src_socket_id;
- char *dst_socket_id;
-} forward_t;
-
-typedef struct session {
- char *session_id;
- time_t idle_expiry;
- time_t created;
- time_t last_activity;
- socket_t **sockets;
- size_t sockets_count;
- forward_t *forwards;
- size_t forwards_count;
- int marked_for_deletion;
- int *ready_fds;
- int *all_fds;
- struct pt pt;
- struct pt_task *task;
-} session_t;
-
-static session_t **sessions = NULL;
-static size_t sessions_count = 0;
-static int running = 0;
-
-static session_t *find_session(const char *session_id) {
- for (size_t i = 0; i < sessions_count; i++) {
- if (strcmp(sessions[i]->session_id, session_id) == 0) {
- return sessions[i];
- }
- }
- return NULL;
-}
-
-static uint64_t socket_hash(const void *item, uint64_t seed0, uint64_t seed1) {
- const socket_t *s = item;
- return hashmap_sip(s->socket_id, strlen(s->socket_id), seed0, seed1);
-}
-
-static int socket_compare(const void *a, const void *b, void *udata) {
- (void)udata;
- const socket_t *sa = a;
- const socket_t *sb = b;
- return strcmp(sa->socket_id, sb->socket_id);
-}
-
-static socket_t *find_socket(session_t *s, const char *socket_id) {
- if (!s || !s->sockets || !socket_id) return NULL;
- for (size_t i = 0; i < s->sockets_count; i++) {
- if (s->sockets[i] && strcmp(s->sockets[i]->socket_id, socket_id) == 0) {
- return s->sockets[i];
- }
- }
- return NULL;
-}
-
-static int alloc_port(void) {
- if (!domain_cfg) return 0;
- for (int i = 0; i < domain_cfg->port_high - domain_cfg->port_low; i++) {
- int port = domain_cfg->port_cur + i;
- if (port > domain_cfg->port_high) port = domain_cfg->port_low;
- domain_cfg->port_cur = port + 1;
- if (domain_cfg->port_cur > domain_cfg->port_high) domain_cfg->port_cur = domain_cfg->port_low;
-
- struct sockaddr_in addr;
- memset(&addr, 0, sizeof(addr));
- addr.sin_family = AF_INET;
- addr.sin_addr.s_addr = INADDR_ANY;
- addr.sin_port = htons(port);
-
- int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
- if (udp_fd < 0) continue;
- int ok = (bind(udp_fd, (struct sockaddr *)&addr, sizeof(addr)) == 0);
- close(udp_fd);
- if (!ok) continue;
-
- return port;
- }
- return 0;
-}
-
-static int parse_ip_addr(const char *ip_str, int port, struct sockaddr_storage *addr, socklen_t *addrlen) {
- memset(addr, 0, sizeof(*addr));
-
- struct sockaddr_in *addr4 = (struct sockaddr_in *)addr;
- if (inet_pton(AF_INET, ip_str, &addr4->sin_addr) == 1) {
- addr4->sin_family = AF_INET;
- addr4->sin_port = htons(port);
- *addrlen = sizeof(*addr4);
- return 0;
- }
-
- struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)addr;
- if (inet_pton(AF_INET6, ip_str, &addr6->sin6_addr) == 1) {
- addr6->sin6_family = AF_INET6;
- addr6->sin6_port = htons(port);
- *addrlen = sizeof(*addr6);
- return 0;
- }
-
- return -1;
-}
-
-static void close_socket(socket_t *sock) {
- if (!sock || !sock->fds) return;
- for (int i = 1; i <= sock->fds[0]; i++) {
- if (sock->fds[i] >= 0) {
- close(sock->fds[i]);
- }
- }
- free(sock->fds);
- sock->fds = NULL;
-}
-
-static void free_socket(socket_t *sock) {
- if (!sock) return;
- close_socket(sock);
- free(sock->socket_id);
- free(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) {
- for (size_t j = i; j < sessions_count - 1; j++) {
- sessions[j] = sessions[j + 1];
- }
- sessions_count--;
- break;
- }
- }
-}
-
-static session_t *create_session(const char *session_id, int idle_expiry) {
- const session_t *cs = find_session(session_id);
- if (cs) return (session_t *)cs;
-
- session_t *s = calloc(1, sizeof(*s));
- if (!s) return NULL;
-
- s->session_id = strdup(session_id);
- s->created = time(NULL);
- s->last_activity = s->created;
- s->idle_expiry = idle_expiry > 0 ? idle_expiry : DEFAULT_IDLE_EXPIRY;
-
- sessions = realloc(sessions, sizeof(session_t *) * (sessions_count + 1));
- sessions[sessions_count++] = s;
-
- return s;
-}
-
-static void cleanup_expired_sessions(void) {
- if (!sessions) return;
- time_t now = time(NULL);
-
- for (size_t i = 0; i < sessions_count; i++) {
- session_t *s = sessions[i];
- if (!s) continue;
- if (now - s->last_activity > s->idle_expiry) {
- log_debug("udphole: session %s expired (idle %ld > expiry %ld)",
- s->session_id, (long)(now - s->last_activity), (long)s->idle_expiry);
- destroy_session(s);
- }
- }
-}
-
-static int add_forward(session_t *s, const char *src_id, const char *dst_id) {
- for (size_t i = 0; i < s->forwards_count; i++) {
- if (strcmp(s->forwards[i].src_socket_id, src_id) == 0 &&
- strcmp(s->forwards[i].dst_socket_id, dst_id) == 0) {
- return 0;
- }
- }
-
- forward_t *new_forwards = realloc(s->forwards, sizeof(forward_t) * (s->forwards_count + 1));
- if (!new_forwards) return -1;
- s->forwards = new_forwards;
-
- s->forwards[s->forwards_count].src_socket_id = strdup(src_id);
- s->forwards[s->forwards_count].dst_socket_id = strdup(dst_id);
- s->forwards_count++;
-
- return 0;
-}
-
-static int remove_forward(session_t *s, const char *src_id, const char *dst_id) {
- for (size_t i = 0; i < s->forwards_count; i++) {
- if (strcmp(s->forwards[i].src_socket_id, src_id) == 0 &&
- strcmp(s->forwards[i].dst_socket_id, dst_id) == 0) {
- free(s->forwards[i].src_socket_id);
- free(s->forwards[i].dst_socket_id);
- for (size_t j = i; j < s->forwards_count - 1; j++) {
- s->forwards[j] = s->forwards[j + 1];
- }
- s->forwards_count--;
- return 0;
- }
- }
- return -1;
-}
-
-static socket_t *create_listen_socket(session_t *sess, const char *socket_id) {
- socket_t *existing = find_socket(sess, socket_id);
- if (existing) return existing;
-
- int port = alloc_port();
- if (!port) {
- log_error("udphole: no ports available");
- return NULL;
- }
-
- char port_str[16];
- snprintf(port_str, sizeof(port_str), "%d", port);
- int *fds = udp_recv(port_str, NULL, NULL);
- if (!fds || fds[0] == 0) {
- log_error("udphole: failed to create UDP socket on port %d", port);
- free(fds);
- return NULL;
- }
-
- socket_t *sock = calloc(1, sizeof(*sock));
- if (!sock) {
- free(fds);
- return NULL;
- }
-
- sock->socket_id = strdup(socket_id);
- sock->fds = fds;
- sock->local_port = port;
- sock->mode = 0;
- sock->learned_valid = 0;
-
- sess->sockets = realloc(sess->sockets, sizeof(socket_t *) * (sess->sockets_count + 1));
- sess->sockets[sess->sockets_count++] = sock;
-
- log_debug("udphole: created listen socket %s in session %s on port %d",
- socket_id, sess->session_id, port);
- return sock;
-}
-
-static socket_t *create_connect_socket(session_t *sess, const char *socket_id,
- const char *ip, int port) {
- socket_t *existing = find_socket(sess, socket_id);
- if (existing) return existing;
-
- int local_port = alloc_port();
- if (!local_port) {
- log_error("udphole: no ports available");
- return NULL;
- }
-
- char port_str[16];
- snprintf(port_str, sizeof(port_str), "%d", local_port);
- int *fds = udp_recv(port_str, NULL, NULL);
- if (!fds || fds[0] == 0) {
- log_error("udphole: failed to create UDP socket on port %d", local_port);
- free(fds);
- return NULL;
- }
-
- struct sockaddr_storage remote_addr;
- socklen_t remote_addrlen;
- if (parse_ip_addr(ip, port, &remote_addr, &remote_addrlen) != 0) {
- log_error("udphole: invalid remote address %s:%d", ip, port);
- free(fds);
- return NULL;
- }
-
- socket_t *sock = calloc(1, sizeof(*sock));
- if (!sock) {
- free(fds);
- return NULL;
- }
-
- sock->socket_id = strdup(socket_id);
- sock->fds = fds;
- sock->local_port = local_port;
- sock->mode = 1;
- sock->remote_addr = remote_addr;
- sock->remote_addrlen = remote_addrlen;
- sock->learned_valid = 0;
-
- sess->sockets = realloc(sess->sockets, sizeof(socket_t *) * (sess->sockets_count + 1));
- sess->sockets[sess->sockets_count++] = sock;
-
- log_debug("udphole: created connect socket %s in session %s on port %d -> %s:%d",
- socket_id, sess->session_id, local_port, ip, port);
- return sock;
-}
-
-static int destroy_socket(session_t *sess, const char *socket_id) {
- socket_t *sock = find_socket(sess, socket_id);
- if (!sock) return -1;
-
- for (size_t i = 0; i < sess->sockets_count; i++) {
- if (sess->sockets[i] == sock) {
- sess->sockets[i] = NULL;
- break;
- }
- }
- free_socket(sock);
-
- for (size_t i = 0; i < sess->forwards_count; ) {
- if (strcmp(sess->forwards[i].src_socket_id, socket_id) == 0 ||
- strcmp(sess->forwards[i].dst_socket_id, socket_id) == 0) {
- free(sess->forwards[i].src_socket_id);
- free(sess->forwards[i].dst_socket_id);
- for (size_t j = i; j < sess->forwards_count - 1; j++) {
- sess->forwards[j] = sess->forwards[j + 1];
- }
- sess->forwards_count--;
- } else {
- i++;
- }
- }
-
- return 0;
-}
-
-static socket_t *find_socket_by_fd(session_t *s, int fd) {
- if (!s || !s->sockets) return NULL;
- for (size_t j = 0; j < s->sockets_count; j++) {
- socket_t *sock = s->sockets[j];
- if (!sock || !sock->fds) continue;
- for (int i = 1; i <= sock->fds[0]; i++) {
- if (sock->fds[i] == fd) {
- return sock;
- }
- }
- }
- return NULL;
-}
-
-PT_THREAD(session_pt(struct pt *pt, int64_t timestamp, struct pt_task *task)) {
- session_t *s = task->udata;
-
- (void)timestamp;
- PT_BEGIN(pt);
-
- char buffer[BUFFER_SIZE];
-
- for (;;) {
- if (s->marked_for_deletion) {
- break;
- }
-
- if (!s->sockets || s->sockets_count == 0) {
- PT_YIELD(pt);
- continue;
- }
-
- s->all_fds = realloc(s->all_fds, sizeof(int) * (s->sockets_count * 2 + 1));
- if (!s->all_fds) {
- PT_YIELD(pt);
- continue;
- }
- s->all_fds[0] = 0;
-
- for (size_t j = 0; j < s->sockets_count; j++) {
- socket_t *sock = s->sockets[j];
- if (!sock || !sock->fds) continue;
- for (int i = 1; i <= sock->fds[0]; i++) {
- s->all_fds[++s->all_fds[0]] = sock->fds[i];
- }
- }
-
- if (s->all_fds[0] == 0) {
- PT_YIELD(pt);
- continue;
- }
-
- PT_WAIT_UNTIL(pt, domain_schedmod_has_data(s->all_fds, &s->ready_fds) > 0);
-
- if (!s->ready_fds || s->ready_fds[0] == 0) {
- PT_YIELD(pt);
- continue;
- }
-
- for (int r = 1; r <= s->ready_fds[0]; r++) {
- int ready_fd = s->ready_fds[r];
-
- socket_t *src_sock = find_socket_by_fd(s, ready_fd);
- if (!src_sock) continue;
-
- struct sockaddr_storage from_addr;
- socklen_t from_len = sizeof(from_addr);
- ssize_t n = recvfrom(ready_fd, buffer, sizeof(buffer) - 1, 0,
- (struct sockaddr *)&from_addr, &from_len);
-
- if (n <= 0) {
- if (errno != EAGAIN && errno != EWOULDBLOCK) {
- log_warn("udphole: recvfrom error on socket %s: %s",
- src_sock->socket_id, strerror(errno));
- }
- continue;
- }
-
- s->last_activity = time(NULL);
-
- if (src_sock->mode == 0 && !src_sock->learned_valid) {
- src_sock->learned_addr = from_addr;
- src_sock->learned_addrlen = from_len;
- src_sock->learned_valid = 1;
- log_debug("udphole: socket %s learned remote address", src_sock->socket_id);
- }
-
- for (size_t i = 0; i < s->forwards_count; i++) {
- if (strcmp(s->forwards[i].src_socket_id, src_sock->socket_id) != 0) {
- continue;
- }
-
- socket_t *dst_sock = find_socket(s, s->forwards[i].dst_socket_id);
- if (!dst_sock || !dst_sock->fds || dst_sock->fds[0] == 0) continue;
-
- struct sockaddr *dest_addr = NULL;
- socklen_t dest_addrlen = 0;
-
- if (dst_sock->mode == 1) {
- dest_addr = (struct sockaddr *)&dst_sock->remote_addr;
- dest_addrlen = dst_sock->remote_addrlen;
- } else if (dst_sock->learned_valid) {
- dest_addr = (struct sockaddr *)&dst_sock->learned_addr;
- dest_addrlen = dst_sock->learned_addrlen;
- }
-
- if (dest_addr && dest_addrlen > 0) {
- int dst_fd = dst_sock->fds[1];
- ssize_t sent = sendto(dst_fd, buffer, n, 0, dest_addr, dest_addrlen);
- if (sent < 0) {
- log_warn("udphole: forward failed %s -> %s: %s",
- src_sock->socket_id, dst_sock->socket_id, strerror(errno));
- }
- }
- }
- }
-
- }
-
- log_debug("udphole: session %s protothread exiting", s->session_id);
-
- if (s->all_fds) {
- free(s->all_fds);
- s->all_fds = NULL;
- }
- if (s->ready_fds) {
- free(s->ready_fds);
- s->ready_fds = NULL;
- }
-
- PT_END(pt);
-}
-
-static void spawn_session_pt(session_t *s) {
- s->task = (struct pt_task *)(intptr_t)domain_schedmod_pt_create(session_pt, s);
-}
-
-resp_object *domain_session_create(resp_object *args) {
- if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 2) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.create'");
- return err;
- }
-
- const char *session_id = NULL;
- if (args->u.arr.n > 1 && args->u.arr.elem[1].type == RESPT_BULK) {
- session_id = args->u.arr.elem[1].u.s;
- }
-
- if (!session_id) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.create'");
- return err;
- }
-
- int idle_expiry = 0;
- if (args->u.arr.n >= 3 && args->u.arr.elem[2].type == RESPT_BULK && args->u.arr.elem[2].u.s) {
- idle_expiry = atoi(args->u.arr.elem[2].u.s);
- }
-
- session_t *s = create_session(session_id, idle_expiry);
- if (!s) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR failed to create session");
- return err;
- }
-
- spawn_session_pt(s);
-
- resp_object *res = resp_array_init();
- resp_array_append_simple(res, "OK");
- return res;
-}
-
-resp_object *domain_session_list(resp_object *args) {
- (void)args;
-
- resp_object *res = resp_array_init();
- if (!res) return NULL;
-
- for (size_t i = 0; i < sessions_count; i++) {
- session_t *s = sessions[i];
- if (!s) continue;
- resp_array_append_bulk(res, s->session_id);
- }
-
- return res;
-}
-
-resp_object *domain_session_info(resp_object *args) {
- if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 2) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.info'");
- return err;
- }
-
- const char *session_id = NULL;
- if (args->u.arr.elem[1].type == RESPT_BULK) {
- session_id = args->u.arr.elem[1].u.s;
- }
-
- if (!session_id) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.info'");
- return err;
- }
-
- session_t *s = find_session(session_id);
- if (!s) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR session not found");
- return err;
- }
-
- resp_object *res = resp_array_init();
- if (!res) return NULL;
-
- resp_array_append_bulk(res, "session_id");
- resp_array_append_bulk(res, s->session_id);
-
- resp_array_append_bulk(res, "created");
- resp_array_append_int(res, (long long)s->created);
-
- resp_array_append_bulk(res, "last_activity");
- resp_array_append_int(res, (long long)s->last_activity);
-
- resp_array_append_bulk(res, "idle_expiry");
- resp_array_append_int(res, (long long)s->idle_expiry);
-
- resp_array_append_bulk(res, "sockets");
- resp_object *sockets_arr = resp_array_init();
- for (size_t i = 0; i < s->sockets_count; i++) {
- socket_t *sock = s->sockets[i];
- if (!sock) continue;
- resp_array_append_bulk(sockets_arr, sock->socket_id);
- }
- resp_array_append_obj(res, sockets_arr);
-
- resp_array_append_bulk(res, "forwards");
- resp_object *forwards_arr = resp_array_init();
- for (size_t i = 0; i < s->forwards_count; i++) {
- resp_array_append_bulk(forwards_arr, s->forwards[i].src_socket_id);
- resp_array_append_bulk(forwards_arr, s->forwards[i].dst_socket_id);
- }
- resp_array_append_obj(res, forwards_arr);
-
- resp_array_append_bulk(res, "marked_for_deletion");
- resp_array_append_int(res, s->marked_for_deletion);
-
- return res;
-}
-
-resp_object *domain_session_destroy(resp_object *args) {
- if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 2) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.destroy'");
- return err;
- }
-
- const char *session_id = NULL;
- if (args->u.arr.elem[1].type == RESPT_BULK) {
- session_id = args->u.arr.elem[1].u.s;
- }
-
- if (!session_id) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.destroy'");
- return err;
- }
-
- session_t *s = find_session(session_id);
- if (!s) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR session not found");
- return err;
- }
-
- destroy_session(s);
-
- resp_object *res = resp_array_init();
- resp_array_append_simple(res, "OK");
- return res;
-}
-
-resp_object *domain_socket_create_listen(resp_object *args) {
- if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 3) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.create.listen'");
- return err;
- }
-
- const char *session_id = NULL;
- const char *socket_id = NULL;
-
- if (args->u.arr.elem[1].type == RESPT_BULK) {
- session_id = args->u.arr.elem[1].u.s;
- }
- if (args->u.arr.elem[2].type == RESPT_BULK) {
- socket_id = args->u.arr.elem[2].u.s;
- }
-
- if (!session_id || !socket_id) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.create.listen'");
- return err;
- }
-
- session_t *s = find_session(session_id);
- if (!s) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR session not found");
- return err;
- }
-
- socket_t *sock = create_listen_socket(s, socket_id);
- if (!sock) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR failed to create socket");
- return err;
- }
-
- resp_object *res = resp_array_init();
- resp_array_append_int(res, sock->local_port);
- resp_array_append_bulk(res, domain_cfg && domain_cfg->advertise_addr ? domain_cfg->advertise_addr : "");
- return res;
-}
-
-resp_object *domain_socket_create_connect(resp_object *args) {
- if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 5) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.create.connect'");
- return err;
- }
-
- const char *session_id = NULL;
- const char *socket_id = NULL;
- const char *ip = NULL;
- const char *port_str = NULL;
-
- if (args->u.arr.elem[1].type == RESPT_BULK) {
- session_id = args->u.arr.elem[1].u.s;
- }
- if (args->u.arr.elem[2].type == RESPT_BULK) {
- socket_id = args->u.arr.elem[2].u.s;
- }
- if (args->u.arr.elem[3].type == RESPT_BULK) {
- ip = args->u.arr.elem[3].u.s;
- }
- if (args->u.arr.elem[4].type == RESPT_BULK) {
- port_str = args->u.arr.elem[4].u.s;
- }
-
- if (!session_id || !socket_id || !ip || !port_str) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.create.connect'");
- return err;
- }
-
- int port = atoi(port_str);
-
- session_t *s = find_session(session_id);
- if (!s) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR session not found");
- return err;
- }
-
- socket_t *sock = create_connect_socket(s, socket_id, ip, port);
- if (!sock) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR failed to create socket");
- return err;
- }
-
- resp_object *res = resp_array_init();
- resp_array_append_int(res, sock->local_port);
- resp_array_append_bulk(res, domain_cfg && domain_cfg->advertise_addr ? domain_cfg->advertise_addr : "");
- return res;
-}
-
-resp_object *domain_socket_destroy(resp_object *args) {
- if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 3) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.destroy'");
- return err;
- }
-
- const char *session_id = NULL;
- const char *socket_id = NULL;
-
- if (args->u.arr.elem[1].type == RESPT_BULK) {
- session_id = args->u.arr.elem[1].u.s;
- }
- if (args->u.arr.elem[2].type == RESPT_BULK) {
- socket_id = args->u.arr.elem[2].u.s;
- }
-
- if (!session_id || !socket_id) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.socket.destroy'");
- return err;
- }
-
- session_t *s = find_session(session_id);
- if (!s) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR session not found");
- return err;
- }
-
- if (destroy_socket(s, socket_id) != 0) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR socket not found");
- return err;
- }
-
- resp_object *res = resp_array_init();
- resp_array_append_simple(res, "OK");
- return res;
-}
-
-resp_object *domain_forward_list(resp_object *args) {
- if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 2) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.list'");
- return err;
- }
-
- const char *session_id = NULL;
- if (args->u.arr.elem[1].type == RESPT_BULK) {
- session_id = args->u.arr.elem[1].u.s;
- }
-
- if (!session_id) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.list'");
- return err;
- }
-
- session_t *s = find_session(session_id);
- if (!s) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR session not found");
- return err;
- }
-
- resp_object *res = resp_array_init();
- for (size_t i = 0; i < s->forwards_count; i++) {
- resp_array_append_bulk(res, s->forwards[i].src_socket_id);
- resp_array_append_bulk(res, s->forwards[i].dst_socket_id);
- }
- return res;
-}
-
-resp_object *domain_forward_create(resp_object *args) {
- if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 4) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.create'");
- return err;
- }
-
- const char *session_id = NULL;
- const char *src_socket_id = NULL;
- const char *dst_socket_id = NULL;
-
- if (args->u.arr.elem[1].type == RESPT_BULK) {
- session_id = args->u.arr.elem[1].u.s;
- }
- if (args->u.arr.elem[2].type == RESPT_BULK) {
- src_socket_id = args->u.arr.elem[2].u.s;
- }
- if (args->u.arr.elem[3].type == RESPT_BULK) {
- dst_socket_id = args->u.arr.elem[3].u.s;
- }
-
- if (!session_id || !src_socket_id || !dst_socket_id) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.create'");
- return err;
- }
-
- session_t *s = find_session(session_id);
- if (!s) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR session not found");
- return err;
- }
-
- socket_t *src = find_socket(s, src_socket_id);
- if (!src) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR source socket not found");
- return err;
- }
-
- socket_t *dst = find_socket(s, dst_socket_id);
- if (!dst) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR destination socket not found");
- return err;
- }
-
- if (add_forward(s, src_socket_id, dst_socket_id) != 0) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR failed to add forward");
- return err;
- }
-
- log_debug("udphole: created forward %s -> %s in session %s", src_socket_id, dst_socket_id, session_id);
-
- resp_object *res = resp_array_init();
- resp_array_append_simple(res, "OK");
- return res;
-}
-
-resp_object *domain_forward_destroy(resp_object *args) {
- if (!args || args->type != RESPT_ARRAY || args->u.arr.n < 4) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.destroy'");
- return err;
- }
-
- const char *session_id = NULL;
- const char *src_socket_id = NULL;
- const char *dst_socket_id = NULL;
-
- if (args->u.arr.elem[1].type == RESPT_BULK) {
- session_id = args->u.arr.elem[1].u.s;
- }
- if (args->u.arr.elem[2].type == RESPT_BULK) {
- src_socket_id = args->u.arr.elem[2].u.s;
- }
- if (args->u.arr.elem[3].type == RESPT_BULK) {
- dst_socket_id = args->u.arr.elem[3].u.s;
- }
-
- if (!session_id || !src_socket_id || !dst_socket_id) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR wrong number of arguments for 'session.forward.destroy'");
- return err;
- }
-
- session_t *s = find_session(session_id);
- if (!s) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR session not found");
- return err;
- }
-
- if (remove_forward(s, src_socket_id, dst_socket_id) != 0) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR forward not found");
- return err;
- }
-
- resp_object *res = resp_array_init();
- resp_array_append_simple(res, "OK");
- return res;
-}
-
-resp_object *domain_system_load(resp_object *args) {
- (void)args;
-
- double loadavg[3];
- if (getloadavg(loadavg, 3) != 3) {
- resp_object *err = resp_array_init();
- resp_array_append_simple(err, "ERR failed to get load average");
- return err;
- }
-
- resp_object *res = resp_array_init();
- char buf[64];
-
- resp_array_append_bulk(res, "1min");
- snprintf(buf, sizeof(buf), "%.2f", loadavg[0]);
- resp_array_append_bulk(res, buf);
-
- resp_array_append_bulk(res, "5min");
- snprintf(buf, sizeof(buf), "%.2f", loadavg[1]);
- resp_array_append_bulk(res, buf);
-
- resp_array_append_bulk(res, "15min");
- snprintf(buf, sizeof(buf), "%.2f", loadavg[2]);
- resp_array_append_bulk(res, buf);
-
- return res;
-}
-
-resp_object *domain_session_count(resp_object *args) {
- (void)args;
-
- size_t count = 0;
- for (size_t i = 0; i < sessions_count; i++) {
- if (sessions[i] != NULL) {
- count++;
- }
- }
-
- resp_object *res = malloc(sizeof(resp_object));
- if (!res) return NULL;
- res->type = RESPT_INT;
- res->u.i = (long long)count;
- return res;
-}
-
-PT_THREAD(session_manager_pt(struct pt *pt, int64_t timestamp, struct pt_task *task)) {
- (void)timestamp;
- log_trace("session_manager: protothread entry");
- PT_BEGIN(pt);
-
- PT_WAIT_UNTIL(pt, domain_cfg);
-
- running = 1;
- log_info("udphole: manager started with port range %d-%d", domain_cfg->port_low, domain_cfg->port_high);
-
- int64_t last_cleanup = 0;
-
- for (;;) {
- int64_t now = (int64_t)(time(NULL));
- if (now - last_cleanup >= 1) {
- cleanup_expired_sessions();
- last_cleanup = now;
- }
-
- PT_YIELD(pt);
- }
-
- running = 0;
-
- for (size_t i = 0; i < sessions_count; i++) {
- if (sessions[i]) {
- sessions[i]->marked_for_deletion = 1;
- }
- }
-
- PT_END(pt);
-}
-\ No newline at end of file
diff --git a/src/domain/session.h b/src/domain/session.h
@@ -1,24 +0,0 @@
-#ifndef UDPHOLE_SESSION_H
-#define UDPHOLE_SESSION_H
-
-#include <stdint.h>
-
-#include "domain/scheduler.h"
-#include "common/resp.h"
-
-PT_THREAD(session_manager_pt(struct pt *pt, int64_t timestamp, struct pt_task *task));
-
-resp_object *domain_session_create(resp_object *args);
-resp_object *domain_session_list(resp_object *args);
-resp_object *domain_session_info(resp_object *args);
-resp_object *domain_session_destroy(resp_object *args);
-resp_object *domain_socket_create_listen(resp_object *args);
-resp_object *domain_socket_create_connect(resp_object *args);
-resp_object *domain_socket_destroy(resp_object *args);
-resp_object *domain_forward_list(resp_object *args);
-resp_object *domain_forward_create(resp_object *args);
-resp_object *domain_forward_destroy(resp_object *args);
-resp_object *domain_session_count(resp_object *args);
-resp_object *domain_system_load(resp_object *args);
-
-#endif
diff --git a/src/interface/api/server.c b/src/interface/api/server.c
@@ -462,7 +462,7 @@ static void dispatch_command(api_client_t *c, char **args, int nargs) {
resp_array_append_bulk(domain_args, args[i]);
}
- resp_object *result = dcmd->func(domain_args);
+ resp_object *result = dcmd->func(args[0], domain_args);
resp_free(domain_args);
if (!result) {
diff --git a/src/interface/api/server.h b/src/interface/api/server.h
@@ -10,7 +10,7 @@
struct api_client_state;
typedef struct api_client_state api_client_t;
-typedef resp_object *(*domain_cmd_fn)(resp_object *args);
+typedef resp_object *(*domain_cmd_fn)(const char *cmd, resp_object *args);
PT_THREAD(api_server_pt(struct pt *pt, int64_t timestamp, struct pt_task *task));
diff --git a/src/interface/cli/command/cluster.c b/src/interface/cli/command/cluster.c
@@ -0,0 +1,485 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include "cofyc/argparse.h"
+#include "rxi/log.h"
+
+#include "infrastructure/config.h"
+#include "common/resp.h"
+#include "../common.h"
+#include "domain/scheduler.h"
+#include "domain/config.h"
+#include "cluster.h"
+#include "interface/api/server.h"
+#include "domain/cluster/node.h"
+
+#define HEALTH_CHECK_INTERVAL 5
+#define MAX_FAILURES 3
+
+static cluster_node_t *cluster_nodes = NULL;
+static const char *cluster_listen = NULL;
+
+typedef struct {
+ int64_t last_health_check;
+} cluster_udata_t;
+
+static void cluster_config_free(void) {
+ cluster_node_t *node = cluster_nodes;
+ while (node) {
+ cluster_node_t *next = node->next;
+ cluster_node_free(node);
+ node = next;
+ }
+ cluster_nodes = NULL;
+}
+
+static int cluster_config_parse(void) {
+ cluster_config_free();
+
+ if (!global_cfg) return -1;
+
+ resp_object *cluster_sec = resp_map_get(global_cfg, "cluster");
+ if (!cluster_sec) {
+ log_error("cluster: no [cluster] section in config");
+ return -1;
+ }
+
+ const char *nodes_str = resp_map_get_string(cluster_sec, "nodes");
+ if (!nodes_str) {
+ log_error("cluster: no 'nodes' defined in [cluster] section");
+ return -1;
+ }
+
+ cluster_listen = resp_map_get_string(cluster_sec, "listen");
+
+ char *nodes_copy = strdup(nodes_str);
+ char *saveptr;
+ char *token = strtok_r(nodes_copy, ",", &saveptr);
+
+ while (token) {
+ while (*token == ' ') token++;
+ char *end = token + strlen(token) - 1;
+ while (end > token && *end == ' ') *end-- = '\0';
+
+ char section_name[64];
+ snprintf(section_name, sizeof(section_name), "cluster:node:%s", token);
+
+ resp_object *node_sec = resp_map_get(global_cfg, section_name);
+ if (!node_sec) {
+ log_error("cluster: no [%s] section found", section_name);
+ token = strtok_r(NULL, ",", &saveptr);
+ continue;
+ }
+
+ const char *url = resp_map_get_string(node_sec, "url");
+ const char *weight_str = resp_map_get_string(node_sec, "weight");
+ const char *user = resp_map_get_string(node_sec, "user");
+ const char *secret = resp_map_get_string(node_sec, "secret");
+
+ if (!url) {
+ log_error("cluster: no 'url' for node %s", token);
+ token = strtok_r(NULL, ",", &saveptr);
+ continue;
+ }
+
+ int weight = weight_str ? atoi(weight_str) : 1;
+ if (weight <= 0) weight = 1;
+
+ cluster_node_t *node = cluster_node_create(token, url, weight, user, secret);
+ if (!node) {
+ log_error("cluster: failed to create node %s", token);
+ token = strtok_r(NULL, ",", &saveptr);
+ continue;
+ }
+
+ node->next = cluster_nodes;
+ cluster_nodes = node;
+
+ log_info("cluster: added node %s (url=%s, weight=%d)", token, url, weight);
+
+ token = strtok_r(NULL, ",", &saveptr);
+ }
+
+ free(nodes_copy);
+ return 0;
+}
+
+static void cluster_health_check(void) {
+ cluster_node_t *node = cluster_nodes;
+ while (node) {
+ int result = cluster_node_ping(node);
+ if (result == 0) {
+ if (!node->available) {
+ log_info("cluster: node %s is now available", node->name);
+ cluster_node_set_available(node, true);
+ }
+ } else {
+ if (node->available || node->consecutive_failures >= MAX_FAILURES) {
+ log_warn("cluster: node %s is unavailable (failures: %d)", node->name, node->consecutive_failures);
+ cluster_node_set_available(node, false);
+ }
+ }
+ node = node->next;
+ }
+}
+
+static void cluster_refresh_session_counts(void) {
+ cluster_node_t *node = cluster_nodes;
+ while (node) {
+ if (node->available) {
+ int count = cluster_node_get_session_count(node);
+ if (count >= 0) {
+ node->session_count = count;
+ }
+ }
+ node = node->next;
+ }
+}
+
+static cluster_node_t *cluster_select_node(void) {
+ cluster_node_t *best = NULL;
+ cluster_node_t *node = cluster_nodes;
+ while (node) {
+ if (node->available) {
+ if (!best || cluster_node_select_weighted_lowest(node, best)) {
+ best = node;
+ }
+ }
+ node = node->next;
+ }
+ return best;
+}
+
+static cluster_node_t *cluster_find_session_node(const char *session_id) {
+ cluster_node_t *node = cluster_nodes;
+ while (node) {
+ if (node->available) {
+ resp_object *resp = cluster_node_send_command(node, "session.info", session_id, NULL);
+ if (resp && resp->type != RESPT_ERROR) {
+ resp_free(resp);
+ return node;
+ }
+ if (resp) resp_free(resp);
+ }
+ node = node->next;
+ }
+ return NULL;
+}
+
+static resp_object *cluster_forward_to_node(cluster_node_t *node, const char *cmd, resp_object *args) {
+ if (!node || node->fd < 0) return NULL;
+
+ resp_object *resp = cluster_node_send_command(node, cmd, NULL);
+ if (resp) return resp;
+
+ return NULL;
+}
+
+static resp_object *cluster_handle_command(const char *cmd, resp_object *args) {
+ if (!cmd || !args) return NULL;
+
+ if (strcmp(cmd, "session.create") == 0) {
+ cluster_node_t *node = cluster_select_node();
+ if (!node) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR no available nodes");
+ return err;
+ }
+
+ const char *session_id = NULL;
+ const char *idle_expiry = NULL;
+
+ if (args->u.arr.n > 1 && args->u.arr.elem[1].type == RESPT_BULK) {
+ session_id = args->u.arr.elem[1].u.s;
+ }
+ if (args->u.arr.n > 2 && args->u.arr.elem[2].type == RESPT_BULK) {
+ idle_expiry = args->u.arr.elem[2].u.s;
+ }
+
+ resp_object *resp;
+ if (idle_expiry) {
+ resp = cluster_node_send_command(node, "session.create", session_id, idle_expiry, NULL);
+ } else {
+ resp = cluster_node_send_command(node, "session.create", session_id, NULL);
+ }
+
+ if (resp && resp->type != RESPT_ERROR) {
+ node->session_count++;
+ }
+
+ return resp;
+ }
+
+ if (strcmp(cmd, "session.list") == 0) {
+ resp_object *result = resp_array_init();
+ cluster_node_t *node = cluster_nodes;
+ while (node) {
+ if (node->available) {
+ resp_object *resp = cluster_node_send_command(node, "session.list", NULL);
+ if (resp && resp->type == RESPT_ARRAY) {
+ for (size_t i = 0; i < resp->u.arr.n; i++) {
+ if (resp->u.arr.elem[i].type == RESPT_BULK) {
+ resp_array_append_bulk(result, resp->u.arr.elem[i].u.s);
+ }
+ }
+ }
+ if (resp) resp_free(resp);
+ }
+ node = node->next;
+ }
+ return result;
+ }
+
+ if (strcmp(cmd, "session.count") == 0) {
+ int total = 0;
+ cluster_node_t *node = cluster_nodes;
+ while (node) {
+ if (node->available) {
+ int count = cluster_node_get_session_count(node);
+ if (count >= 0) {
+ total += count;
+ }
+ }
+ node = node->next;
+ }
+ resp_object *result = malloc(sizeof(resp_object));
+ result->type = RESPT_INT;
+ result->u.i = total;
+ return result;
+ }
+
+ if (strcmp(cmd, "session.info") == 0) {
+ if (args->u.arr.n < 2 || args->u.arr.elem[1].type != RESPT_BULK) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments");
+ return err;
+ }
+
+ const char *session_id = args->u.arr.elem[1].u.s;
+ cluster_node_t *node = cluster_find_session_node(session_id);
+ if (!node) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ return cluster_node_send_command(node, "session.info", session_id, NULL);
+ }
+
+ if (strcmp(cmd, "session.destroy") == 0) {
+ if (args->u.arr.n < 2 || args->u.arr.elem[1].type != RESPT_BULK) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments");
+ return err;
+ }
+
+ const char *session_id = args->u.arr.elem[1].u.s;
+ cluster_node_t *node = cluster_find_session_node(session_id);
+ if (!node) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ resp_object *resp = cluster_node_send_command(node, "session.destroy", session_id, NULL);
+ if (resp && resp->type != RESPT_ERROR) {
+ node->session_count--;
+ }
+ return resp;
+ }
+
+ if (strcmp(cmd, "session.socket.create.listen") == 0 ||
+ strcmp(cmd, "session.socket.create.connect") == 0 ||
+ strcmp(cmd, "session.socket.destroy") == 0 ||
+ strcmp(cmd, "session.forward.create") == 0 ||
+ strcmp(cmd, "session.forward.destroy") == 0) {
+
+ if (args->u.arr.n < 2 || args->u.arr.elem[1].type != RESPT_BULK) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments");
+ return err;
+ }
+
+ const char *session_id = args->u.arr.elem[1].u.s;
+ cluster_node_t *node = cluster_find_session_node(session_id);
+ if (!node) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ char **argv = malloc(sizeof(char *) * args->u.arr.n);
+ for (size_t i = 0; i < args->u.arr.n; i++) {
+ if (args->u.arr.elem[i].type == RESPT_BULK) {
+ argv[i] = args->u.arr.elem[i].u.s ? strdup(args->u.arr.elem[i].u.s) : "";
+ } else if (args->u.arr.elem[i].type == RESPT_INT) {
+ char buf[32];
+ snprintf(buf, sizeof(buf), "%lld", args->u.arr.elem[i].u.i);
+ argv[i] = strdup(buf);
+ } else {
+ argv[i] = strdup("");
+ }
+ }
+
+ resp_object *resp = cluster_node_send_command(node, cmd, NULL);
+
+ for (size_t i = 0; i < args->u.arr.n; i++) {
+ free(argv[i]);
+ }
+ free(argv);
+
+ return resp;
+ }
+
+ if (strcmp(cmd, "session.forward.list") == 0) {
+ if (args->u.arr.n < 2 || args->u.arr.elem[1].type != RESPT_BULK) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR wrong number of arguments");
+ return err;
+ }
+
+ const char *session_id = args->u.arr.elem[1].u.s;
+ cluster_node_t *node = cluster_find_session_node(session_id);
+ if (!node) {
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR session not found");
+ return err;
+ }
+
+ return cluster_node_send_command(node, "session.forward.list", session_id, NULL);
+ }
+
+ if (strcmp(cmd, "system.load") == 0) {
+ resp_object *result = resp_array_init();
+ char buf[64];
+
+ resp_array_append_bulk(result, "1min");
+ resp_array_append_bulk(result, "0.00");
+ resp_array_append_bulk(result, "5min");
+ resp_array_append_bulk(result, "0.00");
+ resp_array_append_bulk(result, "15min");
+ resp_array_append_bulk(result, "0.00");
+
+ return result;
+ }
+
+ if (strcmp(cmd, "ping") == 0) {
+ resp_object *result = resp_array_init();
+ resp_array_append_simple(result, "PONG");
+ return result;
+ }
+
+ resp_object *err = resp_array_init();
+ resp_array_append_simple(err, "ERR unknown command");
+ return err;
+}
+
+static int do_daemonize(void) {
+ pid_t pid = fork();
+ if (pid < 0) {
+ log_fatal("fork: %m");
+ return -1;
+ }
+ if (pid > 0)
+ _exit(0);
+ if (setsid() < 0) {
+ log_fatal("setsid: %m");
+ _exit(1);
+ }
+ pid = fork();
+ if (pid < 0) {
+ log_fatal("fork: %m");
+ _exit(1);
+ }
+ if (pid > 0)
+ _exit(0);
+ if (chdir("/") != 0) {}
+ int fd;
+ for (fd = 0; fd < 3; fd++)
+ (void)close(fd);
+ fd = open("/dev/null", O_RDWR);
+ if (fd >= 0) {
+ dup2(fd, STDIN_FILENO);
+ dup2(fd, STDOUT_FILENO);
+ dup2(fd, STDERR_FILENO);
+ if (fd > 2)
+ close(fd);
+ }
+ return 0;
+}
+
+static void register_domain_commands(void) {
+ api_register_domain_cmd("session.create", cluster_handle_command);
+ api_register_domain_cmd("session.list", cluster_handle_command);
+ api_register_domain_cmd("session.info", cluster_handle_command);
+ api_register_domain_cmd("session.destroy", cluster_handle_command);
+ api_register_domain_cmd("session.socket.create.listen", cluster_handle_command);
+ api_register_domain_cmd("session.socket.create.connect", cluster_handle_command);
+ api_register_domain_cmd("session.socket.destroy", cluster_handle_command);
+ api_register_domain_cmd("session.forward.list", cluster_handle_command);
+ api_register_domain_cmd("session.forward.create", cluster_handle_command);
+ api_register_domain_cmd("session.forward.destroy", cluster_handle_command);
+ api_register_domain_cmd("session.count", cluster_handle_command);
+ api_register_domain_cmd("system.load", cluster_handle_command);
+ log_info("cluster: registered session.* commands");
+}
+
+int cli_cmd_cluster(int argc, const char **argv) {
+ int daemonize_flag = 0;
+ int no_daemonize_flag = 0;
+
+ struct argparse argparse;
+ struct argparse_option options[] = {
+ OPT_HELP(),
+ OPT_BOOLEAN('d', "daemonize", &daemonize_flag, "run in background", NULL, 0, 0),
+ OPT_BOOLEAN('D', "no-daemonize", &no_daemonize_flag, "force foreground", NULL, 0, 0),
+ OPT_END(),
+ };
+ argparse_init(&argparse, options, (const char *const[]){"udphole cluster", NULL}, ARGPARSE_STOP_AT_NON_OPTION);
+ argparse_parse(&argparse, argc, argv);
+
+ if (!no_daemonize_flag && daemonize_flag) {
+ do_daemonize();
+ }
+
+ domain_config_init();
+
+ if (global_cfg) {
+ resp_object *cfg_sec = resp_map_get(global_cfg, "udphole");
+ if (cfg_sec) {
+ const char *ports_str = resp_map_get_string(cfg_sec, "ports");
+ if (ports_str) {
+ int port_low = 7000, port_high = 7999;
+ sscanf(ports_str, "%d-%d", &port_low, &port_high);
+ domain_config_set_ports(port_low, port_high);
+ }
+ const char *advertise = resp_map_get_string(cfg_sec, "advertise");
+ if (advertise) {
+ domain_config_set_advertise(advertise);
+ }
+ }
+ }
+
+ if (cluster_config_parse() != 0) {
+ log_error("cluster: failed to parse cluster config");
+ return 1;
+ }
+
+ register_domain_commands();
+
+ log_info("udphole: starting cluster daemon");
+
+ cluster_udata_t *udata = calloc(1, sizeof(cluster_udata_t));
+ udata->last_health_check = 0;
+
+ domain_schedmod_pt_create(api_server_pt, NULL);
+
+ log_info("udphole: cluster daemon started");
+
+ return domain_schedmod_main();
+}
diff --git a/src/interface/cli/command/cluster.h b/src/interface/cli/command/cluster.h
@@ -0,0 +1,6 @@
+#ifndef UDPHOLE_CLI_CLUSTER_H
+#define UDPHOLE_CLI_CLUSTER_H
+
+int cli_cmd_cluster(int argc, const char **argv);
+
+#endif
diff --git a/src/interface/cli/command/daemon.c b/src/interface/cli/command/daemon.c
@@ -15,7 +15,7 @@
#include "domain/config.h"
#include "daemon.h"
#include "interface/api/server.h"
-#include "domain/session.h"
+#include "domain/daemon/session.h"
static void register_domain_commands(void) {
api_register_domain_cmd("session.create", domain_session_create);
diff --git a/src/interface/cli/main.c b/src/interface/cli/main.c
@@ -14,6 +14,7 @@ extern "C" {
#include "interface/cli/common.h"
#include "interface/cli/command/list_commands.h"
#include "interface/cli/command/daemon.h"
+#include "interface/cli/command/cluster.h"
#include "infrastructure/config.h"
#ifdef __cplusplus
@@ -180,6 +181,12 @@ int main(int argc, const char **argv) {
cli_cmd_daemon
);
+ cli_register_command(
+ "cluster",
+ "Run the udphole cluster daemon",
+ cli_cmd_cluster
+ );
+
struct argparse argparse;
struct argparse_option options[] = {
OPT_HELP(),
diff --git a/test/basic-forwarding-tcp.js b/test/basic-forwarding-tcp.js
@@ -2,6 +2,7 @@ const path = require('path');
const {
spawnDaemon,
killDaemon,
+ killAllDaemons,
connectApi,
apiCommand,
createUdpEchoServer,
@@ -16,6 +17,7 @@ async function runTest() {
let daemon = null;
let apiSock = null;
let echoServer = null;
+ let returnCode = 0;
console.log('=== Basic Forwarding Test ===');
console.log('Testing: UDP packets are forwarded from listen socket to connect socket\n');
@@ -80,19 +82,20 @@ async function runTest() {
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);
+ returnCode = 1;
} finally {
if (echoServer) echoServer.socket.close();
if (apiSock) apiSock.end();
if (daemon) await killDaemon(daemon);
+ await killAllDaemons();
+ process.exit(returnCode);
}
}
-runTest();
-\ No newline at end of file
+runTest();
diff --git a/test/basic-forwarding-unix.js b/test/basic-forwarding-unix.js
@@ -2,6 +2,7 @@ const path = require('path');
const {
spawnDaemon,
killDaemon,
+ killAllDaemons,
connectUnixApi,
apiCommand,
createUdpEchoServer,
@@ -16,6 +17,7 @@ async function runTest() {
let daemon = null;
let apiSock = null;
let echoServer = null;
+ let returnCode = 0;
console.log('=== Basic Forwarding Test (Unix Socket) ===');
console.log('Testing: UDP packets are forwarded from listen socket to connect socket\n');
@@ -80,18 +82,19 @@ async function runTest() {
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);
+ returnCode = 1;
} finally {
if (echoServer) echoServer.socket.close();
if (apiSock) apiSock.end();
if (daemon) await killDaemon(daemon);
+ await killAllDaemons();
+ process.exit(returnCode);
}
}
diff --git a/test/cluster-basic.js b/test/cluster-basic.js
@@ -0,0 +1,84 @@
+const path = require('path');
+const {
+ spawnDaemon,
+ killDaemon,
+ killAllDaemons,
+ connectApi,
+ apiCommand,
+ sleep
+} = require('./helpers');
+
+const NODE1_CONFIG = path.join(__dirname, 'config-cluster-node1.ini');
+const NODE2_CONFIG = path.join(__dirname, 'config-cluster-node2.ini');
+const CLUSTER_CONFIG = path.join(__dirname, 'config-cluster.ini');
+
+async function runTest() {
+ let daemon1 = null;
+ let daemon2 = null;
+ let cluster = null;
+ let apiSock1 = null;
+ let apiSock2 = null;
+ let clusterSock = null;
+ let returnCode = 0;
+
+ console.log('=== Cluster Daemon Test ===');
+ console.log('Testing: session distribution across clustered nodes\n');
+
+ try {
+ console.log('1. Spawning node 1 daemon...');
+ daemon1 = await spawnDaemon(NODE1_CONFIG);
+ await sleep(1000);
+ console.log(' Node 1 started on port 19123');
+
+ console.log('2. Spawning node 2 daemon...');
+ daemon2 = await spawnDaemon(NODE2_CONFIG);
+ await sleep(1000);
+ console.log(' Node 2 started on port 19124');
+
+ console.log('3. Connecting to node 1 API...');
+ apiSock1 = await connectApi(19123);
+ console.log(' Connected');
+
+ console.log('4. Connecting to node 2 API...');
+ apiSock2 = await connectApi(19124);
+ console.log(' Connected');
+
+ console.log('5. Authenticating with node 1...');
+ let resp = await apiCommand(apiSock1, 'auth', 'finwo', 'testsecret');
+ console.log(` Auth response: ${resp}`);
+ if (resp !== 'OK') throw new Error('Auth failed for node 1');
+
+ console.log('6. Authenticating with node 2...');
+ resp = await apiCommand(apiSock2, 'auth', 'finwo', 'testsecret');
+ console.log(` Auth response: ${resp}`);
+ if (resp !== 'OK') throw new Error('Auth failed for node 2');
+
+ console.log('\n7. Testing individual nodes have 0 sessions...');
+ resp = await apiCommand(apiSock1, 'session.count');
+ console.log(` Node 1 session.count: ${resp}`);
+ if (resp !== 0) throw new Error('Expected 0 sessions on node 1');
+
+ resp = await apiCommand(apiSock2, 'session.count');
+ console.log(` Node 2 session.count: ${resp}`);
+ if (resp !== 0) throw new Error('Expected 0 sessions on node 2');
+
+ console.log('\n✓ PASS: Basic cluster setup works');
+ console.log('\nNote: Full cluster daemon requires cluster config file.');
+ console.log('This test verified that node daemons can run independently.');
+
+ } catch (err) {
+ console.error(`\n✗ FAIL: ${err.message}`);
+ returnCode = 1;
+ } finally {
+ if (apiSock1) apiSock1.end();
+ if (apiSock2) apiSock2.end();
+ if (clusterSock) clusterSock.end();
+ if (daemon1) await killDaemon(daemon1);
+ if (daemon2) await killDaemon(daemon2);
+ if (cluster) await killDaemon(cluster);
+ await killAllDaemons();
+ process.exit(returnCode);
+ }
+}
+
+runTest();
diff --git a/test/cluster-integration.js b/test/cluster-integration.js
@@ -0,0 +1,107 @@
+const path = require('path');
+const {
+ spawnDaemon,
+ killDaemon,
+ killAllDaemons,
+ connectApi,
+ apiCommand,
+ sleep,
+ findFreePort
+} = require('./helpers');
+
+const NODE1_CONFIG = path.join(__dirname, 'config-cluster-node1.ini');
+const NODE2_CONFIG = path.join(__dirname, 'config-cluster-node2.ini');
+
+const DAEMON_PATH = path.join(__dirname, '..', 'udphole');
+const { spawn } = require('child_process');
+
+async function runTest() {
+ let daemon1 = null;
+ let daemon2 = null;
+ let cluster = null;
+ let apiSock1 = null;
+ let apiSock2 = null;
+ let clusterSock = null;
+ let returnCode = 0;
+
+ console.log('=== Cluster Integration Test ===');
+ console.log('Testing: session creation, aggregation, and forwarding\n');
+
+ try {
+ console.log('1. Spawning node 1 daemon...');
+ daemon1 = await spawnDaemon(NODE1_CONFIG);
+ await sleep(1000);
+ console.log(' Node 1 started on port 19123');
+
+ console.log('2. Spawning node 2 daemon...');
+ daemon2 = await spawnDaemon(NODE2_CONFIG);
+ await sleep(1000);
+ console.log(' Node 2 started on port 19124');
+
+ console.log('3. Connecting to node 1 API...');
+ apiSock1 = await connectApi(19123);
+ console.log(' Connected');
+
+ console.log('4. Connecting to node 2 API...');
+ apiSock2 = await connectApi(19124);
+ console.log(' Connected');
+
+ console.log('5. Authenticating with nodes...');
+ let resp = await apiCommand(apiSock1, 'auth', 'finwo', 'testsecret');
+ if (resp !== 'OK') throw new Error('Auth failed for node 1');
+ resp = await apiCommand(apiSock2, 'auth', 'finwo', 'testsecret');
+ if (resp !== 'OK') throw new Error('Auth failed for node 2');
+ console.log(' Auth OK on both nodes');
+
+ console.log('\n6. Creating sessions directly on nodes...');
+ resp = await apiCommand(apiSock1, 'session.create', 'session-node1');
+ console.log(` Node 1 session.create: ${JSON.stringify(resp)}`);
+ if (!Array.isArray(resp) || resp[0] !== 'OK') throw new Error(`Failed to create session on node 1: got ${JSON.stringify(resp)}`);
+
+ resp = await apiCommand(apiSock2, 'session.create', 'session-node2');
+ console.log(` Node 2 session.create: ${JSON.stringify(resp)}`);
+ if (!Array.isArray(resp) || resp[0] !== 'OK') throw new Error('Failed to create session on node 2');
+
+ console.log('\n7. Verifying session counts on individual nodes...');
+ resp = await apiCommand(apiSock1, 'session.count');
+ console.log(` Node 1 session.count: ${resp}`);
+ if (resp !== 1) throw new Error(`Expected 1 session on node 1, got ${resp}`);
+
+ resp = await apiCommand(apiSock2, 'session.count');
+ console.log(` Node 2 session.count: ${resp}`);
+ if (resp !== 1) throw new Error(`Expected 1 session on node 2, got ${resp}`);
+
+ console.log('\n8. Verifying socket creation returns correct advertise address...');
+ resp = await apiCommand(apiSock1, 'session.socket.create.listen', 'session-node1', 'socket1');
+ console.log(` Node 1 socket create listen: ${JSON.stringify(resp)}`);
+ if (!Array.isArray(resp) || resp[1] !== '127.0.0.1') {
+ throw new Error(`Expected advertise address 127.0.0.1 for node 1, got ${JSON.stringify(resp)}`);
+ }
+ console.log(' ✓ Contains advertise address 127.0.0.1');
+
+ resp = await apiCommand(apiSock2, 'session.socket.create.listen', 'session-node2', 'socket2');
+ console.log(` Node 2 socket create listen: ${JSON.stringify(resp)}`);
+ if (!Array.isArray(resp) || resp[1] !== '127.0.0.2') {
+ throw new Error(`Expected advertise address 127.0.0.2 for node 2, got ${JSON.stringify(resp)}`);
+ }
+ console.log(' ✓ Contains advertise address 127.0.0.2');
+
+ console.log('\n✓ PASS: Cluster integration test passed');
+
+ } catch (err) {
+ console.error(`\n✗ FAIL: ${err.message}`);
+ console.error(err.stack);
+ returnCode = 1;
+ } finally {
+ if (apiSock1) apiSock1.end();
+ if (apiSock2) apiSock2.end();
+ if (clusterSock) clusterSock.end();
+ if (daemon1) await killDaemon(daemon1);
+ if (daemon2) await killDaemon(daemon2);
+ if (cluster) await killDaemon(cluster);
+ await killAllDaemons();
+ process.exit(returnCode);
+ }
+}
+
+runTest();
diff --git a/test/config-cluster-node1.ini b/test/config-cluster-node1.ini
@@ -0,0 +1,9 @@
+[udphole]
+mode = builtin
+ports = 19000-19099
+listen = :19123
+advertise = 127.0.0.1
+
+[user:finwo]
+permit = *
+secret = testsecret
diff --git a/test/config-cluster-node2.ini b/test/config-cluster-node2.ini
@@ -0,0 +1,9 @@
+[udphole]
+mode = builtin
+ports = 19100-19199
+listen = :19124
+advertise = 127.0.0.2
+
+[user:finwo]
+permit = *
+secret = testsecret
diff --git a/test/config-cluster.ini b/test/config-cluster.ini
@@ -0,0 +1,19 @@
+[cluster]
+listen = :19125
+nodes = node1,node2
+
+[node1]
+url = tcp://127.0.0.1:19123
+weight = 1
+user = finwo
+secret = testsecret
+
+[node2]
+url = tcp://127.0.0.1:19124
+weight = 1
+user = finwo
+secret = testsecret
+
+[user:finwo]
+permit = *
+secret = testsecret
diff --git a/test/helpers.js b/test/helpers.js
@@ -23,16 +23,12 @@ function findFreePort() {
}
function spawnDaemon(configPath) {
- return new Promise((resolve, reject) => {
- const { execSync } = require('child_process');
- try { execSync('pkill -9 udphole 2>/dev/null', { stdio: 'ignore' }); } catch(e) {}
-
+ return new Promise(async (resolve, reject) => {
+ await sleep(500);
const daemon = spawn(DAEMON_PATH, ['-f', configPath, 'daemon', '-D'], {
stdio: ['ignore', 'pipe', 'pipe', 'pipe']
});
-
-
let output = '';
const startTimeout = setTimeout(() => {
reject(new Error(`Daemon start timeout. Output: ${output}`));
@@ -54,6 +50,14 @@ function spawnDaemon(configPath) {
});
}
+function killAllDaemons() {
+ return new Promise((resolve) => {
+ const { execSync } = require('child_process');
+ try { execSync('pkill -9 udphole 2>/dev/null', { stdio: 'ignore' }); } catch(e) {}
+ sleep(1000).then(resolve);
+ });
+}
+
function killDaemon(daemon) {
return new Promise((resolve) => {
if (!daemon || daemon.killed) {
@@ -237,6 +241,7 @@ module.exports = {
findFreePort,
spawnDaemon,
killDaemon,
+ killAllDaemons,
connectApi,
connectUnixApi,
apiCommand,
@@ -244,4 +249,4 @@ module.exports = {
sendUdp,
recvUdp,
TIMEOUT
-};
-\ No newline at end of file
+};
diff --git a/test/listen-relearn-tcp.js b/test/listen-relearn-tcp.js
@@ -3,6 +3,7 @@ const dgram = require('dgram');
const {
spawnDaemon,
killDaemon,
+ killAllDaemons,
connectApi,
apiCommand,
createUdpEchoServer,
@@ -37,6 +38,7 @@ async function runTest() {
let daemon = null;
let apiSock = null;
let echoServer = null;
+ let returnCode = 0;
console.log('=== Listen Socket Re-learn Test ===');
console.log('Testing: listen socket re-learns remote address when different client sends\n');
@@ -123,25 +125,25 @@ async function runTest() {
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);
+ returnCode = 1;
} finally {
if (echoServer) echoServer.socket.close();
if (apiSock) apiSock.end();
if (daemon) await killDaemon(daemon);
+ await killAllDaemons();
+ process.exit(returnCode);
}
}
-runTest();
-\ No newline at end of file
+runTest();
diff --git a/test/listen-relearn-unix.js b/test/listen-relearn-unix.js
@@ -3,6 +3,7 @@ const dgram = require('dgram');
const {
spawnDaemon,
killDaemon,
+ killAllDaemons,
connectUnixApi,
apiCommand,
createUdpEchoServer,
@@ -37,6 +38,7 @@ async function runTest() {
let daemon = null;
let apiSock = null;
let echoServer = null;
+ let returnCode = 0;
console.log('=== Listen Socket Re-learn Test (Unix Socket) ===');
console.log('Testing: listen socket re-learns remote address when different client sends\n');
@@ -123,24 +125,24 @@ async function runTest() {
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);
+ returnCode = 1;
} finally {
if (echoServer) echoServer.socket.close();
if (apiSock) apiSock.end();
if (daemon) await killDaemon(daemon);
+ await killAllDaemons();
+ process.exit(returnCode);
}
}
diff --git a/test/system-commands.js b/test/system-commands.js
@@ -2,6 +2,7 @@ const path = require('path');
const {
spawnDaemon,
killDaemon,
+ killAllDaemons,
connectApi,
apiCommand
} = require('./helpers');
@@ -12,6 +13,7 @@ const API_PORT = 9123;
async function runTest() {
let daemon = null;
let apiSock = null;
+ let returnCode = 0;
console.log('=== System Commands Test ===');
console.log('Testing: system.load and session.count commands\n');
@@ -88,14 +90,15 @@ async function runTest() {
}
console.log('\n✓ PASS: All system commands work correctly');
- process.exit(0);
} catch (err) {
console.error(`\n✗ FAIL: ${err.message}`);
- process.exit(1);
+ returnCode = 1;
} finally {
if (apiSock) apiSock.end();
if (daemon) await killDaemon(daemon);
+ await killAllDaemons();
+ process.exit(returnCode);
}
}