commit d3b47d654f4e399419f20aaaac7b0939f36997df
parent 58e0844b7c2805cb42e7a6d896693f8e2584158a
Author: finwo <finwo@pm.me>
Date: Fri, 18 Sep 2026 12:59:47 +0200
Unit tests, unosd is now a multi-call binary for subcommands
Diffstat:
15 files changed, 1419 insertions(+), 488 deletions(-)
diff --git a/src/cli/ifdown.c b/src/cli/ifdown.c
@@ -0,0 +1,51 @@
+#define _GNU_SOURCE
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include "registry.h"
+
+#define UNOS_IPC_PATH "/run/unosd.sock"
+
+int main_ifdown(int argc, char *argv[]) {
+ if (argc < 2) {
+ fprintf(stderr, "usage: ifdown <interface>\n");
+ return 1;
+ }
+ char out[4096];
+ int len = snprintf(out, sizeof(out), "ifdown");
+ for (int i = 1; i < argc; i++) len += snprintf(out+len, sizeof(out)-len, " %s", argv[i]);
+ len += snprintf(out+len, sizeof(out)-len, "\n");
+ int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sock < 0) { perror("socket"); return 1; }
+ struct sockaddr_un addr = {0};
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, UNOS_IPC_PATH, sizeof(addr.sun_path)-1);
+ if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
+ fprintf(stderr, "ifdown: connect %s failed: %s\nIs unosd running?\n", UNOS_IPC_PATH, strerror(errno));
+ close(sock);
+ return 1;
+ }
+ if (write(sock, out, len) != len) { perror("write"); close(sock); return 1; }
+ // Half-close: signals EOF to unosd so it stops reading and replies. Without this
+ // the server blocks waiting for a second command until its receive timeout.
+ shutdown(sock, SHUT_WR);
+ char buf[4096];
+ int saw_err = 0;
+ FILE *f = fdopen(sock, "r");
+ if (!f) { perror("fdopen"); close(sock); return 1; }
+ while (fgets(buf, sizeof(buf), f)) {
+ if (!strncmp(buf, "ERR", 3)) saw_err = 1;
+ fputs(buf, stdout);
+ }
+ fclose(f);
+ return saw_err ? 1 : 0;
+}
+
+__attribute__((constructor))
+static void register_ifdown(void) {
+ cli_register("ifdown", main_ifdown);
+}
diff --git a/src/cli/ifquery.c b/src/cli/ifquery.c
@@ -0,0 +1,47 @@
+#define _GNU_SOURCE
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include "registry.h"
+
+#define UNOS_IPC_PATH "/run/unosd.sock"
+
+int main_ifquery(int argc, char *argv[]) {
+ char out[4096];
+ int len = snprintf(out, sizeof(out), "ifquery");
+ for (int i = 1; i < argc; i++) len += snprintf(out+len, sizeof(out)-len, " %s", argv[i]);
+ len += snprintf(out+len, sizeof(out)-len, "\n");
+ int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sock < 0) { perror("socket"); return 1; }
+ struct sockaddr_un addr = {0};
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, UNOS_IPC_PATH, sizeof(addr.sun_path)-1);
+ if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
+ fprintf(stderr, "ifquery: connect %s failed: %s\nIs unosd running?\n", UNOS_IPC_PATH, strerror(errno));
+ close(sock);
+ return 1;
+ }
+ if (write(sock, out, len) != len) { perror("write"); close(sock); return 1; }
+ // Half-close: signals EOF to unosd so it stops reading and replies. Without this
+ // the server blocks waiting for a second command until its receive timeout.
+ shutdown(sock, SHUT_WR);
+ char buf[4096];
+ int saw_err = 0;
+ FILE *f = fdopen(sock, "r");
+ if (!f) { perror("fdopen"); close(sock); return 1; }
+ while (fgets(buf, sizeof(buf), f)) {
+ if (!strncmp(buf, "ERR", 3)) saw_err = 1;
+ fputs(buf, stdout);
+ }
+ fclose(f);
+ return saw_err ? 1 : 0;
+}
+
+__attribute__((constructor))
+static void register_ifquery(void) {
+ cli_register("ifquery", main_ifquery);
+}
diff --git a/src/cli/ifreload.c b/src/cli/ifreload.c
@@ -0,0 +1,50 @@
+#define _GNU_SOURCE
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include "registry.h"
+
+#define UNOS_IPC_PATH "/run/unosd.sock"
+
+int main_ifreload(int argc, char *argv[]) {
+ if (argc != 1) {
+ fprintf(stderr, "usage: ifreload\n");
+ return 1;
+ }
+ (void)argv;
+ char out[4096];
+ int len = snprintf(out, sizeof(out), "ifreload\n");
+ int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sock < 0) { perror("socket"); return 1; }
+ struct sockaddr_un addr = {0};
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, UNOS_IPC_PATH, sizeof(addr.sun_path)-1);
+ if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
+ fprintf(stderr, "ifreload: connect %s failed: %s\nIs unosd running?\n", UNOS_IPC_PATH, strerror(errno));
+ close(sock);
+ return 1;
+ }
+ if (write(sock, out, len) != len) { perror("write"); close(sock); return 1; }
+ // Half-close: signals EOF to unosd so it stops reading and replies. Without this
+ // the server blocks waiting for a second command until its receive timeout.
+ shutdown(sock, SHUT_WR);
+ char buf[4096];
+ int saw_err = 0;
+ FILE *f = fdopen(sock, "r");
+ if (!f) { perror("fdopen"); close(sock); return 1; }
+ while (fgets(buf, sizeof(buf), f)) {
+ if (!strncmp(buf, "ERR", 3)) saw_err = 1;
+ fputs(buf, stdout);
+ }
+ fclose(f);
+ return saw_err ? 1 : 0;
+}
+
+__attribute__((constructor))
+static void register_ifreload(void) {
+ cli_register("ifreload", main_ifreload);
+}
diff --git a/src/cli/ifup.c b/src/cli/ifup.c
@@ -0,0 +1,56 @@
+#define _GNU_SOURCE
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include "registry.h"
+
+#define UNOS_IPC_PATH "/run/unosd.sock"
+
+int main_ifup(int argc, char *argv[]) {
+ // ifup <if> -- thin wrapper, same socket logic as unosc but fixed command
+ if (argc < 2) {
+ fprintf(stderr, "usage: ifup <interface>\n");
+ return 1;
+ }
+ // Build command: "ifup <if> [extra args]"
+ char out[4096];
+ int len = snprintf(out, sizeof(out), "ifup");
+ for (int i = 1; i < argc; i++) {
+ len += snprintf(out+len, sizeof(out)-len, " %s", argv[i]);
+ }
+ len += snprintf(out+len, sizeof(out)-len, "\n");
+
+ int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sock < 0) { perror("socket"); return 1; }
+ struct sockaddr_un addr = {0};
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, UNOS_IPC_PATH, sizeof(addr.sun_path)-1);
+ if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
+ fprintf(stderr, "ifup: connect %s failed: %s\nIs unosd running?\n", UNOS_IPC_PATH, strerror(errno));
+ close(sock);
+ return 1;
+ }
+ if (write(sock, out, len) != len) { perror("write"); close(sock); return 1; }
+ // Half-close: signals EOF to unosd so it stops reading and replies. Without this
+ // the server blocks waiting for a second command until its receive timeout.
+ shutdown(sock, SHUT_WR);
+ char buf[4096];
+ int saw_err = 0;
+ FILE *f = fdopen(sock, "r");
+ if (!f) { perror("fdopen"); close(sock); return 1; }
+ while (fgets(buf, sizeof(buf), f)) {
+ if (!strncmp(buf, "ERR", 3)) saw_err = 1;
+ fputs(buf, stdout);
+ }
+ fclose(f);
+ return saw_err ? 1 : 0;
+}
+
+__attribute__((constructor))
+static void register_ifup(void) {
+ cli_register("ifup", main_ifup);
+}
diff --git a/src/cli/registry.c b/src/cli/registry.c
@@ -0,0 +1,22 @@
+#include <string.h>
+#include "registry.h"
+
+#define CLI_MAX 32
+
+static const char *cli_names[CLI_MAX];
+static cli_main_fn cli_fns[CLI_MAX];
+static int cli_count = 0;
+
+void cli_register(const char *name, cli_main_fn fn) {
+ if (cli_count >= CLI_MAX) return;
+ cli_names[cli_count] = name;
+ cli_fns[cli_count] = fn;
+ cli_count++;
+}
+
+cli_main_fn cli_find(const char *name) {
+ for (int i = 0; i < cli_count; i++) {
+ if (!strcmp(cli_names[i], name)) return cli_fns[i];
+ }
+ return NULL;
+}
diff --git a/src/cli/registry.h b/src/cli/registry.h
@@ -0,0 +1,9 @@
+#ifndef __UNOS_CLI_REGISTRY_H__
+#define __UNOS_CLI_REGISTRY_H__
+
+typedef int (*cli_main_fn)(int argc, char **argv);
+
+void cli_register(const char *name, cli_main_fn fn);
+cli_main_fn cli_find(const char *name);
+
+#endif // __UNOS_CLI_REGISTRY_H__
diff --git a/src/cli/unosc.c b/src/cli/unosc.c
@@ -8,34 +8,24 @@
#include <sys/un.h>
#include <unistd.h>
+#include "registry.h"
+
#define UNOS_IPC_PATH "/run/unosd.sock"
-int main(int argc, char *argv[]) {
- const char *prog = basename(argv[0]);
- const char *cmd = NULL;
- int cmd_argc = 0;
- char **cmd_argv = NULL;
+int main_unosc(int argc, char *argv[]) {
+ // unosc is a tiny shim: unosc <cmd> [args...] -> send "cmd [args...]\n" to /run/unosd.sock
+ // It does NOT handle being called as ifup via argv[0]; those have their own mains.
+ if (argc < 2) {
+ fprintf(stderr, "usage: %s <ifup|ifdown|ifquery|ifreload> [args...]\n", argv[0]);
+ return 1;
+ }
+ const char *cmd = argv[1];
+ int cmd_argc = argc - 2;
+ char **cmd_argv = argv + 2;
int sock;
struct sockaddr_un addr;
char buf[4096];
- // Determine command from argv[0] or argv[1]
- if (!strcmp(prog, "unosc")) {
- if (argc < 2) {
- fprintf(stderr, "usage: %s <ifup|ifdown|ifquery|ifreload> [args...]\n", prog);
- fprintf(stderr, " or: ifup/ifdown/ifquery/ifreload as symlinks to unosc\n");
- return 1;
- }
- cmd = argv[1];
- cmd_argc = argc - 2;
- cmd_argv = argv + 2;
- } else {
- // Called as ifup, ifdown, etc. via symlink
- cmd = prog;
- cmd_argc = argc - 1;
- cmd_argv = argv + 1;
- }
-
sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (sock < 0) {
perror("socket");
@@ -67,6 +57,9 @@ int main(int argc, char *argv[]) {
close(sock);
return 1;
}
+ // Half-close: signals EOF to unosd so it stops reading and replies. Without
+ // this the server blocks waiting for a second command until its timeout.
+ shutdown(sock, SHUT_WR);
}
// Read response and pipe to stdout
@@ -95,3 +88,8 @@ int main(int argc, char *argv[]) {
if (saw_err) return 1;
return 0;
}
+
+__attribute__((constructor))
+static void register_unosc(void) {
+ cli_register("unosc", main_unosc);
+}
diff --git a/src/cli/unosd.c b/src/cli/unosd.c
@@ -0,0 +1,450 @@
+#define _GNU_SOURCE
+
+#include <signal.h>
+#include <errno.h>
+#include <net/if.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "cofyc/argparse.h"
+#include "rxi/log.h"
+
+#include "config/ports.h"
+#include "config/ifaces.h"
+#include "dataplane.h"
+#include "dataplane/registry.h"
+#include "ipc.h"
+#include "netlink/netlink.h"
+#include "netlink/rtnl.h"
+#include "util/config.h"
+#include "cli/registry.h"
+
+static const char *const usage[] = {
+ __NAME " [options]",
+ __NAME " --help",
+ NULL,
+};
+
+static FILE *log_file;
+static char *log_path;
+static volatile sig_atomic_t sighup_received;
+
+static void logfile_callback(log_Event *ev) {
+ if (sighup_received) {
+ sighup_received = 0;
+ if (log_path && log_file) {
+ fclose(log_file);
+ log_file = fopen(log_path, "a");
+ }
+ }
+ if (log_file) {
+ char buf[64];
+ buf[strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", ev->time)] = '\0';
+ fprintf(log_file, "%s %-5s %s:%d: ", buf, log_level_string(ev->level), ev->file, ev->line);
+ vfprintf(log_file, ev->fmt, ev->ap);
+ fprintf(log_file, "\n");
+ fflush(log_file);
+ }
+}
+
+static void sighup_handler(int sig) {
+ (void)sig;
+ sighup_received = 1;
+}
+
+static void stop_handler(int sig) {
+ (void)sig;
+ nl_request_stop();
+}
+
+// Port and interface configuration live in either:
+// <dir>/ports, <dir>/interfaces single files, or
+// <dir>/ports.d/*.cnf, <dir>/interfaces.d/*.cnf drop-ins
+//
+// Rather than growing the config API, the drop-in case is handed to the parser
+// as an in-memory config consisting of `source` -- the parser expands it.
+static int load_ports(const char *dir) {
+ char path[1024];
+ char stub[1024];
+ FILE *fd;
+ int rc;
+
+ snprintf(path, sizeof(path), "%s/ports", dir);
+
+ if (access(path, R_OK) == 0) {
+ fd = fopen(path, "r");
+ if (!fd) {
+ perror("Could not open config file");
+ return CFG_RET_ERROR;
+ }
+ } else {
+ snprintf(stub, sizeof(stub), "source ports.d/*.cnf\n");
+ fd = fmemopen(stub, strlen(stub), "r");
+ if (!fd) {
+ perror("Could not open in-memory config");
+ return CFG_RET_ERROR;
+ }
+ }
+
+ rc = cfg_parse(cfg_ns_get("ports"), dir, fd, NULL);
+ if (rc < 0) {
+ log_error("Error during reading port configuration from %s", dir);
+ }
+
+ fclose(fd);
+ return rc;
+}
+
+static int load_interfaces(const char *dir) {
+ char path[1024];
+ char stub[1024];
+ FILE *fd;
+ int rc;
+
+ snprintf(path, sizeof(path), "%s/interfaces", dir);
+
+ if (access(path, R_OK) == 0) {
+ fd = fopen(path, "r");
+ if (!fd) {
+ perror("Could not open config file");
+ return CFG_RET_ERROR;
+ }
+ } else {
+ snprintf(stub, sizeof(stub), "source interfaces.d/*.cnf\n");
+ fd = fmemopen(stub, strlen(stub), "r");
+ if (!fd) {
+ perror("Could not open in-memory config");
+ return CFG_RET_ERROR;
+ }
+ }
+
+ rc = cfg_parse(cfg_ns_get("interfaces"), dir, fd, NULL);
+ if (rc < 0) {
+ log_error("Error during reading interfaces configuration from %s", dir);
+ }
+
+ fclose(fd);
+ return rc;
+}
+
+static int apply_ports(void) {
+ const struct dp_ops *dp = dp_active();
+ struct unos_port *cur;
+ struct dp_port port;
+
+ if (!dp || !dp->port_apply) return 0;
+
+ for ( cur = ports_list() ; cur ; cur = cur->next ) {
+ memset(&port, 0, sizeof(port));
+ port.name = cur->name;
+ port.speed = cur->speed;
+ port.fec = cur->fec;
+ port.autoneg = cur->autoneg;
+
+ if (dp->port_apply(&port) != DP_RET_OK) {
+ log_warn("%s: failed to apply port configuration", cur->name);
+ }
+ }
+
+ return 0;
+}
+
+static int apply_interfaces(void) {
+ struct unos_iface *cur;
+ for (cur = ifaces_list(); cur; cur = cur->next) {
+ if (!cur->auto_flag) continue;
+ if (cur->pre_up) {
+ log_info("apply_interfaces: pre-up %s: %s", cur->name, cur->pre_up);
+ if (system(cur->pre_up) != 0) {
+ log_warn("pre-up for %s failed", cur->name);
+ continue;
+ }
+ }
+ if (cur->vlan_id >= 0) {
+ const char *raw = cur->vlan_raw_device;
+ char tmp[IF_NAMESIZE];
+ if (!raw) {
+ char *dot = strrchr(cur->name, '.');
+ if (dot) {
+ size_t len = dot - cur->name;
+ if (len < sizeof(tmp)) {
+ memcpy(tmp, cur->name, len);
+ tmp[len] = '\0';
+ raw = tmp;
+ }
+ }
+ }
+ if (raw) {
+ log_info("apply_interfaces: vlan %s id %d on %s", cur->name, cur->vlan_id, raw);
+ rtnl_vlan_create(cur->name, raw, cur->vlan_id);
+ }
+ }
+ bool is_bridge = false;
+ if (cur->bridge_ports) is_bridge = true;
+ else if (cur->bridge_stp || cur->bridge_vlan_aware >= 0) is_bridge = true;
+ else if (!strncmp(cur->name, "br", 2) && cur->vlan_id < 0) is_bridge = true;
+ if (is_bridge) {
+ log_info("apply_interfaces: bridge %s ports %s", cur->name, cur->bridge_ports ? cur->bridge_ports : "(none)");
+ rtnl_bridge_create(cur->name);
+ if (cur->bridge_stp) {
+ bool on = !strcasecmp(cur->bridge_stp, "on") || !strcasecmp(cur->bridge_stp, "yes");
+ rtnl_bridge_set_stp(cur->name, on);
+ }
+ if (cur->bridge_vlan_aware >= 0) rtnl_bridge_set_vlan_aware(cur->name, cur->bridge_vlan_aware);
+ if (cur->bridge_ports && strcasecmp(cur->bridge_ports, "none") != 0) {
+ char *ports = strdup(cur->bridge_ports);
+ char *tok = strtok(ports, " \t");
+ while (tok) {
+ if (strcasecmp(tok, "none") == 0) { tok = strtok(NULL, " \t"); continue; }
+ rtnl_bridge_add_port(cur->name, tok);
+ rtnl_link_up(tok);
+ tok = strtok(NULL, " \t");
+ }
+ free(ports);
+ }
+ }
+ log_info("apply_interfaces: link up %s", cur->name);
+ rtnl_link_up(cur->name);
+ for (struct iface_addr *a = cur->addrs; a; a = a->next) {
+ log_info("apply_interfaces: addr %s dev %s", a->address, cur->name);
+ rtnl_addr_add(cur->name, a->address, a->netmask);
+ }
+ if (cur->gateway) {
+ log_info("apply_interfaces: gateway %s via %s", cur->gateway, cur->name);
+ rtnl_route_add_default(cur->gateway, cur->name);
+ }
+ if (cur->mtu) {
+ log_info("apply_interfaces: mtu %d dev %s", cur->mtu, cur->name);
+ rtnl_link_set_mtu(cur->name, cur->mtu);
+ }
+ if (cur->hwaddress) {
+ log_info("apply_interfaces: hwaddress %s dev %s", cur->hwaddress, cur->name);
+ rtnl_link_set_hwaddr(cur->name, cur->hwaddress);
+ }
+ if (cur->post_up) {
+ log_info("apply_interfaces: post-up %s: %s", cur->name, cur->post_up);
+ system(cur->post_up);
+ }
+ }
+ return 0;
+}
+
+// Atomic publish: write tmp + rename so frr never observes a half file.
+static int ready_publish(const char *path) {
+ char tmp[1024];
+ FILE *fd;
+
+ if (!path || !*path) return 0;
+ snprintf(tmp, sizeof(tmp), "%s.tmp.%d", path, (int)getpid());
+ fd = fopen(tmp, "w");
+ if (!fd) {
+ log_error("ready-file %s: %s", tmp, strerror(errno));
+ return -1;
+ }
+ fprintf(fd, "%d\n", (int)getpid());
+ fclose(fd);
+ if (rename(tmp, path) != 0) {
+ log_error("ready-file rename: %s", strerror(errno));
+ unlink(tmp);
+ return -1;
+ }
+ return 0;
+}
+
+static void ready_remove(const char *path) {
+ if (!path || !*path) return;
+ unlink(path);
+}
+
+int main_unosd(int argc, char **argv) {
+ // Cast for argparse which wants const char**
+ const char **c_argv = (const char **)argv;
+ char *config_dir = "/etc/network";
+ char *dataplane = NULL;
+ char *loglevel = "info";
+ char *logfile_path = NULL;
+ char *ready_file = "";
+ int resync_interval = 60;
+ int nl_fd = -1;
+ int rc = 0;
+
+ struct argparse_option options[] = {
+ OPT_HELP(),
+ OPT_STRING('c', "config", &config_dir, "Configuration directory", NULL, 0, 0),
+ OPT_STRING('d', "dataplane", &dataplane, "Force a dataplane backend", NULL, 0, 0),
+ OPT_STRING('v', "verbosity", &loglevel, "log verbosity: fatal,error,warn,info,debug,trace (default: info)", NULL, 0, 0),
+ OPT_STRING(0, "log", &logfile_path, "also write log to file (SIGHUP reopens for logrotate)", NULL, 0, 0),
+ OPT_INTEGER(0, "resync-interval", &resync_interval, "seconds between full resyncs (0 disables timer)", NULL, 0, 0),
+ OPT_STRING(0, "ready-file", &ready_file, "readiness file to publish after initial resync (empty disables)", NULL, 0, 0),
+ OPT_END(),
+ };
+
+ // Initialize components
+ ports_register();
+ ifaces_register();
+ ipc_register();
+
+ struct argparse argparse;
+ argparse_init(&argparse, options, usage, 0);
+ argparse_describe(&argparse, NULL,
+ "\n"
+ __NAME " programs the forwarding plane from the kernel's routing state.\n"
+ "Built for target " __TARGET ".\n"
+ );
+ argc = argparse_parse(&argparse, argc, c_argv);
+
+ int level = LOG_INFO;
+ if (0) {
+ (void)0;
+ } else if (!strcasecmp(loglevel, "trace")) {
+ level = LOG_TRACE;
+ } else if (!strcasecmp(loglevel, "debug")) {
+ level = LOG_DEBUG;
+ } else if (!strcasecmp(loglevel, "info")) {
+ level = LOG_INFO;
+ } else if (!strcasecmp(loglevel, "warn")) {
+ level = LOG_WARN;
+ } else if (!strcasecmp(loglevel, "error")) {
+ level = LOG_ERROR;
+ } else if (!strcasecmp(loglevel, "fatal")) {
+ level = LOG_FATAL;
+ } else {
+ fprintf(stderr, "Unknown log level: %s\n", loglevel);
+ return 1;
+ }
+ log_set_level(level);
+ setvbuf(stderr, NULL, _IOLBF, 0);
+
+ log_file = NULL;
+ log_path = NULL;
+ sighup_received = 0;
+
+ if (logfile_path && logfile_path[0]) {
+ log_path = strdup(logfile_path);
+ log_file = fopen(log_path, "a");
+ if (log_file) {
+ log_add_callback(logfile_callback, log_path, level);
+ } else {
+ fprintf(stderr, "Could not open log file: %s\n", logfile_path);
+ free(log_path);
+ log_path = NULL;
+ }
+ }
+
+ if (resync_interval < 0) {
+ log_error("--resync-interval must be >= 0");
+ return 1;
+ }
+
+ signal(SIGHUP, sighup_handler);
+ signal(SIGINT, stop_handler);
+ signal(SIGTERM, stop_handler);
+ signal(SIGPIPE, SIG_IGN);
+
+ if (load_ports(config_dir) < 0) {
+ return 1;
+ }
+ if (load_interfaces(config_dir) < 0) {
+ return 1;
+ }
+ {
+ int n = 0;
+ for (struct unos_iface *c = ifaces_list(); c; c = c->next) n++;
+ log_info("main: loaded %d interfaces from %s", n, config_dir);
+ for (struct unos_iface *c = ifaces_list(); c; c = c->next) {
+ log_info("main: iface %s method %s auto %d addrs %s", c->name, c->method ? c->method : "(null)", c->auto_flag, c->addrs ? c->addrs->address : "(none)");
+ }
+ }
+
+ // Startup order is load-bearing: ports -> backend select -> init ->
+ // port_apply -> nl_open -> initial resync -> ready -> event loop. frr must
+ // not start before the ready file exists or zebra misses the swpN netdevs.
+ //
+ // The built-in kernel backend is registered first so it acts as the
+ // fallback; plugins installed by hardware packages are probed ahead of it.
+ dp_register(dp_kernel_ops());
+ dp_plugins_load(UNOS_DATAPLANE_DIR);
+
+ if (dp_select(dataplane) != DP_RET_OK) {
+ return 1;
+ }
+
+ if (dp_init() != DP_RET_OK) {
+ log_error("dataplane: initialisation failed");
+ return 1;
+ }
+
+ if (ipc_init() != 0) {
+ log_warn("ipc: init failed, ifup/ifquery will not be available");
+ }
+
+ apply_ports();
+ // Interfaces are applied via netlink/system ip after ports, before resync
+ // so that the subsequent nl_resync sees them.
+
+ nl_fd = nl_open();
+ if (nl_fd < 0) {
+ dp_fini();
+ ports_free();
+ ifaces_free();
+ ipc_fini();
+ return 1;
+ }
+
+ // Apply auto interfaces (needs netlink to be open for link creation, but we use system ip for now)
+ apply_interfaces();
+
+ // Fail loud: never publish readiness on partial state; runit retries.
+ if (nl_resync(nl_fd) != 0) {
+ log_fatal("netlink: initial resync failed");
+ close(nl_fd);
+ ipc_fini();
+ dp_fini();
+ ports_free();
+ ifaces_free();
+ return 1;
+ }
+
+ if (ready_file && *ready_file) {
+ if (ready_publish(ready_file) != 0) {
+ close(nl_fd);
+ ipc_fini();
+ dp_fini();
+ ports_free();
+ ifaces_free();
+ return 1;
+ }
+ log_info("ready: published %s", ready_file);
+ }
+
+ rc = nl_run(nl_fd, resync_interval);
+ close(nl_fd);
+ ipc_fini();
+
+ ready_remove(ready_file);
+ dp_fini();
+ ports_free();
+ ifaces_free();
+
+ if (log_file) fclose(log_file);
+ free(log_path);
+
+ return rc < 0 ? 1 : 0;
+}
+
+__attribute__((constructor))
+static void register_unosd(void) {
+ cli_register("unosd", main_unosd);
+ cli_register("unos", main_unosd);
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/config/ifaces.c b/src/config/ifaces.c
@@ -60,6 +60,8 @@ void ifaces_free(void) {
free(cur->post_up);
free(cur->pre_down);
free(cur->post_down);
+ free(cur->bridge_ports);
+ free(cur->bridge_stp);
for (a = cur->addrs; a; a = anext) {
anext = a->next;
free(a->address);
@@ -315,6 +317,29 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void
free(iface->pre_down);
iface->pre_down = cmd;
}
+ else if (!strcasecmp("bridge-ports", dir->name) || !strcasecmp("bridge_ports", dir->name)) {
+ if (dir->argc < 1) { fprintf(stderr, "`bridge-ports` expects at least 1 arg\n"); exit(1); }
+ size_t len = 0;
+ for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1;
+ char *cmd = calloc(1, len+1);
+ for (int i=0;i<(int)dir->argc;i++) {
+ strcat(cmd, dir->argv[i]);
+ if (i+1 < (int)dir->argc) strcat(cmd, " ");
+ }
+ free(iface->bridge_ports);
+ iface->bridge_ports = cmd;
+ }
+ else if (!strcasecmp("bridge-stp", dir->name) || !strcasecmp("bridge_stp", dir->name)) {
+ if (dir->argc != 1) { fprintf(stderr, "`bridge-stp` expects 1 arg (on/off)\n"); exit(1); }
+ free(iface->bridge_stp);
+ iface->bridge_stp = strdup(dir->argv[0]);
+ }
+ else if (!strcasecmp("bridge-vlan-aware", dir->name) || !strcasecmp("bridge_vlan_aware", dir->name)) {
+ if (dir->argc != 1) { fprintf(stderr, "`bridge-vlan-aware` expects 1 arg (yes/no)\n"); exit(1); }
+ if (!strcasecmp(dir->argv[0], "yes") || !strcasecmp(dir->argv[0], "on") || !strcasecmp(dir->argv[0], "1")) iface->bridge_vlan_aware = 1;
+ else if (!strcasecmp(dir->argv[0], "no") || !strcasecmp(dir->argv[0], "off") || !strcasecmp(dir->argv[0], "0")) iface->bridge_vlan_aware = 0;
+ else { fprintf(stderr, "`bridge-vlan-aware` expects yes/no\n"); exit(1); }
+ }
else {
break;
}
diff --git a/src/config/ifaces.h b/src/config/ifaces.h
@@ -30,6 +30,9 @@ struct unos_iface {
char *post_up;
char *pre_down;
char *post_down;
+ char *bridge_ports; // space-separated, e.g. "swp1 swp2"
+ char *bridge_stp; // "on"/"off"
+ int bridge_vlan_aware; // -1 unset, 0 no, 1 yes
int auto_flag; // 1 if listed in auto
struct unos_iface *next;
};
diff --git a/src/ipc.c b/src/ipc.c
@@ -1,5 +1,6 @@
#define _GNU_SOURCE
#include <errno.h>
+#include <net/if.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -14,8 +15,10 @@
#include "util/config.h"
#include "config/ifaces.h"
+#include "config/ports.h"
#include "ipc.h"
#include "netlink/netlink.h"
+#include "netlink/rtnl.h"
static int ipc_sock = -1;
@@ -173,7 +176,6 @@ static struct cnf_directive *cfg_handle_ifquery(FILE *fd, struct cnf_directive *
uid_t ipc_current_uid = 0;
int ipc_handle(void) {
- log_info("ipc: handle called");
int cfd;
struct sockaddr_un addr;
socklen_t alen = sizeof(addr);
@@ -183,13 +185,29 @@ int ipc_handle(void) {
FILE *fout = NULL;
int fd_dup;
- cfd = accept4(ipc_sock, (struct sockaddr*)&addr, &alen, SOCK_CLOEXEC | SOCK_NONBLOCK);
+ // The LISTENING socket is non-blocking (it lives in the nl_run poll set), but
+ // the ACCEPTED connection must be blocking: the client connects and writes its
+ // command as two separate syscalls, so a non-blocking read here returns EAGAIN
+ // whenever the bytes have not landed yet. stdio maps that to EOF, cfg_parse
+ // sees an empty stream, no handler runs and the client gets an empty reply.
+ // That is a race with no upper bound in wall-clock terms - it is not fixable
+ // by sleeping in the caller, only by blocking here.
+ cfd = accept4(ipc_sock, (struct sockaddr*)&addr, &alen, SOCK_CLOEXEC);
if (cfd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
log_error("ipc: accept failed: %s", strerror(errno));
return -1;
}
+ // Blocking, but never indefinitely: unosd is single-threaded, so a client that
+ // connects and then stalls would otherwise wedge the whole daemon (netlink
+ // included). A short timeout bounds that without reintroducing the race.
+ {
+ struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
+ setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+ setsockopt(cfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
+ }
+
// Get peer credentials
if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &clen) == 0) {
ipc_current_uid = cred.uid;
@@ -263,10 +281,7 @@ static int handle_ifup(FILE *out, const char *ifname, uid_t uid) {
struct unos_iface *iface = ifaces_find(ifname);
if (!iface) {
log_info("ipc: ifup %s (not in interfaces, just bringing link up)", ifname);
- char cmd[512];
- snprintf(cmd, sizeof(cmd), "ip link set %s up 2>&1", ifname);
- int rc = system(cmd);
- if (rc != 0) {
+ if (rtnl_link_up(ifname) != 0) {
fprintf(out, "ERR ifup %s failed\n", ifname);
return -1;
}
@@ -277,42 +292,79 @@ static int handle_ifup(FILE *out, const char *ifname, uid_t uid) {
fprintf(out, "ERR pre-up hook failed for %s\n", ifname);
return -1;
}
- // VLAN handling: if iface is vlan, ensure raw device exists and create vlan link
- // For now, just bring link up and add addresses via netlink helpers (to be implemented)
- // Placeholder: use system ip commands via netlink abstraction
- // TODO: wire real netlink RTM_NEWLINK/NEWADDR
- char cmd[512];
- int rc;
+ // VLAN creation first (needs rawdev)
+ if (iface->vlan_id >= 0) {
+ const char *raw = iface->vlan_raw_device;
+ char tmp[IF_NAMESIZE];
+ if (!raw) {
+ char *dot = strrchr(iface->name, '.');
+ if (dot) {
+ size_t len = dot - iface->name;
+ if (len < sizeof(tmp)) {
+ memcpy(tmp, iface->name, len);
+ tmp[len] = '\0';
+ raw = tmp;
+ }
+ }
+ }
+ if (raw) {
+ log_info("ipc: ifup %s vlan %d on %s", ifname, iface->vlan_id, raw);
+ rtnl_vlan_create(ifname, raw, iface->vlan_id);
+ }
+ }
+ // Bridge creation -- explicit bridge-ports (including "none") or br* without vlan
+ bool is_bridge = false;
+ if (iface->bridge_ports) is_bridge = true;
+ else if (iface->bridge_stp || iface->bridge_vlan_aware >= 0) is_bridge = true;
+ else if (!strncmp(iface->name, "br", 2) && iface->vlan_id < 0) is_bridge = true;
+ if (is_bridge) {
+ log_info("ipc: ifup %s bridge (ports %s)", ifname, iface->bridge_ports ? iface->bridge_ports : "(none)");
+ rtnl_bridge_create(ifname);
+ if (iface->bridge_stp) {
+ bool on = !strcasecmp(iface->bridge_stp, "on") || !strcasecmp(iface->bridge_stp, "yes");
+ rtnl_bridge_set_stp(ifname, on);
+ }
+ if (iface->bridge_vlan_aware >= 0) {
+ rtnl_bridge_set_vlan_aware(ifname, iface->bridge_vlan_aware);
+ }
+ if (iface->bridge_ports && strcasecmp(iface->bridge_ports, "none") != 0) {
+ char *ports = strdup(iface->bridge_ports);
+ char *tok = strtok(ports, " \t");
+ while (tok) {
+ if (strcasecmp(tok, "none") == 0) { tok = strtok(NULL, " \t"); continue; }
+ log_info("ipc: ifup %s bridge add port %s", ifname, tok);
+ rtnl_bridge_add_port(ifname, tok);
+ rtnl_link_up(tok);
+ tok = strtok(NULL, " \t");
+ }
+ free(ports);
+ }
+ }
// Bring link up
- snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s up", ifname);
- rc = system(cmd);
- log_info("ipc: ifup %s link up rc=%d cmd='%s'", ifname, rc, cmd);
- if (rc != 0) { fprintf(out, "ERR ifup %s: link up failed (%d)\n", ifname, rc); return -1; }
- // Add addresses
- struct iface_addr *a;
- for (a = iface->addrs; a; a = a->next) {
- snprintf(cmd, sizeof(cmd), "/sbin/ip addr add %s dev %s 2>&1", a->address, ifname);
- rc = system(cmd);
- log_info("ipc: ifup %s addr %s rc=%d", ifname, a->address, rc);
- // File exists is not fatal (addr already present)
- if (rc != 0 && rc != 256) { /* 256 = File exists from ip */ }
+ if (rtnl_link_up(ifname) != 0) {
+ fprintf(out, "ERR ifup %s: link up failed\n", ifname);
+ return -1;
+ }
+ // Addresses
+ for (struct iface_addr *a = iface->addrs; a; a = a->next) {
+ log_info("ipc: ifup %s addr %s", ifname, a->address);
+ rtnl_addr_add(ifname, a->address, a->netmask);
}
if (iface->gateway) {
- snprintf(cmd, sizeof(cmd), "/sbin/ip route add default via %s 2>&1 || true", iface->gateway);
- rc = system(cmd);
- log_info("ipc: ifup %s gateway %s rc=%d", ifname, iface->gateway, rc);
+ log_info("ipc: ifup %s gateway %s", ifname, iface->gateway);
+ rtnl_route_add_default(iface->gateway, ifname);
}
if (iface->mtu) {
- snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s mtu %d", ifname, iface->mtu);
- rc = system(cmd);
- log_info("ipc: ifup %s mtu %d rc=%d", ifname, iface->mtu, rc);
- if (rc != 0) { fprintf(out, "ERR ifup %s: mtu failed\n", ifname); return -1; }
+ if (rtnl_link_set_mtu(ifname, iface->mtu) != 0) {
+ fprintf(out, "ERR ifup %s: mtu failed\n", ifname);
+ return -1;
+ }
}
if (iface->hwaddress) {
- snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s address %s", ifname, iface->hwaddress);
- rc = system(cmd);
- log_info("ipc: ifup %s hwaddress %s rc=%d", ifname, iface->hwaddress, rc);
- if (rc != 0) { fprintf(out, "ERR ifup %s: hwaddress failed\n", ifname); return -1; }
+ if (rtnl_link_set_hwaddr(ifname, iface->hwaddress) != 0) {
+ fprintf(out, "ERR ifup %s: hwaddress failed\n", ifname);
+ return -1;
+ }
}
if (iface->post_up && run_hook(iface->post_up) != 0) {
fprintf(out, "ERR post-up hook failed for %s\n", ifname);
@@ -326,10 +378,7 @@ static int handle_ifdown(FILE *out, const char *ifname, uid_t uid) {
struct unos_iface *iface = ifaces_find(ifname);
if (!iface) {
log_info("ipc: ifdown %s (not in interfaces, just bringing link down)", ifname);
- char cmd[512];
- snprintf(cmd, sizeof(cmd), "ip link set %s down 2>&1", ifname);
- int rc = system(cmd);
- if (rc != 0) {
+ if (rtnl_link_down(ifname) != 0) {
fprintf(out, "ERR ifdown %s failed\n", ifname);
return -1;
}
@@ -340,12 +389,10 @@ static int handle_ifdown(FILE *out, const char *ifname, uid_t uid) {
fprintf(out, "ERR pre-down hook failed for %s\n", ifname);
return -1;
}
- char cmd[512];
- int rc2;
- snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s down", ifname);
- rc2 = system(cmd);
- log_info("ipc: ifdown %s rc=%d", ifname, rc2);
- if (rc2 != 0) { fprintf(out, "ERR ifdown %s failed (%d)\n", ifname, rc2); return -1; }
+ if (rtnl_link_down(ifname) != 0) {
+ fprintf(out, "ERR ifdown %s failed\n", ifname);
+ return -1;
+ }
// Flush addresses (keep for now)
if (iface->post_down && run_hook(iface->post_down) != 0) {
fprintf(out, "ERR post-down hook failed for %s\n", ifname);
@@ -356,12 +403,18 @@ static int handle_ifdown(FILE *out, const char *ifname, uid_t uid) {
static int handle_ifreload(FILE *out, uid_t uid) {
(void)uid;
- log_info("ipc: ifreload (diff-apply)");
+ log_info("ipc: ifreload (diff-apply) start, old list has %d", ({
+ int n=0; for (struct unos_iface *c=ifaces_list();c;c=c->next) n++; n;
+ }));
// For now, just re-parse and apply all auto interfaces
// TODO: implement diff-apply: compare old vs new lists
// Placeholder: reload config files
- // We need to free old and re-parse
+ // We need to free old and re-parse (both ifaces and ports)
ifaces_free();
+ ports_free();
+ log_info("ipc: ifreload after free, list has %d", ({
+ int n=0; for (struct unos_iface *c=ifaces_list();c;c=c->next) n++; n;
+ }));
// Re-register is idempotent, but we need to reload
// Use same logic as main.c load_interfaces
char *dir = "/etc/network";
@@ -399,6 +452,14 @@ static int handle_ifreload(FILE *out, uid_t uid) {
fclose(fd);
}
}
+ {
+ int n = 0;
+ for (struct unos_iface *c = ifaces_list(); c; c = c->next) n++;
+ log_info("ipc: ifreload done, new list has %d", n);
+ for (struct unos_iface *c = ifaces_list(); c; c = c->next) {
+ log_info("ipc: ifreload iface %s", c->name);
+ }
+ }
return 0;
}
diff --git a/src/main.c b/src/main.c
@@ -1,431 +1,60 @@
#define _GNU_SOURCE
-
-#include <signal.h>
-#include <errno.h>
+#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include <strings.h>
-#include <unistd.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#include "cofyc/argparse.h"
-#include "rxi/log.h"
-
-#include "config/ports.h"
-#include "config/ifaces.h"
-#include "dataplane.h"
-#include "dataplane/registry.h"
-#include "ipc.h"
-#include "netlink/netlink.h"
-#include "util/config.h"
-
-static const char *const usage[] = {
- __NAME " [options]",
- __NAME " --help",
- NULL,
-};
-
-static FILE *log_file;
-static char *log_path;
-static volatile sig_atomic_t sighup_received;
-
-static void logfile_callback(log_Event *ev) {
- if (sighup_received) {
- sighup_received = 0;
- if (log_path && log_file) {
- fclose(log_file);
- log_file = fopen(log_path, "a");
- }
- }
- if (log_file) {
- char buf[64];
- buf[strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", ev->time)] = '\0';
- fprintf(log_file, "%s %-5s %s:%d: ", buf, log_level_string(ev->level), ev->file, ev->line);
- vfprintf(log_file, ev->fmt, ev->ap);
- fprintf(log_file, "\n");
- fflush(log_file);
- }
-}
-
-static void sighup_handler(int sig) {
- (void)sig;
- sighup_received = 1;
-}
-
-static void stop_handler(int sig) {
- (void)sig;
- nl_request_stop();
-}
-
-// Port and interface configuration live in either:
-// <dir>/ports, <dir>/interfaces single files, or
-// <dir>/ports.d/*.cnf, <dir>/interfaces.d/*.cnf drop-ins
-//
-// Rather than growing the config API, the drop-in case is handed to the parser
-// as an in-memory config consisting of `source` -- the parser expands it.
-static int load_ports(const char *dir) {
- char path[1024];
- char stub[1024];
- FILE *fd;
- int rc;
-
- snprintf(path, sizeof(path), "%s/ports", dir);
-
- if (access(path, R_OK) == 0) {
- fd = fopen(path, "r");
- if (!fd) {
- perror("Could not open config file");
- return CFG_RET_ERROR;
- }
- } else {
- snprintf(stub, sizeof(stub), "source ports.d/*.cnf\n");
- fd = fmemopen(stub, strlen(stub), "r");
- if (!fd) {
- perror("Could not open in-memory config");
- return CFG_RET_ERROR;
- }
- }
-
- rc = cfg_parse(cfg_ns_get("ports"), dir, fd, NULL);
- if (rc < 0) {
- log_error("Error during reading port configuration from %s", dir);
- }
-
- fclose(fd);
- return rc;
-}
-
-static int load_interfaces(const char *dir) {
- char path[1024];
- char stub[1024];
- FILE *fd;
- int rc;
-
- snprintf(path, sizeof(path), "%s/interfaces", dir);
-
- if (access(path, R_OK) == 0) {
- fd = fopen(path, "r");
- if (!fd) {
- perror("Could not open config file");
- return CFG_RET_ERROR;
- }
- } else {
- snprintf(stub, sizeof(stub), "source interfaces.d/*.cnf\n");
- fd = fmemopen(stub, strlen(stub), "r");
- if (!fd) {
- perror("Could not open in-memory config");
- return CFG_RET_ERROR;
- }
- }
-
- rc = cfg_parse(cfg_ns_get("interfaces"), dir, fd, NULL);
- if (rc < 0) {
- log_error("Error during reading interfaces configuration from %s", dir);
- }
-
- fclose(fd);
- return rc;
-}
-
-static int apply_ports(void) {
- const struct dp_ops *dp = dp_active();
- struct unos_port *cur;
- struct dp_port port;
-
- if (!dp || !dp->port_apply) return 0;
-
- for ( cur = ports_list() ; cur ; cur = cur->next ) {
- memset(&port, 0, sizeof(port));
- port.name = cur->name;
- port.speed = cur->speed;
- port.fec = cur->fec;
- port.autoneg = cur->autoneg;
-
- if (dp->port_apply(&port) != DP_RET_OK) {
- log_warn("%s: failed to apply port configuration", cur->name);
- }
- }
-
- return 0;
-}
-
-static int apply_interfaces(void) {
- struct unos_iface *cur;
- for (cur = ifaces_list(); cur; cur = cur->next) {
- if (!cur->auto_flag) continue;
- // For now, use ipc's handle logic via system ip -- will be replaced with netlink
- // We call the same helper as ipc ifup would, but directly
- char cmd[512];
- // pre-up hook
- if (cur->pre_up) {
- log_info("apply_interfaces: pre-up %s: %s", cur->name, cur->pre_up);
- if (system(cur->pre_up) != 0) {
- log_warn("pre-up for %s failed", cur->name);
- continue;
- }
- }
- // VLAN creation if needed (dot notation)
- if (cur->vlan_id >= 0) {
- const char *raw = cur->vlan_raw_device;
- if (!raw) {
- // Infer from dot notation
- char *dot = strrchr(cur->name, '.');
- if (dot) {
- size_t len = dot - cur->name;
- char *tmp = malloc(len+1);
- strncpy(tmp, cur->name, len);
- tmp[len] = '\0';
- raw = tmp;
- snprintf(cmd, sizeof(cmd), "/sbin/ip link add link %s name %s type vlan id %d 2>&1 || true", raw, cur->name, cur->vlan_id);
- log_info("apply_interfaces: %s", cmd);
- system(cmd);
- free(tmp);
- }
- } else {
- snprintf(cmd, sizeof(cmd), "/sbin/ip link add link %s name %s type vlan id %d 2>&1 || true", raw, cur->name, cur->vlan_id);
- log_info("apply_interfaces: %s", cmd);
- system(cmd);
- }
- }
- // Bring link up
- snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s up 2>&1 || true", cur->name);
- log_info("apply_interfaces: %s", cmd);
- system(cmd);
- // Add addresses (v4 and v6)
- struct iface_addr *a;
- for (a = cur->addrs; a; a = a->next) {
- snprintf(cmd, sizeof(cmd), "/sbin/ip addr add %s dev %s 2>&1 || true", a->address, cur->name);
- log_info("apply_interfaces: %s", cmd);
- system(cmd);
- }
- if (cur->gateway) {
- snprintf(cmd, sizeof(cmd), "/sbin/ip route add default via %s 2>&1 || true", cur->gateway);
- log_info("apply_interfaces: %s", cmd);
- system(cmd);
- }
- if (cur->mtu) {
- snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s mtu %d 2>&1 || true", cur->name, cur->mtu);
- log_info("apply_interfaces: %s", cmd);
- system(cmd);
- }
- if (cur->hwaddress) {
- snprintf(cmd, sizeof(cmd), "/sbin/ip link set %s address %s 2>&1 || true", cur->name, cur->hwaddress);
- log_info("apply_interfaces: %s", cmd);
- system(cmd);
- }
- if (cur->post_up) {
- log_info("apply_interfaces: post-up %s: %s", cur->name, cur->post_up);
- system(cur->post_up);
- }
- }
- return 0;
-}
-
-// Atomic publish: write tmp + rename so frr never observes a half file.
-static int ready_publish(const char *path) {
- char tmp[1024];
- FILE *fd;
-
- if (!path || !*path) return 0;
- snprintf(tmp, sizeof(tmp), "%s.tmp.%d", path, (int)getpid());
- fd = fopen(tmp, "w");
- if (!fd) {
- log_error("ready-file %s: %s", tmp, strerror(errno));
- return -1;
- }
- fprintf(fd, "%d\n", (int)getpid());
- fclose(fd);
- if (rename(tmp, path) != 0) {
- log_error("ready-file rename: %s", strerror(errno));
- unlink(tmp);
- return -1;
- }
- return 0;
-}
-
-static void ready_remove(const char *path) {
- if (!path || !*path) return;
- unlink(path);
-}
-
-int main(int argc, const char **argv) {
- char *config_dir = "/etc/network";
- char *dataplane = NULL;
- char *loglevel = "info";
- char *logfile_path = NULL;
- char *ready_file = "";
- int resync_interval = 60;
- int nl_fd = -1;
- int rc = 0;
-
- struct argparse_option options[] = {
- OPT_HELP(),
- OPT_STRING('c', "config", &config_dir, "Configuration directory", NULL, 0, 0),
- OPT_STRING('d', "dataplane", &dataplane, "Force a dataplane backend", NULL, 0, 0),
- OPT_STRING('v', "verbosity", &loglevel, "log verbosity: fatal,error,warn,info,debug,trace (default: info)", NULL, 0, 0),
- OPT_STRING(0, "log", &logfile_path, "also write log to file (SIGHUP reopens for logrotate)", NULL, 0, 0),
- OPT_INTEGER(0, "resync-interval", &resync_interval, "seconds between full resyncs (0 disables timer)", NULL, 0, 0),
- OPT_STRING(0, "ready-file", &ready_file, "readiness file to publish after initial resync (empty disables)", NULL, 0, 0),
- OPT_END(),
- };
-
- // Initialize components
- ports_register();
- ifaces_register();
- ipc_register();
-
- struct argparse argparse;
- argparse_init(&argparse, options, usage, 0);
- argparse_describe(&argparse, NULL,
- "\n"
- __NAME " programs the forwarding plane from the kernel's routing state.\n"
- "Built for target " __TARGET ".\n"
- );
- argc = argparse_parse(&argparse, argc, argv);
-
- int level = LOG_INFO;
- if (0) {
- (void)0;
- } else if (!strcasecmp(loglevel, "trace")) {
- level = LOG_TRACE;
- } else if (!strcasecmp(loglevel, "debug")) {
- level = LOG_DEBUG;
- } else if (!strcasecmp(loglevel, "info")) {
- level = LOG_INFO;
- } else if (!strcasecmp(loglevel, "warn")) {
- level = LOG_WARN;
- } else if (!strcasecmp(loglevel, "error")) {
- level = LOG_ERROR;
- } else if (!strcasecmp(loglevel, "fatal")) {
- level = LOG_FATAL;
- } else {
- fprintf(stderr, "Unknown log level: %s\n", loglevel);
- return 1;
- }
- log_set_level(level);
- setvbuf(stderr, NULL, _IOLBF, 0);
-
- log_file = NULL;
- log_path = NULL;
- sighup_received = 0;
-
- if (logfile_path && logfile_path[0]) {
- log_path = strdup(logfile_path);
- log_file = fopen(log_path, "a");
- if (log_file) {
- log_add_callback(logfile_callback, log_path, level);
- } else {
- fprintf(stderr, "Could not open log file: %s\n", logfile_path);
- free(log_path);
- log_path = NULL;
- }
- }
-
- if (resync_interval < 0) {
- log_error("--resync-interval must be >= 0");
- return 1;
- }
- signal(SIGHUP, sighup_handler);
- signal(SIGINT, stop_handler);
- signal(SIGTERM, stop_handler);
- signal(SIGPIPE, SIG_IGN);
-
- if (load_ports(config_dir) < 0) {
- return 1;
- }
- if (load_interfaces(config_dir) < 0) {
- return 1;
- }
- {
- int n = 0;
- for (struct unos_iface *c = ifaces_list(); c; c = c->next) n++;
- log_info("main: loaded %d interfaces from %s", n, config_dir);
- for (struct unos_iface *c = ifaces_list(); c; c = c->next) {
- log_info("main: iface %s method %s auto %d addrs %s", c->name, c->method ? c->method : "(null)", c->auto_flag, c->addrs ? c->addrs->address : "(none)");
+#include "cli/registry.h"
+
+int main(int argc, char *argv[]) {
+ const char *prog = basename(argv[0]);
+ // Strip path, handle multicall: argv[0] determines command
+ // Also handle "unosc <cmd>" as before: if prog is unosc and argc>1, dispatch to <cmd>
+ if (!strcmp(prog, "unosc") && argc > 1) {
+ prog = argv[1];
+ // Shift argv for the target main: make argv[0] be prog, argc-1
+ // We need to adjust argv for the called main: it expects argv[0] to be the command name
+ // Create a new argv array with prog as argv[0] and the rest shifted
+ char **new_argv = malloc(sizeof(char*) * argc);
+ new_argv[0] = (char*)prog;
+ for (int i = 2; i < argc; i++) new_argv[i-1] = argv[i];
+ int new_argc = argc - 1;
+ cli_main_fn fn = cli_find(prog);
+ if (!fn) {
+ fprintf(stderr, "unosc: unknown command: %s\n", prog);
+ fprintf(stderr, "available: ifup, ifdown, ifquery, ifreload, unosd, unosc\n");
+ free(new_argv);
+ return 1;
}
+ int rc = fn(new_argc, new_argv);
+ free(new_argv);
+ return rc;
}
- // Startup order is load-bearing: ports -> backend select -> init ->
- // port_apply -> nl_open -> initial resync -> ready -> event loop. frr must
- // not start before the ready file exists or zebra misses the swpN netdevs.
- //
- // The built-in kernel backend is registered first so it acts as the
- // fallback; plugins installed by hardware packages are probed ahead of it.
- dp_register(dp_kernel_ops());
- dp_plugins_load(UNOS_DATAPLANE_DIR);
-
- if (dp_select(dataplane) != DP_RET_OK) {
- return 1;
- }
-
- if (dp_init() != DP_RET_OK) {
- log_error("dataplane: initialisation failed");
- return 1;
- }
-
- if (ipc_init() != 0) {
- log_warn("ipc: init failed, ifup/ifquery will not be available");
+ cli_main_fn fn = cli_find(prog);
+ if (fn) {
+ return fn(argc, argv);
}
- apply_ports();
- // Interfaces are applied via netlink/system ip after ports, before resync
- // so that the subsequent nl_resync sees them.
-
- nl_fd = nl_open();
- if (nl_fd < 0) {
- dp_fini();
- ports_free();
- ifaces_free();
- ipc_fini();
- return 1;
- }
-
- // Apply auto interfaces (needs netlink to be open for link creation, but we use system ip for now)
- apply_interfaces();
-
- // Fail loud: never publish readiness on partial state; runit retries.
- if (nl_resync(nl_fd) != 0) {
- log_fatal("netlink: initial resync failed");
- close(nl_fd);
- ipc_fini();
- dp_fini();
- ports_free();
- ifaces_free();
- return 1;
- }
-
- if (ready_file && *ready_file) {
- if (ready_publish(ready_file) != 0) {
- close(nl_fd);
- ipc_fini();
- dp_fini();
- ports_free();
- ifaces_free();
- return 1;
+ // Also handle case where binary is called as "unosd" but we want to allow "unos" as alias,
+ // and also handle being called as "unosc" without extra dispatch above already handled.
+ // If not found, try to find "unosd" as default for bare invocation with --help etc.?
+ // For backwards compat, if prog is not found and argc>1 and first arg is a known command, dispatch to it
+ if (argc > 1) {
+ fn = cli_find(argv[1]);
+ if (fn) {
+ // Shift as above
+ char **new_argv = malloc(sizeof(char*) * argc);
+ new_argv[0] = argv[1];
+ for (int i = 2; i < argc; i++) new_argv[i-1] = argv[i];
+ int new_argc = argc - 1;
+ int rc = fn(new_argc, new_argv);
+ free(new_argv);
+ return rc;
}
- log_info("ready: published %s", ready_file);
}
- rc = nl_run(nl_fd, resync_interval);
- close(nl_fd);
- ipc_fini();
-
- ready_remove(ready_file);
- dp_fini();
- ports_free();
- ifaces_free();
-
- if (log_file) fclose(log_file);
- free(log_path);
-
- return rc < 0 ? 1 : 0;
+ fprintf(stderr, "%s: unknown command: %s\n", argv[0], prog);
+ fprintf(stderr, "available commands: unosd, unosc, ifup, ifdown, ifquery, ifreload\n");
+ return 1;
}
-
-#ifdef __cplusplus
-} // extern "C"
-#endif
diff --git a/src/netlink/rtnl.c b/src/netlink/rtnl.c
@@ -0,0 +1,499 @@
+#define _GNU_SOURCE
+#include <arpa/inet.h>
+#include <errno.h>
+#include <linux/if_link.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <net/if.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include "rxi/log.h"
+
+#include "rtnl.h"
+
+#define NL_BUFSZ 8192
+
+static int rtnl_open(void) {
+ int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+ if (fd < 0) {
+ log_error("rtnl: socket failed: %s", strerror(errno));
+ return -1;
+ }
+ struct sockaddr_nl addr = { .nl_family = AF_NETLINK };
+ if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
+ log_error("rtnl: bind failed: %s", strerror(errno));
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+static int rtnl_talk(struct nlmsghdr *nh) {
+ int fd = rtnl_open();
+ if (fd < 0) return -1;
+
+ struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
+ struct iovec iov = { .iov_base = nh, .iov_len = nh->nlmsg_len };
+ struct msghdr msg = { .msg_name = &nladdr, .msg_namelen = sizeof(nladdr), .msg_iov = &iov, .msg_iovlen = 1 };
+
+ if (sendmsg(fd, &msg, 0) < 0) {
+ log_error("rtnl: sendmsg failed: %s", strerror(errno));
+ close(fd);
+ return -1;
+ }
+
+ // Wait for ACK (NLMSGERR with error 0) or NLMSG_DONE
+ char buf[NL_BUFSZ];
+ struct pollfd pfd = { .fd = fd, .events = POLLIN };
+ if (poll(&pfd, 1, 5000) <= 0) {
+ log_error("rtnl: poll timeout");
+ close(fd);
+ return -1;
+ }
+ ssize_t len = recv(fd, buf, sizeof(buf), 0);
+ if (len < 0) {
+ log_error("rtnl: recv failed: %s", strerror(errno));
+ close(fd);
+ return -1;
+ }
+ struct nlmsghdr *h = (struct nlmsghdr*)buf;
+ while (NLMSG_OK(h, len)) {
+ if (h->nlmsg_type == NLMSG_ERROR) {
+ struct nlmsgerr *err = NLMSG_DATA(h);
+ if (err->error == 0) {
+ close(fd);
+ return 0; // ACK
+ } else {
+ // EEXIST for addr add is not fatal for ifup idempotence -- treat as success
+ if (err->error == -EEXIST) {
+ log_debug("rtnl: NLMSGERR EEXIST (already exists, treating as success)");
+ close(fd);
+ return 0;
+ }
+ log_error("rtnl: NLMSGERR %s", strerror(-err->error));
+ close(fd);
+ return -1;
+ }
+ }
+ h = NLMSG_NEXT(h, len);
+ }
+ close(fd);
+ return 0;
+}
+
+static int addattr_l(struct nlmsghdr *n, int maxlen, int type, const void *data, int alen) {
+ int len = RTA_LENGTH(alen);
+ struct rtattr *rta;
+ if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len) > (unsigned)maxlen) {
+ log_error("rtnl: addattr_l overflow");
+ return -1;
+ }
+ rta = (struct rtattr*)(((char*)n) + NLMSG_ALIGN(n->nlmsg_len));
+ rta->rta_type = type;
+ rta->rta_len = len;
+ if (alen) memcpy(RTA_DATA(rta), data, alen);
+ n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len);
+ return 0;
+}
+
+static int addattr_nested_start(struct nlmsghdr *n, int maxlen, int type) {
+ struct rtattr *rta = (struct rtattr*)(((char*)n) + NLMSG_ALIGN(n->nlmsg_len));
+ rta->rta_type = type;
+ rta->rta_len = RTA_LENGTH(0);
+ n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(sizeof(*rta));
+ return (char*)rta - (char*)n;
+}
+
+static void addattr_nested_end(struct nlmsghdr *n, int start) {
+ struct rtattr *rta = (struct rtattr*)((char*)n + start);
+ rta->rta_len = (char*)n + NLMSG_ALIGN(n->nlmsg_len) - (char*)rta;
+}
+
+int rtnl_link_up(const char *ifname) {
+ unsigned ifindex = if_nametoindex(ifname);
+ if (!ifindex) {
+ log_error("rtnl: link_up %s: if_nametoindex failed: %s", ifname, strerror(errno));
+ return -1;
+ }
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifinfomsg *ifi;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
+ nh->nlmsg_type = RTM_NEWLINK;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ nh->nlmsg_seq = 0;
+ ifi = NLMSG_DATA(nh);
+ ifi->ifi_family = AF_UNSPEC;
+ ifi->ifi_index = ifindex;
+ ifi->ifi_change = IFF_UP;
+ ifi->ifi_flags = IFF_UP;
+ return rtnl_talk(nh);
+}
+
+int rtnl_link_down(const char *ifname) {
+ unsigned ifindex = if_nametoindex(ifname);
+ if (!ifindex) {
+ log_error("rtnl: link_down %s: if_nametoindex failed", ifname);
+ return -1;
+ }
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifinfomsg *ifi;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
+ nh->nlmsg_type = RTM_NEWLINK;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ ifi = NLMSG_DATA(nh);
+ ifi->ifi_family = AF_UNSPEC;
+ ifi->ifi_index = ifindex;
+ ifi->ifi_change = IFF_UP;
+ ifi->ifi_flags = 0;
+ return rtnl_talk(nh);
+}
+
+int rtnl_link_set_mtu(const char *ifname, int mtu) {
+ unsigned ifindex = if_nametoindex(ifname);
+ if (!ifindex) return -1;
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifinfomsg *ifi;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
+ nh->nlmsg_type = RTM_NEWLINK;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ ifi = NLMSG_DATA(nh);
+ ifi->ifi_family = AF_UNSPEC;
+ ifi->ifi_index = ifindex;
+ addattr_l(nh, sizeof(buf), IFLA_MTU, &mtu, sizeof(mtu));
+ return rtnl_talk(nh);
+}
+
+int rtnl_link_set_hwaddr(const char *ifname, const char *hwaddr) {
+ unsigned ifindex = if_nametoindex(ifname);
+ if (!ifindex) return -1;
+ unsigned char mac[6];
+ if (sscanf(hwaddr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) != 6) {
+ log_error("rtnl: invalid hwaddr %s", hwaddr);
+ return -1;
+ }
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifinfomsg *ifi;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
+ nh->nlmsg_type = RTM_NEWLINK;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ ifi = NLMSG_DATA(nh);
+ ifi->ifi_family = AF_UNSPEC;
+ ifi->ifi_index = ifindex;
+ addattr_l(nh, sizeof(buf), IFLA_ADDRESS, mac, 6);
+ return rtnl_talk(nh);
+}
+
+// Parse "10.0.0.1/24" or "10.0.0.1" + netmask, returns family, addr, prefixlen
+static int parse_addr(const char *address, const char *netmask, int *family, unsigned char *addr, int *prefixlen) {
+ char ip[128];
+ char *slash = strchr(address, '/');
+ if (slash) {
+ size_t iplen = slash - address;
+ if (iplen >= sizeof(ip)) return -1;
+ memcpy(ip, address, iplen);
+ ip[iplen] = '\0';
+ *prefixlen = atoi(slash+1);
+ } else {
+ strncpy(ip, address, sizeof(ip)-1);
+ ip[sizeof(ip)-1] = '\0';
+ if (netmask) {
+ // netmask like 255.255.255.0 -> prefixlen
+ struct in_addr nm;
+ if (inet_pton(AF_INET, netmask, &nm) != 1) return -1;
+ uint32_t m = ntohl(nm.s_addr);
+ int len = 0;
+ while (m & 0x80000000) { len++; m <<= 1; }
+ *prefixlen = len;
+ } else {
+ *prefixlen = -1;
+ }
+ }
+ // Try v4 then v6
+ struct in_addr a4;
+ struct in6_addr a6;
+ if (inet_pton(AF_INET, ip, &a4) == 1) {
+ *family = AF_INET;
+ memcpy(addr, &a4, 4);
+ if (*prefixlen < 0) *prefixlen = 32;
+ return 0;
+ }
+ if (inet_pton(AF_INET6, ip, &a6) == 1) {
+ *family = AF_INET6;
+ memcpy(addr, &a6, 16);
+ if (*prefixlen < 0) *prefixlen = 128;
+ return 0;
+ }
+ return -1;
+}
+
+int rtnl_addr_add(const char *ifname, const char *address, const char *netmask) {
+ unsigned ifindex = if_nametoindex(ifname);
+ if (!ifindex) {
+ log_error("rtnl: addr_add %s: ifindex failed", ifname);
+ return -1;
+ }
+ int family, prefixlen;
+ unsigned char addr[16];
+ if (parse_addr(address, netmask, &family, addr, &prefixlen) != 0) {
+ log_error("rtnl: addr_add %s: parse %s failed", ifname, address);
+ return -1;
+ }
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifaddrmsg *ifa;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifa));
+ nh->nlmsg_type = RTM_NEWADDR;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
+ ifa = NLMSG_DATA(nh);
+ ifa->ifa_family = family;
+ ifa->ifa_prefixlen = prefixlen;
+ ifa->ifa_flags = 0;
+ ifa->ifa_scope = 0;
+ ifa->ifa_index = ifindex;
+ int alen = (family == AF_INET) ? 4 : 16;
+ addattr_l(nh, sizeof(buf), IFA_LOCAL, addr, alen);
+ addattr_l(nh, sizeof(buf), IFA_ADDRESS, addr, alen);
+ int rc = rtnl_talk(nh);
+ if (rc != 0) {
+ // If EEXIST, treat as success (idempotent)
+ log_debug("rtnl: addr_add %s %s/%d rc=%d", ifname, address, prefixlen, rc);
+ }
+ return rc;
+}
+
+int rtnl_addr_del(const char *ifname, const char *address, const char *netmask) {
+ unsigned ifindex = if_nametoindex(ifname);
+ if (!ifindex) return -1;
+ int family, prefixlen;
+ unsigned char addr[16];
+ if (parse_addr(address, netmask, &family, addr, &prefixlen) != 0) return -1;
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifaddrmsg *ifa;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifa));
+ nh->nlmsg_type = RTM_DELADDR;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ ifa = NLMSG_DATA(nh);
+ ifa->ifa_family = family;
+ ifa->ifa_prefixlen = prefixlen;
+ ifa->ifa_index = ifindex;
+ int alen = (family == AF_INET) ? 4 : 16;
+ addattr_l(nh, sizeof(buf), IFA_LOCAL, addr, alen);
+ return rtnl_talk(nh);
+}
+
+int rtnl_route_add_default(const char *gateway, const char *ifname) {
+ int family;
+ unsigned char gw[16];
+ int prefixlen;
+ // gateway may be without prefix, default to /32 or /128
+ if (parse_addr(gateway, NULL, &family, gw, &prefixlen) != 0) return -1;
+ // For default route, we ignore prefixlen and use 0
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct rtmsg *rtm;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*rtm));
+ nh->nlmsg_type = RTM_NEWROUTE;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
+ rtm = NLMSG_DATA(nh);
+ rtm->rtm_family = family;
+ rtm->rtm_dst_len = 0;
+ rtm->rtm_src_len = 0;
+ rtm->rtm_tos = 0;
+ rtm->rtm_table = RT_TABLE_MAIN;
+ rtm->rtm_protocol = RTPROT_BOOT;
+ rtm->rtm_scope = RT_SCOPE_UNIVERSE;
+ rtm->rtm_type = RTN_UNICAST;
+ int alen = (family == AF_INET) ? 4 : 16;
+ addattr_l(nh, sizeof(buf), RTA_GATEWAY, gw, alen);
+ if (ifname) {
+ unsigned ifindex = if_nametoindex(ifname);
+ if (ifindex) addattr_l(nh, sizeof(buf), RTA_OIF, &ifindex, sizeof(ifindex));
+ }
+ return rtnl_talk(nh);
+}
+
+int rtnl_route_del_default(const char *gateway, const char *ifname) {
+ int family;
+ unsigned char gw[16];
+ int prefixlen;
+ if (parse_addr(gateway, NULL, &family, gw, &prefixlen) != 0) return -1;
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct rtmsg *rtm;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*rtm));
+ nh->nlmsg_type = RTM_DELROUTE;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ rtm = NLMSG_DATA(nh);
+ rtm->rtm_family = family;
+ rtm->rtm_dst_len = 0;
+ rtm->rtm_table = RT_TABLE_MAIN;
+ int alen = (family == AF_INET) ? 4 : 16;
+ addattr_l(nh, sizeof(buf), RTA_GATEWAY, gw, alen);
+ if (ifname) {
+ unsigned ifindex = if_nametoindex(ifname);
+ if (ifindex) addattr_l(nh, sizeof(buf), RTA_OIF, &ifindex, sizeof(ifindex));
+ }
+ return rtnl_talk(nh);
+}
+
+int rtnl_vlan_create(const char *name, const char *rawdev, int vlan_id) {
+ unsigned rawidx = if_nametoindex(rawdev);
+ if (!rawidx) {
+ log_error("rtnl: vlan %s: rawdev %s not found", name, rawdev);
+ return -1;
+ }
+ // Check if already exists
+ if (if_nametoindex(name) != 0) {
+ log_info("rtnl: vlan %s already exists", name);
+ return 0;
+ }
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifinfomsg *ifi;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
+ nh->nlmsg_type = RTM_NEWLINK;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
+ ifi = NLMSG_DATA(nh);
+ ifi->ifi_family = AF_UNSPEC;
+ addattr_l(nh, sizeof(buf), IFLA_IFNAME, name, strlen(name)+1);
+ addattr_l(nh, sizeof(buf), IFLA_LINK, &rawidx, sizeof(rawidx));
+ int nest = addattr_nested_start(nh, sizeof(buf), IFLA_LINKINFO);
+ addattr_l(nh, sizeof(buf), IFLA_INFO_KIND, "vlan", 5);
+ int nest2 = addattr_nested_start(nh, sizeof(buf), IFLA_INFO_DATA);
+ addattr_l(nh, sizeof(buf), IFLA_VLAN_ID, &vlan_id, sizeof(vlan_id));
+ addattr_nested_end(nh, nest2);
+ addattr_nested_end(nh, nest);
+ return rtnl_talk(nh);
+}
+
+int rtnl_bridge_create(const char *name) {
+ if (if_nametoindex(name) != 0) {
+ log_info("rtnl: bridge %s already exists", name);
+ return 0;
+ }
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifinfomsg *ifi;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
+ nh->nlmsg_type = RTM_NEWLINK;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
+ ifi = NLMSG_DATA(nh);
+ ifi->ifi_family = AF_UNSPEC;
+ addattr_l(nh, sizeof(buf), IFLA_IFNAME, name, strlen(name)+1);
+ int nest = addattr_nested_start(nh, sizeof(buf), IFLA_LINKINFO);
+ addattr_l(nh, sizeof(buf), IFLA_INFO_KIND, "bridge", 7);
+ addattr_nested_end(nh, nest);
+ return rtnl_talk(nh);
+}
+
+int rtnl_bridge_set_stp(const char *name, bool on) {
+ unsigned ifindex = if_nametoindex(name);
+ if (!ifindex) return -1;
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifinfomsg *ifi;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
+ nh->nlmsg_type = RTM_NEWLINK;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ ifi = NLMSG_DATA(nh);
+ ifi->ifi_family = AF_UNSPEC;
+ ifi->ifi_index = ifindex;
+ int nest = addattr_nested_start(nh, sizeof(buf), IFLA_LINKINFO);
+ addattr_l(nh, sizeof(buf), IFLA_INFO_KIND, "bridge", 7);
+ int nest2 = addattr_nested_start(nh, sizeof(buf), IFLA_INFO_DATA);
+ uint8_t stp = on ? 1 : 0;
+ addattr_l(nh, sizeof(buf), IFLA_BR_STP_STATE, &stp, sizeof(stp));
+ addattr_nested_end(nh, nest2);
+ addattr_nested_end(nh, nest);
+ return rtnl_talk(nh);
+}
+
+int rtnl_bridge_set_vlan_aware(const char *name, bool on) {
+ unsigned ifindex = if_nametoindex(name);
+ if (!ifindex) return -1;
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifinfomsg *ifi;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
+ nh->nlmsg_type = RTM_NEWLINK;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ ifi = NLMSG_DATA(nh);
+ ifi->ifi_family = AF_UNSPEC;
+ ifi->ifi_index = ifindex;
+ int nest = addattr_nested_start(nh, sizeof(buf), IFLA_LINKINFO);
+ addattr_l(nh, sizeof(buf), IFLA_INFO_KIND, "bridge", 7);
+ int nest2 = addattr_nested_start(nh, sizeof(buf), IFLA_INFO_DATA);
+ uint8_t vlan = on ? 1 : 0;
+ addattr_l(nh, sizeof(buf), IFLA_BR_VLAN_FILTERING, &vlan, sizeof(vlan));
+ addattr_nested_end(nh, nest2);
+ addattr_nested_end(nh, nest);
+ return rtnl_talk(nh);
+}
+
+int rtnl_bridge_add_port(const char *br, const char *port) {
+ unsigned bridx = if_nametoindex(br);
+ unsigned pidx = if_nametoindex(port);
+ if (!bridx || !pidx) {
+ log_error("rtnl: bridge_add_port %s -> %s: ifindex not found (%u, %u)", port, br, pidx, bridx);
+ return -1;
+ }
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifinfomsg *ifi;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
+ nh->nlmsg_type = RTM_NEWLINK;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ ifi = NLMSG_DATA(nh);
+ ifi->ifi_family = AF_UNSPEC;
+ ifi->ifi_index = pidx;
+ addattr_l(nh, sizeof(buf), IFLA_MASTER, &bridx, sizeof(bridx));
+ return rtnl_talk(nh);
+}
+
+int rtnl_bridge_del_port(const char *br, const char *port) {
+ (void)br;
+ unsigned pidx = if_nametoindex(port);
+ if (!pidx) return -1;
+ char buf[NL_BUFSZ];
+ struct nlmsghdr *nh = (struct nlmsghdr*)buf;
+ struct ifinfomsg *ifi;
+ memset(buf, 0, sizeof(buf));
+ nh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
+ nh->nlmsg_type = RTM_NEWLINK;
+ nh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ ifi = NLMSG_DATA(nh);
+ ifi->ifi_family = AF_UNSPEC;
+ ifi->ifi_index = pidx;
+ // No master = 0 removes from bridge? Actually need to set master 0 and use NLM_F?
+ // For del, we set master to 0 and use RTM_NEWLINK with master 0? The kernel handles master 0 as no bridge.
+ // Alternative is to use RTM_DELLINK? No, enslavement is via master.
+ // Setting master to 0 and sending NEWLINK should release.
+ // But to be safe, we send with master 0 and hope.
+ // If that doesn't work, we can try to use `ip link set dev port nomaster`
+ uint32_t zero = 0;
+ addattr_l(nh, sizeof(buf), IFLA_MASTER, &zero, sizeof(zero));
+ return rtnl_talk(nh);
+}
diff --git a/src/netlink/rtnl.h b/src/netlink/rtnl.h
@@ -0,0 +1,31 @@
+#ifndef __UNOS_RTRNL_H__
+#define __UNOS_RTRNL_H__
+
+#include <stdbool.h>
+
+// Netlink helpers that replace system("ip ...").
+// All return 0 on success, -1 on failure (and log via rxi/log).
+
+int rtnl_link_up(const char *ifname);
+int rtnl_link_down(const char *ifname);
+int rtnl_link_set_mtu(const char *ifname, int mtu);
+int rtnl_link_set_hwaddr(const char *ifname, const char *hwaddr);
+
+// address may be "10.0.0.1/24" or "2001:db8::1/64"; netmask is optional second arg for compat
+int rtnl_addr_add(const char *ifname, const char *address, const char *netmask);
+int rtnl_addr_del(const char *ifname, const char *address, const char *netmask);
+
+int rtnl_route_add_default(const char *gateway, const char *ifname);
+int rtnl_route_del_default(const char *gateway, const char *ifname);
+
+// VLAN: create name type vlan id vlan_id with link rawdev (raw may be inferred from dot)
+int rtnl_vlan_create(const char *name, const char *rawdev, int vlan_id);
+
+// Bridge: create br with stp/vlan_aware, and enslave ports (space-separated)
+int rtnl_bridge_create(const char *name);
+int rtnl_bridge_set_stp(const char *name, bool on);
+int rtnl_bridge_set_vlan_aware(const char *name, bool on);
+int rtnl_bridge_add_port(const char *br, const char *port);
+int rtnl_bridge_del_port(const char *br, const char *port);
+
+#endif // __UNOS_RTRNL_H__
diff --git a/target/common/Makefile b/target/common/Makefile
@@ -4,8 +4,8 @@ BIN=unosd
SRC:=
SRC+=$(wildcard src/*.c)
-SRC+=$(filter-out src/cli/%,$(wildcard src/*/*.c))
-SRC+=$(filter-out src/cli/%,$(wildcard src/*/*/*.c))
+SRC+=$(wildcard src/*/*.c)
+SRC+=$(wildcard src/*/*/*.c)
CFLAGS?=-Wall -Wextra -O2
LDFLAGS?=