linkd

Control plane daemon for unos
git clone git://git.finwo.net/app/linkd
Log | Files | Refs | README

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:
Asrc/cli/ifdown.c | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/cli/ifquery.c | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asrc/cli/ifreload.c | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/cli/ifup.c | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/cli/registry.c | 22++++++++++++++++++++++
Asrc/cli/registry.h | 9+++++++++
Msrc/cli/unosc.c | 42++++++++++++++++++++----------------------
Asrc/cli/unosd.c | 450+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/config/ifaces.c | 25+++++++++++++++++++++++++
Msrc/config/ifaces.h | 3+++
Msrc/ipc.c | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/main.c | 463++++++++-----------------------------------------------------------------------
Asrc/netlink/rtnl.c | 499+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/netlink/rtnl.h | 31+++++++++++++++++++++++++++++++
Mtarget/common/Makefile | 4++--
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?=