linkd

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

commit d56f938937aa14a04d88f755400c2bbccecd8aa8
parent dacf038ab263016b646983a09539da397b2779ec
Author: finwo <finwo@pm.me>
Date:   Thu, 24 Sep 2026 09:47:13 +0200

Rename to linkd

Diffstat:
MMakefile | 2+-
AREADME.md | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplugins/bcm/Makefile | 4++--
Mplugins/bcm/README.md | 14+++++++-------
Mplugins/bcm/src/dataplane.c | 14+++++++-------
Msrc/cli/ifdown.c | 9+++++----
Msrc/cli/ifquery.c | 9+++++----
Msrc/cli/ifreload.c | 9+++++----
Msrc/cli/ifup.c | 11++++++-----
Asrc/cli/linkctl.c | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/cli/linkd.c | 407+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/cli/registry.h | 6+++---
Dsrc/cli/unosc.c | 95-------------------------------------------------------------------------------
Dsrc/cli/unosd.c | 471-------------------------------------------------------------------------------
Asrc/config/daemon.c | 293+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/config/daemon.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/config/ifaces.c | 113+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/config/ifaces.h | 20++++++++++----------
Msrc/config/ports.c | 67+++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/config/ports.h | 14+++++++-------
Msrc/dataplane.h | 16++++++++--------
Msrc/dataplane/kernel.c | 4++--
Msrc/dataplane/plugin.c | 12++++++------
Msrc/dataplane/registry.c | 4++--
Msrc/dataplane/registry.h | 6+++---
Msrc/ipc.c | 106+++++++++++++++++++++++++++++++++++--------------------------------------------
Msrc/ipc.h | 9++++-----
Msrc/main.c | 70++++++++++++++++++++++++++++++++--------------------------------------
Msrc/netlink/filter.h | 6+++---
Msrc/netlink/netlink.h | 6+++---
Msrc/netlink/rtnl.c | 14++++++++++++++
Msrc/netlink/rtnl.h | 6+++---
Msrc/util/config.c | 21+++++++++++++++++++++
Msrc/util/config.h | 13++++++++++---
Mtarget/common/Makefile | 2+-
Atests/helpers.sh | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/run.sh | 37+++++++++++++++++++++++++++++++++++++
Atests/unit/test_ascii.sh | 33+++++++++++++++++++++++++++++++++
Atests/unit/test_multicall.sh | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/unit/test_netlink.sh | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/unit/test_parse.sh | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
41 files changed, 1657 insertions(+), 836 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,6 +1,6 @@ TARGET:=linux-glibc-amd64 -BIN:=unosd +BIN:=linkd .PHONY: default default: build/${TARGET}/${BIN} diff --git a/README.md b/README.md @@ -0,0 +1,114 @@ +linkd +===== + +A network interface management daemon: an `ifupdown` replacement with +pluggable dataplane backends. It owns `/etc/network/ports` and +`/etc/network/interfaces`, and applies that configuration to the kernel over +raw netlink. + +`linkd` is a multicall binary. It dispatches on `basename(argv[0])`, and the +package ships `ifup`, `ifdown`, `ifquery`, `ifreload` and `linkctl` as symlinks +to it. `linkctl` is the exception: it is a thin shim that takes its subcommand +from `argv[1]` and does not dispatch on `argv[0]`. + +This repository contains only the daemon. Packaging, OS images and any +integration tests that boot a real machine belong to whatever OS consumes it. + + +Design constraints +------------------ + +**No shelling out to `ip`.** Every link, address, route, VLAN, bridge and VRF +operation goes through `src/netlink/rtnl.c` against a raw `AF_NETLINK` socket. +This is not a style preference. Calling `ip` reintroduces a `PATH` dependency +on the switch and, worse, makes operations asynchronous: the command returns +before the kernel has applied anything, which is what forces tests to sleep and +retry. Netlink acks before we reply, so callers are synchronous by +construction. `tests/unit/test_netlink.sh` enforces this. + +**No libnl or libmnl.** `src/netlink/netlink.h` talks to the kernel directly. +The only link-time dependency is `-ldl`, for `dlopen`ing dataplane plugins. + +**Accepted IPC connections are blocking.** The listener is non-blocking so it +can sit in `poll()`, but the accepted connection must not be: a non-blocking +accepted fd makes the server read `EAGAIN`, stdio report EOF, and the parser +see an empty stream, so it replies with nothing. That was the actual cause of +what looked like IPC flakiness. Bounded instead by `SO_RCVTIMEO`/`SO_SNDTIMEO`. + +**Clients half-close after writing.** Without `shutdown(sock, SHUT_WR)` the +server waits for a second command until its receive timeout expires. + + +Building +-------- + + make # build for linux-glibc-amd64 + make targets # list available target triples + make clean + +Requires [`dep`](https://github.com/finwo/dep) on `PATH` and network access to +`git.finwo.net`. + +The top-level `Makefile` is an assembler, not a compiler. It stages a +self-contained tree by layering three things into `build/<triple>/`: + + src/ -> build/<triple>/src the daemon sources + target/common/ -> build/<triple>/ the real Makefile + .dep + target/<triple>/ -> build/<triple>/ per-triple overlay + +then runs `dep install` inside it and builds there. `target/<triple>/` is an +extension point for a `Makefile.target`, which `target/common/Makefile` picks +up with `-include`; the one existing triple has none, so it is currently empty. + +`build/` is generated and gitignored in full. + +### Dependencies + +`target/common/.dep` declares three: `cofyc/argparse`, `finwo/cnfparse` and +`rxi/log`. A fourth, `finwo/buf`, arrives transitively through cnfparse's own +manifest. All four are compiled from source straight into the binary; nothing +is linked against a prebuilt library. + +They are fetched from branch tips rather than tags, and are not checksummed. +That means a build is not reproducible across upstream changes, and a sha256 on +a release tarball of *this* repository pins its own code but not the four +libraries compiled into the resulting binary. This is a deliberate, known +deviation from the pin-everything rule this project otherwise follows, and the fix +(tagging those repositories) is upstream of here. + + +Testing +------- + + ./tests/run.sh + +All tests are unit tests: they build the daemon and exercise it directly. +`test_parse.sh` links the real `ifaces`/`ports`/`config` objects and parses a +generated config, so it fails when the grammar changes rather than when a +fixture drifts. + +Anything requiring a booted system -- bridges and VLANs against a real kernel, +VRF table allocation, address application -- is an integration test and lives +in the consuming OS repository, which can build a rootfs and run a VM. + +Note for anyone editing the source-grepping guards in `test_netlink.sh`: each +one asserts the file exists before grepping it. A grep that matches nothing +because a file moved is indistinguishable from one that matches nothing because +the code is correct, and only the second should pass. + + +Dataplane plugins +----------------- + +`src/dataplane/plugin.c` `dlopen`s shared objects from +`/usr/lib/linkd/dataplane/` (`LINKD_DATAPLANE_DIR` in `src/dataplane.h`), each +exporting a `struct dp_ops` matching `LINKD_DATAPLANE_ABI`. Without one, linkd +uses the built-in kernel dataplane. + +`plugins/bcm/` is the Broadcom XGS plugin. It is not built by the top-level +Makefile and requires `SDK=` pointing at a built OpenBCM tree: + + make -C plugins/bcm SDK=/path/to/opennsl + +It is intended to be built and shipped by the `openbcm` package, which does not +exist yet. diff --git a/plugins/bcm/Makefile b/plugins/bcm/Makefile @@ -7,7 +7,7 @@ SDK?= CFLAGS?=-Wall -Wextra -O2 CFLAGS+=-fPIC -# unosd's dataplane contract +# linkd's dataplane contract INCLUDES:= INCLUDES+=-I ../../src @@ -46,4 +46,4 @@ clean: .PHONY: install install: $(BIN) - install -Dm0755 $(BIN) $(DESTDIR)/usr/lib/unos/dataplane/$(BIN) + install -Dm0755 $(BIN) $(DESTDIR)/usr/lib/linkd/dataplane/$(BIN) diff --git a/plugins/bcm/README.md b/plugins/bcm/README.md @@ -1,14 +1,14 @@ # plugins/bcm -Broadcom XGS dataplane backend for `unosd`. +Broadcom XGS dataplane backend for `linkd`. -This is **not** built by the `unosd` build. It is built and shipped by the +This is **not** built by the `linkd` build. It is built and shipped by the `openbcm` package, which is installed only on hardware that has a Broadcom switching ASIC. -That split is deliberate: a UNOS image is byte-identical on every machine. +That split is deliberate: the OS image is byte-identical on every machine. Hardware capability arrives as a package, not as a separate image flavour. -`unosd` dlopens whatever it finds in `/usr/lib/unos/dataplane/`, so on a box +`linkd` dlopens whatever it finds in `/usr/lib/linkd/dataplane/`, so on a box without an ASIC the directory is simply empty and the built-in kernel dataplane is used instead. @@ -17,11 +17,11 @@ dataplane is used instead. A plugin is a shared object exporting exactly one symbol: ```c -struct dp_ops unos_dataplane_ops; +struct dp_ops linkd_dataplane_ops; ``` -`dp_ops` is declared in `src/unosd/src/dataplane.h`. The `abi` field must equal -`UNOS_DATAPLANE_ABI` or the plugin is refused at load time. +`dp_ops` is declared in `src/dataplane.h`. The `abi` field must equal +`LINKD_DATAPLANE_ABI` or the plugin is refused at load time. `probe()` must be cheap and side-effect free. It runs against every registered backend before one is chosen. This backend probes for `/dev/linux-kernel-bde`, diff --git a/plugins/bcm/src/dataplane.c b/plugins/bcm/src/dataplane.c @@ -1,10 +1,10 @@ -// Broadcom XGS dataplane backend for unosd. +// Broadcom XGS dataplane backend for linkd. // -// Built and shipped by the `openbcm` package, NOT by the unosd build. It is a -// plugin precisely so that a single UNOS image runs unchanged on hardware with +// Built and shipped by the `openbcm` package, NOT by the linkd build. It is a +// plugin precisely so that a single OS image runs unchanged on hardware with // and without a switching ASIC: on a Broadcom box the openbcm package drops -// this object into UNOS_DATAPLANE_DIR, everywhere else the directory stays -// empty and unosd falls back to the built-in kernel backend. +// this object into LINKD_DATAPLANE_DIR, everywhere else the directory stays +// empty and linkd falls back to the built-in kernel backend. // // Building this requires the OpenBCM SDK headers and libbcm. See the README // next to this file. @@ -106,8 +106,8 @@ static int bcm_resync(void) { return DP_RET_ERROR; } -struct dp_ops unos_dataplane_ops = { - .abi = UNOS_DATAPLANE_ABI, +struct dp_ops linkd_dataplane_ops = { + .abi = LINKD_DATAPLANE_ABI, .name = "bcm", .probe = bcm_probe, .init = bcm_init, diff --git a/src/cli/ifdown.c b/src/cli/ifdown.c @@ -6,9 +6,9 @@ #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> +#include "config/daemon.h" #include "registry.h" -#define UNOS_IPC_PATH "/run/unosd.sock" int main_ifdown(int argc, char *argv[]) { if (argc < 2) { @@ -19,18 +19,19 @@ int main_ifdown(int argc, char *argv[]) { 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"); + const char *sockpath = linkd_client_socket(); 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); + strncpy(addr.sun_path, sockpath, 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)); + fprintf(stderr, "ifdown: connect %s failed: %s\nIs linkd running?\n", sockpath, 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 + // Half-close: signals EOF to linkd 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]; diff --git a/src/cli/ifquery.c b/src/cli/ifquery.c @@ -6,27 +6,28 @@ #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> +#include "config/daemon.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"); + const char *sockpath = linkd_client_socket(); 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); + strncpy(addr.sun_path, sockpath, 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)); + fprintf(stderr, "ifquery: connect %s failed: %s\nIs linkd running?\n", sockpath, 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 + // Half-close: signals EOF to linkd 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]; diff --git a/src/cli/ifreload.c b/src/cli/ifreload.c @@ -6,9 +6,9 @@ #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> +#include "config/daemon.h" #include "registry.h" -#define UNOS_IPC_PATH "/run/unosd.sock" int main_ifreload(int argc, char *argv[]) { if (argc != 1) { @@ -18,18 +18,19 @@ int main_ifreload(int argc, char *argv[]) { (void)argv; char out[4096]; int len = snprintf(out, sizeof(out), "ifreload\n"); + const char *sockpath = linkd_client_socket(); 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); + strncpy(addr.sun_path, sockpath, 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)); + fprintf(stderr, "ifreload: connect %s failed: %s\nIs linkd running?\n", sockpath, 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 + // Half-close: signals EOF to linkd 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]; diff --git a/src/cli/ifup.c b/src/cli/ifup.c @@ -6,12 +6,12 @@ #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> +#include "config/daemon.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 + // ifup <if> -- thin wrapper, same socket logic as linkctl but fixed command if (argc < 2) { fprintf(stderr, "usage: ifup <interface>\n"); return 1; @@ -24,18 +24,19 @@ int main_ifup(int argc, char *argv[]) { } len += snprintf(out+len, sizeof(out)-len, "\n"); + const char *sockpath = linkd_client_socket(); 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); + strncpy(addr.sun_path, sockpath, 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)); + fprintf(stderr, "ifup: connect %s failed: %s\nIs linkd running?\n", sockpath, 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 + // Half-close: signals EOF to linkd 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]; diff --git a/src/cli/linkctl.c b/src/cli/linkctl.c @@ -0,0 +1,96 @@ +#define _GNU_SOURCE +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <libgen.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <unistd.h> + +#include "config/daemon.h" +#include "registry.h" + + +int main_linkctl(int argc, char *argv[]) { + // linkctl is a tiny shim: linkctl <cmd> [args...] -> send "cmd [args...]\n" to /var/run/linkd.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]; + + const char *sockpath = linkd_client_socket(); + sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sock < 0) { + perror("socket"); + return 1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, sockpath, sizeof(addr.sun_path)-1); + + if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + fprintf(stderr, "linkctl: connect %s failed: %s\n", sockpath, strerror(errno)); + fprintf(stderr, "Is linkd running?\n"); + close(sock); + return 1; + } + + // Send command: "cmd [args...]\n" + { + char out[4096]; + int len = 0; + len += snprintf(out+len, sizeof(out)-len, "%s", cmd); + for (int i = 0; i < cmd_argc; i++) { + len += snprintf(out+len, sizeof(out)-len, " %s", cmd_argv[i]); + } + len += snprintf(out+len, sizeof(out)-len, "\n"); + if (write(sock, out, len) != len) { + perror("write"); + close(sock); + return 1; + } + // Half-close: signals EOF to linkd 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 + // Protocol: server sends lines, first line is OK/ERR, but for ifquery the dump is the response + // We just pipe everything to stdout, and exit 0 on OK, 1 on ERR + int saw_ok = 0; + int saw_err = 0; + FILE *f = fdopen(sock, "r"); + if (!f) { + perror("fdopen"); + close(sock); + return 1; + } + while (fgets(buf, sizeof(buf), f)) { + // Check for OK/ERR prefix on first line? Just print and detect + if (!saw_ok && !saw_err) { + if (!strncmp(buf, "OK", 2)) saw_ok = 1; + else if (!strncmp(buf, "ERR", 3)) saw_err = 1; + } + fputs(buf, stdout); + // For non-ifquery, the response is just OK/ERR line + // For ifquery, there may be multiple lines plus OK + } + fclose(f); + // If we saw ERR, exit 1 + if (saw_err) return 1; + return 0; +} + +__attribute__((constructor)) +static void register_linkctl(void) { + cli_register("linkctl", main_linkctl); +} diff --git a/src/cli/linkd.c b/src/cli/linkd.c @@ -0,0 +1,407 @@ +#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/daemon.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(); +} + +static int apply_ports(void) { + const struct dp_ops *dp = dp_active(); + struct linkd_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; +} + +// Applies the already-parsed in-memory config (ifaces_list()); nothing here +// reads the filesystem. Two ordered phases, because interfaces depend on each +// other: a member cannot join a VRF that does not exist yet, and members are +// routinely declared first -- the production Cumulus config we captured has +// `iface eth0` carrying `vrf mgmt` above the `iface mgmt` stanza that defines +// it. Bridges need no such phase: membership is declared on the bridge +// (`bridge-ports`), not on the member. +static int apply_interfaces(void) { + struct linkd_iface *cur; + + // Phase 1: bring every VRF device into existence. + for (cur = ifaces_list(); cur; cur = cur->next) { + if (!cur->auto_flag) continue; + if (!iface_is_vrf(cur)) continue; + log_info("apply_interfaces: vrf %s table %d", cur->name, cur->vrf_table); + rtnl_vrf_create(cur->name, (uint32_t)cur->vrf_table); + rtnl_link_up(cur->name); + } + + // Phase 2: apply every interface, VRF devices included (create is + // idempotent -- rtnl_vrf_create returns early when the device exists). + 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); + } + } + if (iface_is_bridge(cur)) { + 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); + } + } + // Enslave to a VRF before bringing the link up and assigning addresses: + // moving an interface into a VRF flushes its addresses, so doing it after + // would silently discard everything we just configured. + if (cur->vrf_master) { + rtnl_vrf_add_port(cur->vrf_master, cur->name); + } + 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_linkd(int argc, char **argv) { + // Cast for argparse which wants const char** + const char **c_argv = (const char **)argv; + char *config_path = LINKD_CONFIG_PATH; + 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_path, "Configuration file (default: " LINKD_CONFIG_PATH ")", 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 + daemon_cfg_register(); + 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 (daemon_cfg_load(config_path) < 0) { + return 1; + } + const struct linkd_daemon_cfg *dcfg = daemon_cfg(); + + if (load_namespace("ports", dcfg->ports, dcfg->ports_n) < 0) { + return 1; + } + if (load_namespace("interfaces", dcfg->iface, dcfg->iface_n) < 0) { + return 1; + } + { + int n = 0; + for (struct linkd_iface *c = ifaces_list(); c; c = c->next) n++; + log_info("main: loaded %d interfaces", n); + for (struct linkd_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(LINKD_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_linkd(void) { + cli_register("linkd", main_linkd); +} + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/src/cli/registry.h b/src/cli/registry.h @@ -1,9 +1,9 @@ -#ifndef __UNOS_CLI_REGISTRY_H__ -#define __UNOS_CLI_REGISTRY_H__ +#ifndef __LINKD_CLI_REGISTRY_H__ +#define __LINKD_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__ +#endif // __LINKD_CLI_REGISTRY_H__ diff --git a/src/cli/unosc.c b/src/cli/unosc.c @@ -1,95 +0,0 @@ -#define _GNU_SOURCE -#include <errno.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <libgen.h> -#include <sys/socket.h> -#include <sys/un.h> -#include <unistd.h> - -#include "registry.h" - -#define UNOS_IPC_PATH "/run/unosd.sock" - -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]; - - sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); - if (sock < 0) { - perror("socket"); - return 1; - } - - memset(&addr, 0, sizeof(addr)); - 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, "unosc: connect %s failed: %s\n", UNOS_IPC_PATH, strerror(errno)); - fprintf(stderr, "Is unosd running?\n"); - close(sock); - return 1; - } - - // Send command: "cmd [args...]\n" - { - char out[4096]; - int len = 0; - len += snprintf(out+len, sizeof(out)-len, "%s", cmd); - for (int i = 0; i < cmd_argc; i++) { - len += snprintf(out+len, sizeof(out)-len, " %s", cmd_argv[i]); - } - len += snprintf(out+len, sizeof(out)-len, "\n"); - 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 timeout. - shutdown(sock, SHUT_WR); - } - - // Read response and pipe to stdout - // Protocol: server sends lines, first line is OK/ERR, but for ifquery the dump is the response - // We just pipe everything to stdout, and exit 0 on OK, 1 on ERR - int saw_ok = 0; - int saw_err = 0; - FILE *f = fdopen(sock, "r"); - if (!f) { - perror("fdopen"); - close(sock); - return 1; - } - while (fgets(buf, sizeof(buf), f)) { - // Check for OK/ERR prefix on first line? Just print and detect - if (!saw_ok && !saw_err) { - if (!strncmp(buf, "OK", 2)) saw_ok = 1; - else if (!strncmp(buf, "ERR", 3)) saw_err = 1; - } - fputs(buf, stdout); - // For non-ifquery, the response is just OK/ERR line - // For ifquery, there may be multiple lines plus OK - } - fclose(f); - // If we saw ERR, exit 1 - 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 @@ -1,471 +0,0 @@ -#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; -} - -// Applies the already-parsed in-memory config (ifaces_list()); nothing here -// reads the filesystem. Two ordered phases, because interfaces depend on each -// other: a member cannot join a VRF that does not exist yet, and members are -// routinely declared first -- the production Cumulus config we captured has -// `iface eth0` carrying `vrf mgmt` above the `iface mgmt` stanza that defines -// it. Bridges need no such phase: membership is declared on the bridge -// (`bridge-ports`), not on the member. -static int apply_interfaces(void) { - struct unos_iface *cur; - - // Phase 1: bring every VRF device into existence. - for (cur = ifaces_list(); cur; cur = cur->next) { - if (!cur->auto_flag) continue; - if (!iface_is_vrf(cur)) continue; - log_info("apply_interfaces: vrf %s table %d", cur->name, cur->vrf_table); - rtnl_vrf_create(cur->name, (uint32_t)cur->vrf_table); - rtnl_link_up(cur->name); - } - - // Phase 2: apply every interface, VRF devices included (create is - // idempotent -- rtnl_vrf_create returns early when the device exists). - 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); - } - } - if (iface_is_bridge(cur)) { - 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); - } - } - // Enslave to a VRF before bringing the link up and assigning addresses: - // moving an interface into a VRF flushes its addresses, so doing it after - // would silently discard everything we just configured. - if (cur->vrf_master) { - rtnl_vrf_add_port(cur->vrf_master, cur->name); - } - 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/daemon.c b/src/config/daemon.c @@ -0,0 +1,293 @@ +#define _GNU_SOURCE +#include <libgen.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <unistd.h> + +#include "finwo/cnfparse.h" + +#include "config/daemon.h" +#include "util/config.h" + +static struct linkd_daemon_cfg cfg; +static int cfg_defaults_applied = 0; + +// --------------------------------------------------------------------------- +// string-list helpers + +static int list_append(char ***list, size_t *n, const char *val) { + char **grown = realloc(*list, sizeof(char *) * (*n + 1)); + if (!grown) return -1; + *list = grown; + (*list)[*n] = strdup(val); + if (!(*list)[*n]) return -1; + (*n)++; + return 0; +} + +static void list_free(char ***list, size_t *n) { + for (size_t i = 0; i < *n; i++) free((*list)[i]); + free(*list); + *list = NULL; + *n = 0; +} + +// Replace rather than append: a later config_iface overrides, not merges. +static struct cnf_directive *take_list(struct cnf_directive *dir, char ***list, size_t *n) { + if (dir->argc < 1) { + cfg_error("%s needs at least one path or glob", dir->name); + cnf_directive_free(dir); + return NULL; + } + list_free(list, n); + for (size_t i = 0; i < dir->argc; i++) { + if (list_append(list, n, dir->argv[i]) != 0) { + cfg_error("out of memory reading %s", dir->name); + cnf_directive_free(dir); + return NULL; + } + } + cnf_directive_free(dir); + return NULL; +} + +// --------------------------------------------------------------------------- +// directives + +static struct cnf_directive *h_config_ports(FILE *fd, struct cnf_directive *dir, void *user) { + (void)fd; (void)user; + return take_list(dir, &cfg.ports, &cfg.ports_n); +} + +static struct cnf_directive *h_config_iface(FILE *fd, struct cnf_directive *dir, void *user) { + (void)fd; (void)user; + return take_list(dir, &cfg.iface, &cfg.iface_n); +} + +// Additive: listening on several addresses at once is the point. +static struct cnf_directive *h_listen(FILE *fd, struct cnf_directive *dir, void *user) { + (void)fd; (void)user; + if (dir->argc != 1) { + cfg_error("listen needs exactly one address, e.g. unix:///var/run/linkd.sock"); + cnf_directive_free(dir); + return NULL; + } + const char *a = dir->argv[0]; + if (strncmp(a, "unix://", 7) && strncmp(a, "tcp://", 6)) { + cfg_error("listen: unsupported address `%s' (expected unix:// or tcp://)", a); + cnf_directive_free(dir); + return NULL; + } + if (list_append(&cfg.listen, &cfg.listen_n, a) != 0) { + cfg_error("out of memory reading listen"); + } + cnf_directive_free(dir); + return NULL; +} + +static struct cnf_directive *h_authfile(FILE *fd, struct cnf_directive *dir, void *user) { + (void)fd; (void)user; + if (dir->argc != 1) { + cfg_error("authfile needs exactly one path"); + cnf_directive_free(dir); + return NULL; + } + free(cfg.authfile); + cfg.authfile = strdup(dir->argv[0]); + if (!cfg.authfile) cfg_error("out of memory reading authfile"); + cnf_directive_free(dir); + return NULL; +} + +// Opens a stanza. Known sub-directives handled here; the first unknown one +// ends the stanza and is handed back for re-dispatch. +static struct cnf_directive *h_plugin(FILE *fd, struct cnf_directive *dir, void *user) { + (void)user; + + if (dir->argc != 1) { + cfg_error("plugin needs exactly one executable path or tcp:// address"); + cnf_directive_free(dir); + return NULL; + } + + struct linkd_plugin_cfg *p = calloc(1, sizeof(*p)); + if (!p) { + cfg_error("out of memory reading plugin"); + cnf_directive_free(dir); + return NULL; + } + p->target = strdup(dir->argv[0]); + if (!p->target) { + free(p); + cfg_error("out of memory reading plugin"); + cnf_directive_free(dir); + return NULL; + } + cnf_directive_free(dir); + + // Append: plugins are broadcast to in declaration order. + struct linkd_plugin_cfg **tail = &cfg.plugins; + while (*tail) tail = &(*tail)->next; + *tail = p; + + for (;;) { + struct cnf_directive *sub = cnf_directive_read(fd); + if (!sub) return NULL; // EOF ends the stanza + + if (!strcasecmp(sub->name, "optional")) { + if (sub->argc != 0) { + cfg_error("plugin %s: optional takes no arguments", p->target); + cnf_directive_free(sub); + return NULL; + } + p->optional = 1; + cnf_directive_free(sub); + continue; + } + + // Not ours: hand it back so the caller re-dispatches it. + return sub; + } +} + +// --------------------------------------------------------------------------- + +void daemon_cfg_register(void) { + struct cfg_ns *ns = cfg_ns_get("linkd"); + cfg_register_directive(ns, "config_ports", h_config_ports); + cfg_register_directive(ns, "config_iface", h_config_iface); + cfg_register_directive(ns, "listen", h_listen); + cfg_register_directive(ns, "authfile", h_authfile); + cfg_register_directive(ns, "plugin", h_plugin); +} + +// Per-field, so a config setting only `listen` still gets working paths. +static void apply_defaults(void) { + if (cfg_defaults_applied) return; + cfg_defaults_applied = 1; + + if (cfg.ports_n == 0) { + list_append(&cfg.ports, &cfg.ports_n, "/etc/network/ports"); + list_append(&cfg.ports, &cfg.ports_n, "/etc/network/ports.d/*.cnf"); + } + if (cfg.iface_n == 0) { + list_append(&cfg.iface, &cfg.iface_n, "/etc/network/interfaces"); + list_append(&cfg.iface, &cfg.iface_n, "/etc/network/interfaces.d/*.cnf"); + } + if (cfg.listen_n == 0) { + list_append(&cfg.listen, &cfg.listen_n, "unix:///var/run/linkd.sock"); + } +} + +int daemon_cfg_load(const char *path) { + if (!path) path = LINKD_CONFIG_PATH; + + // Missing file is fine: defaults are a working configuration. + if (access(path, R_OK) != 0) { + apply_defaults(); + return CFG_RET_OK; + } + + FILE *fd = fopen(path, "r"); + if (!fd) { + fprintf(stderr, "config: cannot open %s\n", path); + return CFG_RET_ERROR; + } + + // Relative `source` resolves against the config file's own directory. + char *dup = strdup(path); + char *wd = dup ? dirname(dup) : NULL; + + cfg_error_reset(); + int rc = cfg_parse(cfg_ns_get("linkd"), wd, fd, NULL); + fclose(fd); + free(dup); + + if (rc != CFG_RET_OK) return rc; + + apply_defaults(); + return CFG_RET_OK; +} + +int load_namespace(const char *ns_name, char **patterns, size_t n) { + if (n == 0) return CFG_RET_OK; + + size_t len = strlen("source"); + for (size_t i = 0; i < n; i++) len += 1 + strlen(patterns[i]); + len += 2; // newline + NUL + + char *stub = malloc(len); + if (!stub) { + fprintf(stderr, "config: out of memory building %s source list\n", ns_name); + return CFG_RET_ERROR; + } + + char *p = stub + sprintf(stub, "source"); + for (size_t i = 0; i < n; i++) p += sprintf(p, " %s", patterns[i]); + *p++ = '\n'; + *p = '\0'; + + FILE *fd = fmemopen(stub, strlen(stub), "r"); + if (!fd) { + perror("Could not open in-memory config"); + free(stub); + return CFG_RET_ERROR; + } + + // wd NULL: patterns come from linkd.cnf and are expected to be absolute. + cfg_error_reset(); + int rc = cfg_parse(cfg_ns_get(ns_name), NULL, fd, NULL); + if (rc < 0) { + fprintf(stderr, "config: error reading %s configuration\n", ns_name); + } + + fclose(fd); + free(stub); + return rc; +} + +const char *daemon_cfg_unix_socket(void) { + apply_defaults(); + for (size_t i = 0; i < cfg.listen_n; i++) { + if (!strncmp(cfg.listen[i], "unix://", 7)) return cfg.listen[i] + 7; + } + return NULL; +} + +const char *linkd_client_socket(void) { + static int loaded = 0; + if (!loaded) { + loaded = 1; + daemon_cfg_register(); + // Failure is not fatal here: fall through to the default rather than + // leaving the operator with no way to reach a running daemon. + daemon_cfg_load(getenv("LINKD_CONFIG")); + } + const char *p = daemon_cfg_unix_socket(); + return p ? p : "/var/run/linkd.sock"; +} + +const struct linkd_daemon_cfg *daemon_cfg(void) { + apply_defaults(); + return &cfg; +} + +void daemon_cfg_free(void) { + list_free(&cfg.ports, &cfg.ports_n); + list_free(&cfg.iface, &cfg.iface_n); + list_free(&cfg.listen, &cfg.listen_n); + free(cfg.authfile); + cfg.authfile = NULL; + + struct linkd_plugin_cfg *p = cfg.plugins; + while (p) { + struct linkd_plugin_cfg *next = p->next; + free(p->target); + free(p); + p = next; + } + cfg.plugins = NULL; + cfg_defaults_applied = 0; +} diff --git a/src/config/daemon.h b/src/config/daemon.h @@ -0,0 +1,50 @@ +#ifndef __LINKD_CONFIG_DAEMON_H__ +#define __LINKD_CONFIG_DAEMON_H__ + +#include <stddef.h> + +// linkd's own configuration: /etc/linkd.cnf. Same grammar as the rest, so +// `source` works here too. + +struct linkd_plugin_cfg { + char *target; // executable path, or tcp://[user:pass@]host[:port] + int optional; // failures do not fail the operation + struct linkd_plugin_cfg *next; +}; + +struct linkd_daemon_cfg { + char **ports; size_t ports_n; // path/glob patterns + char **iface; size_t iface_n; + char **listen; size_t listen_n; + char *authfile; + + struct linkd_plugin_cfg *plugins; +}; + +void daemon_cfg_register(void); + +// A missing config file is not an error; the defaults are usable. +int daemon_cfg_load(const char *path); + +// Never NULL; returns defaults before daemon_cfg_load() runs. +const struct linkd_daemon_cfg *daemon_cfg(void); + +void daemon_cfg_free(void); + +// First unix:// listen address, minus the scheme. NULL if only tcp:// is +// configured. Used by the daemon to bind and by the clients to connect. +const char *daemon_cfg_unix_socket(void); + +// Same, for the CLI tools: loads the config first, honouring $LINKD_CONFIG. +// Never NULL -- falls back to the built-in default. +const char *linkd_client_socket(void); + +// Load "ports" / "interfaces" from config_ports / config_iface patterns. +// Expanded via `source`, so a pattern matching nothing is not an error. +// Shared by startup and ifreload so the two cannot drift apart. +int load_namespace(const char *ns_name, char **patterns, size_t n); + +// Default search path. +#define LINKD_CONFIG_PATH "/etc/linkd.cnf" + +#endif // __LINKD_CONFIG_DAEMON_H__ diff --git a/src/config/ifaces.c b/src/config/ifaces.c @@ -9,8 +9,8 @@ #include "ifaces.h" -static struct unos_iface *ifaces = NULL; -static struct unos_iface *ifaces_tail = NULL; +static struct linkd_iface *ifaces = NULL; +static struct linkd_iface *ifaces_tail = NULL; // Allocate an iface with every "unset" field at its sentinel. // @@ -24,8 +24,8 @@ static struct unos_iface *ifaces_tail = NULL; // an existing device fails harmlessly. Centralising the sentinels here means a // new tri-state field cannot repeat that by being forgotten at one of the two // allocation sites. -static struct unos_iface * iface_new(const char *name) { - struct unos_iface *ifc = calloc(1, sizeof(*ifc)); +static struct linkd_iface * iface_new(const char *name) { + struct linkd_iface *ifc = calloc(1, sizeof(*ifc)); if (!ifc) return NULL; ifc->name = name ? strdup(name) : NULL; ifc->vlan_id = -1; @@ -36,11 +36,11 @@ static struct unos_iface * iface_new(const char *name) { // Is this interface a bridge? // -// Single source of truth: this was duplicated in cli/unosd.c and ipc.c, which +// Single source of truth: this was duplicated in cli/linkd.c and ipc.c, which // is how the two could drift. Explicit bridge-* directives win; the name // prefix is only a fallback, and never applies to something already declared // a VLAN or enslaved to a VRF. -bool iface_is_bridge(const struct unos_iface *ifc) { +bool iface_is_bridge(const struct linkd_iface *ifc) { if (!ifc) return false; if (ifc->bridge_ports) return true; if (ifc->bridge_stp) return true; @@ -52,7 +52,7 @@ bool iface_is_bridge(const struct unos_iface *ifc) { // Is this interface a VRF? `vrf-table` makes it one; `vrf <name>` makes it a // MEMBER of one, which is a different thing entirely. -bool iface_is_vrf(const struct unos_iface *ifc) { +bool iface_is_vrf(const struct linkd_iface *ifc) { return ifc && ifc->vrf_table >= 0; } @@ -65,7 +65,7 @@ bool iface_is_vrf(const struct unos_iface *ifc) { // claimed explicitly, so `auto` and a hardcoded id cannot collide. #define VRF_TABLE_AUTO_BASE 1001 static int vrf_table_auto(const char *name) { - struct unos_iface *cur; + struct linkd_iface *cur; int candidate; for (candidate = VRF_TABLE_AUTO_BASE; candidate < 2147483647; candidate++) { int taken = 0; @@ -76,16 +76,15 @@ static int vrf_table_auto(const char *name) { } if (!taken) return candidate; } - fprintf(stderr, "vrf-table auto: no free table id\n"); - exit(1); + return -1; } -struct unos_iface * ifaces_list(void) { +struct linkd_iface * ifaces_list(void) { return ifaces; } -struct unos_iface * ifaces_find(const char *name) { - struct unos_iface *cur; +struct linkd_iface * ifaces_find(const char *name) { + struct linkd_iface *cur; for (cur = ifaces; cur; cur = cur->next) { if (!strcmp(cur->name, name)) return cur; } @@ -93,7 +92,7 @@ struct unos_iface * ifaces_find(const char *name) { } void ifaces_set_auto(const char *name, int flag) { - struct unos_iface *cur = ifaces_find(name); + struct linkd_iface *cur = ifaces_find(name); if (cur) { cur->auto_flag = flag; return; @@ -110,8 +109,8 @@ void ifaces_set_auto(const char *name, int flag) { } void ifaces_free(void) { - struct unos_iface *cur = ifaces; - struct unos_iface *next; + struct linkd_iface *cur = ifaces; + struct linkd_iface *next; struct iface_addr *a, *anext; while (cur) { next = cur->next; @@ -142,7 +141,7 @@ void ifaces_free(void) { ifaces_tail = NULL; } -static void iface_add_addr(struct unos_iface *iface, const char *addr, const char *netmask) { +static void iface_add_addr(struct linkd_iface *iface, const char *addr, const char *netmask) { struct iface_addr *a = calloc(1, sizeof(*a)); a->address = strdup(addr); if (netmask) a->netmask = strdup(netmask); @@ -170,12 +169,12 @@ struct cnf_directive * cfg_parse_auto(FILE *fd, struct cnf_directive *dir, void // iface <name> [inet|inet6 <method>] -- cumulus style `iface <name>` preferred (no inet) struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void *user) { - struct unos_iface *iface; + struct linkd_iface *iface; (void)user; if (dir->argc < 1 || dir->argc > 3) { - fprintf(stderr, "`iface` directive: expected `iface <name>` or `iface <name> inet <method>`\n"); - exit(1); + cfg_error("`iface` directive: expected `iface <name>` or `iface <name> inet <method>`"); + goto fail; } const char *name = dir->argv[0]; @@ -190,12 +189,12 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void family = dir->argv[1]; method = dir->argv[2]; if (strcasecmp(family, "inet") && strcasecmp(family, "inet6")) { - fprintf(stderr, "`iface %s` unknown family: %s (expected inet/inet6)\n", name, family); - exit(1); + cfg_error("`iface %s` unknown family: %s (expected inet/inet6)", name, family); + goto fail; } } else { - fprintf(stderr, "`iface` directive: expected `iface <name>` or `iface <name> inet <method>`\n"); - exit(1); + cfg_error("`iface` directive: expected `iface <name>` or `iface <name> inet <method>`"); + goto fail; } // Find existing placeholder from auto, or create new @@ -258,8 +257,8 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void if(0) {} else if (!strcasecmp("address", dir->name)) { if (dir->argc < 1 || dir->argc > 2) { - fprintf(stderr, "`address` expects 1 or 2 args (address + optional netmask)\n"); - exit(1); + cfg_error("`address` expects 1 or 2 args (address + optional netmask)"); + goto fail; } const char *addr = dir->argv[0]; const char *nm = dir->argc > 1 ? dir->argv[1] : NULL; @@ -267,49 +266,49 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void } else if (!strcasecmp("netmask", dir->name)) { if (dir->argc != 1) { - fprintf(stderr, "`netmask` expects 1 arg\n"); - exit(1); + cfg_error("`netmask` expects 1 arg"); + goto fail; } // Attach netmask to last address without netmask struct iface_addr *a = iface->addrs; if (!a) { - fprintf(stderr, "`netmask` without preceding `address`\n"); - exit(1); + cfg_error("`netmask` without preceding `address`"); + goto fail; } while (a->next) a = a->next; free(a->netmask); a->netmask = strdup(dir->argv[0]); } else if (!strcasecmp("broadcast", dir->name)) { - if (dir->argc != 1) { fprintf(stderr, "`broadcast` expects 1 arg\n"); exit(1); } + if (dir->argc != 1) { cfg_error("`broadcast` expects 1 arg"); goto fail; } free(iface->broadcast); iface->broadcast = strdup(dir->argv[0]); } else if (!strcasecmp("gateway", dir->name)) { - if (dir->argc != 1) { fprintf(stderr, "`gateway` expects 1 arg\n"); exit(1); } + if (dir->argc != 1) { cfg_error("`gateway` expects 1 arg"); goto fail; } free(iface->gateway); iface->gateway = strdup(dir->argv[0]); } else if (!strcasecmp("mtu", dir->name)) { - if (dir->argc != 1) { fprintf(stderr, "`mtu` expects 1 arg\n"); exit(1); } + if (dir->argc != 1) { cfg_error("`mtu` expects 1 arg"); goto fail; } iface->mtu = atoi(dir->argv[0]); } else if (!strcasecmp("hwaddress", dir->name) || !strcasecmp("hw-address", dir->name)) { - if (dir->argc != 1) { fprintf(stderr, "`hwaddress` expects 1 arg\n"); exit(1); } + if (dir->argc != 1) { cfg_error("`hwaddress` expects 1 arg"); goto fail; } free(iface->hwaddress); iface->hwaddress = strdup(dir->argv[0]); } else if (!strcasecmp("vlan-raw-device", dir->name) || !strcasecmp("vlan_raw_device", dir->name)) { - if (dir->argc != 1) { fprintf(stderr, "`vlan-raw-device` expects 1 arg\n"); exit(1); } + if (dir->argc != 1) { cfg_error("`vlan-raw-device` expects 1 arg"); goto fail; } free(iface->vlan_raw_device); iface->vlan_raw_device = strdup(dir->argv[0]); } else if (!strcasecmp("vlan-id", dir->name) || !strcasecmp("vlan_id", dir->name)) { - if (dir->argc != 1) { fprintf(stderr, "`vlan-id` expects 1 arg\n"); exit(1); } + if (dir->argc != 1) { cfg_error("`vlan-id` expects 1 arg"); goto fail; } iface->vlan_id = atoi(dir->argv[0]); } else if (!strcasecmp("pre-up", dir->name) || !strcasecmp("pre_up", dir->name)) { - if (dir->argc < 1) { fprintf(stderr, "`pre-up` expects at least 1 arg\n"); exit(1); } + if (dir->argc < 1) { cfg_error("`pre-up` expects at least 1 arg"); goto fail; } // Join argv with spaces size_t len = 0; for (int i=0;i<(int)dir->argc;i++) len += strlen(dir->argv[i])+1; @@ -322,7 +321,7 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void iface->pre_up = cmd; } else if (!strcasecmp("post-up", dir->name) || !strcasecmp("post_up", dir->name)) { - if (dir->argc < 1) { fprintf(stderr, "`post-up` expects at least 1 arg\n"); exit(1); } + if (dir->argc < 1) { cfg_error("`post-up` expects at least 1 arg"); goto fail; } 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); @@ -334,7 +333,7 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void iface->post_up = cmd; } else if (!strcasecmp("pre-down", dir->name) || !strcasecmp("pre_down", dir->name)) { - if (dir->argc < 1) { fprintf(stderr, "`pre-down` expects at least 1 arg\n"); exit(1); } + if (dir->argc < 1) { cfg_error("`pre-down` expects at least 1 arg"); goto fail; } 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); @@ -346,7 +345,7 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void iface->pre_down = cmd; } else if (!strcasecmp("post-down", dir->name) || !strcasecmp("post_down", dir->name)) { - if (dir->argc < 1) { fprintf(stderr, "`post-down` expects at least 1 arg\n"); exit(1); } + if (dir->argc < 1) { cfg_error("`post-down` expects at least 1 arg"); goto fail; } 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); @@ -359,7 +358,7 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void } else if (!strcasecmp("up", dir->name)) { // alias for post-up - if (dir->argc < 1) { fprintf(stderr, "`up` expects at least 1 arg\n"); exit(1); } + if (dir->argc < 1) { cfg_error("`up` expects at least 1 arg"); goto fail; } 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); @@ -371,7 +370,7 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void iface->post_up = cmd; } else if (!strcasecmp("down", dir->name)) { - if (dir->argc < 1) { fprintf(stderr, "`down` expects at least 1 arg\n"); exit(1); } + if (dir->argc < 1) { cfg_error("`down` expects at least 1 arg"); goto fail; } 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); @@ -383,7 +382,7 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void 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); } + if (dir->argc < 1) { cfg_error("`bridge-ports` expects at least 1 arg"); goto fail; } 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); @@ -395,19 +394,19 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void 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); } + if (dir->argc != 1) { cfg_error("`bridge-stp` expects 1 arg (on/off)"); goto fail; } 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 (dir->argc != 1) { cfg_error("`bridge-vlan-aware` expects 1 arg (yes/no)"); goto fail; } 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 { cfg_error("`bridge-vlan-aware` expects yes/no"); goto fail; } } // `vrf <name>`: enslave this interface to an existing VRF device. else if (!strcasecmp("vrf", dir->name)) { - if (dir->argc != 1) { fprintf(stderr, "`vrf` expects 1 arg (vrf device name)\n"); exit(1); } + if (dir->argc != 1) { cfg_error("`vrf` expects 1 arg (vrf device name)"); goto fail; } free(iface->vrf_master); iface->vrf_master = strdup(dir->argv[0]); } @@ -415,19 +414,23 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void // Cumulus spells the automatic case `auto`; we allocate from a private // range rather than colliding with main(254)/local(255)/default(253). else if (!strcasecmp("vrf-table", dir->name) || !strcasecmp("vrf_table", dir->name)) { - if (dir->argc != 1) { fprintf(stderr, "`vrf-table` expects 1 arg (table id or `auto`)\n"); exit(1); } + if (dir->argc != 1) { cfg_error("`vrf-table` expects 1 arg (table id or `auto`)"); goto fail; } if (!strcasecmp(dir->argv[0], "auto")) { iface->vrf_table = vrf_table_auto(iface->name); + if (iface->vrf_table < 0) { + cfg_error("`vrf-table auto`: no free table id"); + goto fail; + } } else { char *end = NULL; long v = strtol(dir->argv[0], &end, 10); if (!end || *end || v <= 0 || v > 2147483647L) { - fprintf(stderr, "`vrf-table` expects a positive table id or `auto`, got `%s`\n", dir->argv[0]); - exit(1); + cfg_error("`vrf-table` expects a positive table id or `auto`, got `%s`", dir->argv[0]); + goto fail; } if (v == 253 || v == 254 || v == 255) { - fprintf(stderr, "`vrf-table` %ld is a reserved table (default/main/local)\n", v); - exit(1); + cfg_error("`vrf-table` %ld is a reserved table (default/main/local)", v); + goto fail; } iface->vrf_table = (int)v; } @@ -438,6 +441,12 @@ struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void } return dir; + +// The iface is already linked into the module list by this point, so +// ifaces_free() owns it; only the directive needs releasing here. +fail: + cnf_directive_free(dir); + return NULL; } void ifaces_register(void) { diff --git a/src/config/ifaces.h b/src/config/ifaces.h @@ -1,5 +1,5 @@ -#ifndef __UNOS_CONFIG_IFACES_H__ -#define __UNOS_CONFIG_IFACES_H__ +#ifndef __LINKD_CONFIG_IFACES_H__ +#define __LINKD_CONFIG_IFACES_H__ #include <stdbool.h> #include <stdint.h> @@ -16,7 +16,7 @@ struct iface_addr { struct iface_addr *next; }; -struct unos_iface { +struct linkd_iface { char *name; // e.g., "eth0", "swp1.100" -- cumulus style `iface <name>` preferred char *method; // "static", "dhcp", "loopback", "manual" -- optional, defaults to "static" when `iface <name>` used without `inet` char *family; // "inet", "inet6" -- optional, NULL when cumulus style @@ -44,22 +44,22 @@ struct unos_iface { int vrf_table; // -1 unset, otherwise 1..2^31-1 char *vrf_master; // NULL unset int auto_flag; // 1 if listed in auto - struct unos_iface *next; + struct linkd_iface *next; }; -// Shared classification. Previously duplicated in cli/unosd.c and ipc.c, which +// Shared classification. Previously duplicated in cli/linkd.c and ipc.c, which // let the two drift; keep exactly one definition. -bool iface_is_bridge(const struct unos_iface *ifc); -bool iface_is_vrf(const struct unos_iface *ifc); +bool iface_is_bridge(const struct linkd_iface *ifc); +bool iface_is_vrf(const struct linkd_iface *ifc); void ifaces_register(void); struct cnf_directive * cfg_parse_auto(FILE *fd, struct cnf_directive *dir, void *user); struct cnf_directive * cfg_parse_iface(FILE *fd, struct cnf_directive *dir, void *user); -struct unos_iface * ifaces_list(void); -struct unos_iface * ifaces_find(const char *name); +struct linkd_iface * ifaces_list(void); +struct linkd_iface * ifaces_find(const char *name); void ifaces_free(void); void ifaces_set_auto(const char *name, int flag); -#endif // __UNOS_CONFIG_IFACES_H__ +#endif // __LINKD_CONFIG_IFACES_H__ diff --git a/src/config/ports.c b/src/config/ports.c @@ -9,15 +9,15 @@ #include "ports.h" -static struct unos_port *ports = NULL; -static struct unos_port *ports_tail = NULL; +static struct linkd_port *ports = NULL; +static struct linkd_port *ports_tail = NULL; -struct unos_port * ports_list(void) { +struct linkd_port * ports_list(void) { return ports; } -struct unos_port * ports_find(const char *name) { - struct unos_port *cur; +struct linkd_port * ports_find(const char *name) { + struct linkd_port *cur; for ( cur = ports ; cur ; cur = cur->next ) { if (!strcmp(cur->name, name)) return cur; } @@ -25,8 +25,8 @@ struct unos_port * ports_find(const char *name) { } void ports_free(void) { - struct unos_port *cur = ports; - struct unos_port *next; + struct linkd_port *cur = ports; + struct linkd_port *next; while (cur) { next = cur->next; @@ -75,21 +75,30 @@ static int ports_is_reserved(const char *name) { return 0; } +static void port_free(struct linkd_port *port) { + if (!port) return; + free(port->name); + free(port->breakout); + free(port); +} + struct cnf_directive * cfg_parse_port(FILE *fd, struct cnf_directive *dir, void *user) { - struct unos_port *port; + struct linkd_port *port; (void)user; if (dir->argc != 1) { - fprintf(stderr, "`port` directive accepts only 1 argument\n"); - exit(1); + cfg_error("`port` directive accepts only 1 argument"); + cnf_directive_free(dir); + return NULL; } if (ports_find(dir->argv[0])) { - fprintf(stderr, "duplicate `port` definition: %s\n", dir->argv[0]); - exit(1); + cfg_error("duplicate `port` definition: %s", dir->argv[0]); + cnf_directive_free(dir); + return NULL; } - port = calloc(1, sizeof(struct unos_port)); + port = calloc(1, sizeof(struct linkd_port)); port->name = strdup(dir->argv[0]); port->fec = DP_FEC_UNSET; port->autoneg = -1; @@ -104,47 +113,44 @@ struct cnf_directive * cfg_parse_port(FILE *fd, struct cnf_directive *dir, void else if (!strcasecmp("speed", dir->name)) { if (dir->argc != 1) { - fprintf(stderr, "`speed` directive accepts only 1 argument\n"); - exit(1); + cfg_error("`speed` directive accepts only 1 argument"); + goto fail; } port->speed = (uint32_t)strtoul(dir->argv[0], NULL, 10); } else if (!strcasecmp("fec", dir->name)) { if (dir->argc != 1) { - fprintf(stderr, "`fec` directive accepts only 1 argument\n"); - exit(1); + cfg_error("`fec` directive accepts only 1 argument"); + goto fail; } port->fec = ports_parse_fec(dir->argv[0]); if (port->fec == DP_FEC_UNSET) { - fprintf(stderr, "unknown `fec` value: %s\n", dir->argv[0]); - exit(1); + cfg_error("unknown `fec` value: %s", dir->argv[0]); + goto fail; } } else if (!strcasecmp("autoneg", dir->name)) { if (dir->argc != 1) { - fprintf(stderr, "`autoneg` directive accepts only 1 argument\n"); - exit(1); + cfg_error("`autoneg` directive accepts only 1 argument"); + goto fail; } port->autoneg = ports_parse_bool(dir->argv[0]); } else if (!strcasecmp("breakout", dir->name)) { if (dir->argc != 1) { - fprintf(stderr, "`breakout` directive accepts only 1 argument\n"); - exit(1); + cfg_error("`breakout` directive accepts only 1 argument"); + goto fail; } if (port->breakout) free(port->breakout); port->breakout = strdup(dir->argv[0]); } else if (ports_is_reserved(dir->name)) { - fprintf(stderr, - "`%s` is handled by ifupdown; configure it in " - "/etc/network/interfaces.d/ instead of ports.cnf\n", - dir->name); - exit(1); + cfg_error("`%s` is handled by the interfaces config, not ports", dir->name); + goto fail; } else { @@ -163,6 +169,11 @@ struct cnf_directive * cfg_parse_port(FILE *fd, struct cnf_directive *dir, void // Return the unparsed dir if set return dir; + +fail: + port_free(port); + cnf_directive_free(dir); + return NULL; } void ports_register(void) { diff --git a/src/config/ports.h b/src/config/ports.h @@ -1,5 +1,5 @@ -#ifndef __UNOS_CONFIG_PORTS_H__ -#define __UNOS_CONFIG_PORTS_H__ +#ifndef __LINKD_CONFIG_PORTS_H__ +#define __LINKD_CONFIG_PORTS_H__ #include <stdint.h> #include <stdio.h> @@ -14,13 +14,13 @@ // handled by ifupdown via /etc/network/interfaces{,.d}; re-implementing them // here would mean two sources of truth for the same value. What remains is // what ifupdown cannot reach on a KNET stub netdev. -struct unos_port { +struct linkd_port { char *name; uint32_t speed; // Mbit/s, 0 = unset enum dp_fec fec; int autoneg; // -1 unset, 0 off, 1 on char *breakout; // e.g. "4x25g", NULL = none - struct unos_port *next; + struct linkd_port *next; }; // Registers the `port` directive with the config parser @@ -28,8 +28,8 @@ void ports_register(void); struct cnf_directive * cfg_parse_port(FILE *fd, struct cnf_directive *dir, void *user); -struct unos_port * ports_list(void); -struct unos_port * ports_find(const char *name); +struct linkd_port * ports_list(void); +struct linkd_port * ports_find(const char *name); void ports_free(void); -#endif // __UNOS_CONFIG_PORTS_H__ +#endif // __LINKD_CONFIG_PORTS_H__ diff --git a/src/dataplane.h b/src/dataplane.h @@ -1,5 +1,5 @@ -#ifndef __UNOS_DATAPLANE_H__ -#define __UNOS_DATAPLANE_H__ +#ifndef __LINKD_DATAPLANE_H__ +#define __LINKD_DATAPLANE_H__ #include <stdbool.h> #include <stddef.h> @@ -12,15 +12,15 @@ // Until then the structs below are fluid: extend freely without bumping. // Plugins declaring a different ABI are refused at load time rather than // crashing us later. -#define UNOS_DATAPLANE_ABI 1 +#define LINKD_DATAPLANE_ABI 1 // Where dlopen'd backends live. The openbcm package drops its backend here; // on hardware without a switching ASIC the directory is simply empty and we // fall back to the built-in kernel dataplane. -#define UNOS_DATAPLANE_DIR "/usr/lib/unos/dataplane" +#define LINKD_DATAPLANE_DIR "/usr/lib/linkd/dataplane" // Symbol every plugin must export -#define UNOS_DATAPLANE_SYM "unos_dataplane_ops" +#define LINKD_DATAPLANE_SYM "linkd_dataplane_ops" enum dp_fec { DP_FEC_UNSET = 0, @@ -36,7 +36,7 @@ struct dp_addr { uint8_t prefixlen; }; -// Physical-layer properties of a switch port, from /etc/unos/ports.cnf. +// Physical-layer properties of a switch port, from /etc/linkd/ports.cnf. // // Deliberately narrow: anything the kernel already models on a netdev (MTU, // admin state, addresses, MAC) is owned by ifupdown and reaches us through @@ -113,7 +113,7 @@ struct dp_ops { // Built-in backend. Always available, always probes successfully. const struct dp_ops * dp_kernel_ops(void); -// Load plugins from UNOS_DATAPLANE_DIR. Safe to call when the directory is +// Load plugins from LINKD_DATAPLANE_DIR. Safe to call when the directory is // absent or empty. int dp_plugins_load(const char *dir); @@ -126,4 +126,4 @@ const struct dp_ops * dp_active(void); int dp_init(void); void dp_fini(void); -#endif // __UNOS_DATAPLANE_H__ +#endif // __LINKD_DATAPLANE_H__ diff --git a/src/dataplane/kernel.c b/src/dataplane/kernel.c @@ -11,7 +11,7 @@ // - speed / FEC / autoneg are PHY properties of a real NIC, driven through // ethtool, and meaningless in a VM // -// Mirroring any of it back into the kernel would be circular. unosd on generic +// Mirroring any of it back into the kernel would be circular. linkd on generic // hardware is therefore close to inert by design: it parses configuration, // validates it, and lets Linux do the work. // @@ -51,7 +51,7 @@ static int kernel_resync(void) { } static const struct dp_ops kernel_ops = { - .abi = UNOS_DATAPLANE_ABI, + .abi = LINKD_DATAPLANE_ABI, .name = "kernel", .probe = kernel_probe, .init = kernel_init, diff --git a/src/dataplane/plugin.c b/src/dataplane/plugin.c @@ -1,11 +1,11 @@ // Dataplane plugin loader. // -// A UNOS image is identical on every machine. Hardware support arrives as a +// A single linkd image is identical on every machine. Hardware support arrives as a // package: on a Broadcom box the `openbcm` package installs its kernel modules -// and drops a backend into UNOS_DATAPLANE_DIR. On anything else the directory +// and drops a backend into LINKD_DATAPLANE_DIR. On anything else the directory // is absent or empty and we quietly fall back to the built-in kernel backend. // -// A plugin is a shared object exporting `struct dp_ops unos_dataplane_ops`. +// A plugin is a shared object exporting `struct dp_ops linkd_dataplane_ops`. #include <dirent.h> #include <dlfcn.h> @@ -25,7 +25,7 @@ int dp_plugins_load(const char *dir) { size_t len; int loaded = 0; - if (!dir) dir = UNOS_DATAPLANE_DIR; + if (!dir) dir = LINKD_DATAPLANE_DIR; dh = opendir(dir); if (!dh) { @@ -46,9 +46,9 @@ int dp_plugins_load(const char *dir) { continue; } - ops = dlsym(handle, UNOS_DATAPLANE_SYM); + ops = dlsym(handle, LINKD_DATAPLANE_SYM); if (!ops) { - fprintf(stderr, "dataplane: %s: missing symbol `%s`\n", path, UNOS_DATAPLANE_SYM); + fprintf(stderr, "dataplane: %s: missing symbol `%s`\n", path, LINKD_DATAPLANE_SYM); dlclose(handle); continue; } diff --git a/src/dataplane/registry.c b/src/dataplane/registry.c @@ -15,9 +15,9 @@ static const struct dp_ops *active = NULL; int dp_register(const struct dp_ops *ops) { if (!ops) return DP_RET_ERROR; - if (ops->abi != UNOS_DATAPLANE_ABI) { + if (ops->abi != LINKD_DATAPLANE_ABI) { fprintf(stderr, "dataplane: refusing backend `%s`: abi %u, expected %u\n", - ops->name ? ops->name : "(unnamed)", ops->abi, UNOS_DATAPLANE_ABI); + ops->name ? ops->name : "(unnamed)", ops->abi, LINKD_DATAPLANE_ABI); return DP_RET_ERROR; } diff --git a/src/dataplane/registry.h b/src/dataplane/registry.h @@ -1,5 +1,5 @@ -#ifndef __UNOS_DATAPLANE_REGISTRY_H__ -#define __UNOS_DATAPLANE_REGISTRY_H__ +#ifndef __LINKD_DATAPLANE_REGISTRY_H__ +#define __LINKD_DATAPLANE_REGISTRY_H__ #include "dataplane.h" @@ -7,4 +7,4 @@ // match or which omit mandatory entry points. int dp_register(const struct dp_ops *ops); -#endif // __UNOS_DATAPLANE_REGISTRY_H__ +#endif // __LINKD_DATAPLANE_REGISTRY_H__ diff --git a/src/ipc.c b/src/ipc.c @@ -1,4 +1,5 @@ #define _GNU_SOURCE +#include <libgen.h> #include <errno.h> #include <net/if.h> #include <stdio.h> @@ -14,6 +15,7 @@ #include "rxi/log.h" #include "util/config.h" +#include "config/daemon.h" #include "config/ifaces.h" #include "config/ports.h" #include "ipc.h" @@ -21,6 +23,7 @@ #include "netlink/rtnl.h" static int ipc_sock = -1; +static const char *ipc_path = NULL; int ipc_fd(void) { return ipc_sock; @@ -28,9 +31,19 @@ int ipc_fd(void) { int ipc_init(void) { struct sockaddr_un addr; - // Ensure /run exists (tmpfs mount in rcS, but early boot may not have it) - mkdir("/run", 0755); - unlink(UNOS_IPC_PATH); + + ipc_path = daemon_cfg_unix_socket(); + if (!ipc_path) { + log_error("ipc: no unix:// listen address configured"); + return -1; + } + + // Create the socket's directory; early boot may not have mounted it yet. + { + char *d = strdup(ipc_path); + if (d) { mkdir(dirname(d), 0755); free(d); } + } + unlink(ipc_path); ipc_sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); if (ipc_sock < 0) { @@ -40,28 +53,28 @@ int ipc_init(void) { memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, UNOS_IPC_PATH, sizeof(addr.sun_path)-1); + strncpy(addr.sun_path, ipc_path, sizeof(addr.sun_path)-1); if (bind(ipc_sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) { - log_error("ipc: bind %s failed: %s", UNOS_IPC_PATH, strerror(errno)); + log_error("ipc: bind %s failed: %s", ipc_path, strerror(errno)); close(ipc_sock); ipc_sock = -1; return -1; } - if (chmod(UNOS_IPC_PATH, 0600) != 0) { - log_warn("ipc: chmod %s failed: %s", UNOS_IPC_PATH, strerror(errno)); + if (chmod(ipc_path, 0600) != 0) { + log_warn("ipc: chmod %s failed: %s", ipc_path, strerror(errno)); } if (listen(ipc_sock, 8) != 0) { log_error("ipc: listen failed: %s", strerror(errno)); close(ipc_sock); ipc_sock = -1; - unlink(UNOS_IPC_PATH); + unlink(ipc_path); return -1; } - log_info("ipc: listening on %s", UNOS_IPC_PATH); + log_info("ipc: listening on %s", ipc_path); return 0; } @@ -70,7 +83,7 @@ void ipc_fini(void) { close(ipc_sock); ipc_sock = -1; } - unlink(UNOS_IPC_PATH); + unlink(ipc_path); } // Helpers to send response @@ -199,7 +212,7 @@ int ipc_handle(void) { return -1; } - // Blocking, but never indefinitely: unosd is single-threaded, so a client that + // Blocking, but never indefinitely: linkd 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. { @@ -278,7 +291,7 @@ static int run_hook(const char *cmd) { static int handle_ifup(FILE *out, const char *ifname, uid_t uid) { (void)uid; - struct unos_iface *iface = ifaces_find(ifname); + struct linkd_iface *iface = ifaces_find(ifname); if (!iface) { log_info("ipc: ifup %s (not in interfaces, just bringing link up)", ifname); if (rtnl_link_up(ifname) != 0) { @@ -384,7 +397,7 @@ static int handle_ifup(FILE *out, const char *ifname, uid_t uid) { static int handle_ifdown(FILE *out, const char *ifname, uid_t uid) { (void)uid; - struct unos_iface *iface = ifaces_find(ifname); + struct linkd_iface *iface = ifaces_find(ifname); if (!iface) { log_info("ipc: ifdown %s (not in interfaces, just bringing link down)", ifname); if (rtnl_link_down(ifname) != 0) { @@ -410,62 +423,37 @@ static int handle_ifdown(FILE *out, const char *ifname, uid_t uid) { return 0; } +static int count_ifaces(void) { + int n = 0; + for (struct linkd_iface *c = ifaces_list(); c; c = c->next) n++; + return n; +} + static int handle_ifreload(FILE *out, uid_t uid) { (void)uid; - 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; - })); + log_info("ipc: ifreload (diff-apply) start, old list has %d", count_ifaces()); // 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 (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"; - char path[1024]; - char stub[1024]; - FILE *fd; - snprintf(path, sizeof(path), "%s/interfaces", dir); - if (access(path, R_OK) == 0) { - fd = fopen(path, "r"); - if (fd) { - cfg_parse(cfg_ns_get("interfaces"), dir, fd, NULL); - fclose(fd); - } - } else { - snprintf(stub, sizeof(stub), "source interfaces.d/*.cnf\n"); - fd = fmemopen(stub, strlen(stub), "r"); - if (fd) { - cfg_parse(cfg_ns_get("interfaces"), dir, fd, NULL); - fclose(fd); - } + log_info("ipc: ifreload after free, list has %d", count_ifaces()); + + // Same loader startup uses. Was a private copy hardcoding /etc/network, + // so --config was silently ignored here. + const struct linkd_daemon_cfg *dcfg = daemon_cfg(); + if (load_namespace("interfaces", dcfg->iface, dcfg->iface_n) < 0) { + ipc_send_err(out, "ifreload: failed to reload interface configuration"); + return -1; } - // Also reload ports? - snprintf(path, sizeof(path), "%s/ports", dir); - if (access(path, R_OK) == 0) { - fd = fopen(path, "r"); - if (fd) { - cfg_parse(cfg_ns_get("ports"), dir, fd, NULL); - fclose(fd); - } - } else { - snprintf(stub, sizeof(stub), "source ports.d/*.cnf\n"); - fd = fmemopen(stub, strlen(stub), "r"); - if (fd) { - cfg_parse(cfg_ns_get("ports"), dir, fd, NULL); - fclose(fd); - } + if (load_namespace("ports", dcfg->ports, dcfg->ports_n) < 0) { + ipc_send_err(out, "ifreload: failed to reload port configuration"); + return -1; } { int n = 0; - for (struct unos_iface *c = ifaces_list(); c; c = c->next) n++; + for (struct linkd_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) { + for (struct linkd_iface *c = ifaces_list(); c; c = c->next) { log_info("ipc: ifreload iface %s", c->name); } } @@ -473,7 +461,7 @@ static int handle_ifreload(FILE *out, uid_t uid) { } static int handle_ifquery(FILE *out, const char *ifname) { - struct unos_iface *cur; + struct linkd_iface *cur; int n = 0; for (cur = ifaces_list(); cur; cur = cur->next) n++; log_info("ipc: ifquery %s (list has %d)", ifname ? ifname : "(all)", n); diff --git a/src/ipc.h b/src/ipc.h @@ -1,13 +1,12 @@ -#ifndef __UNOS_IPC_H__ -#define __UNOS_IPC_H__ +#ifndef __LINKD_IPC_H__ +#define __LINKD_IPC_H__ -#define UNOS_IPC_PATH "/run/unosd.sock" -// Server side (unosd) +// Server side (linkd) int ipc_init(void); int ipc_fd(void); int ipc_handle(void); // accept and handle one client, returns 0 on handled, -1 on error void ipc_fini(void); void ipc_register(void); -#endif // __UNOS_IPC_H__ +#endif // __LINKD_IPC_H__ diff --git a/src/main.c b/src/main.c @@ -6,55 +6,49 @@ #include "cli/registry.h" +// Multicall: basename(argv[0]) selects which main runs. Commands self-register +// from constructors in src/cli/. linkctl is the exception -- it takes its +// subcommand from argv[1]. + +static const char *AVAILABLE = "linkd, linkctl, ifup, ifdown, ifquery, ifreload"; + +// Shift argv left by one so the callee sees its own name in argv[0]. +static int dispatch_shifted(cli_main_fn fn, int argc, char *argv[]) { + char **shifted = malloc(sizeof(char *) * (size_t)argc); + if (!shifted) { + fprintf(stderr, "out of memory\n"); + return 1; + } + shifted[0] = argv[1]; + for (int i = 2; i < argc; i++) shifted[i - 1] = argv[i]; + int rc = fn(argc - 1, shifted); + free(shifted); + return rc; +} + 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); + + // linkctl <command> [args...] + if (!strcmp(prog, "linkctl") && argc > 1) { + cli_main_fn fn = cli_find(argv[1]); if (!fn) { - fprintf(stderr, "unosc: unknown command: %s\n", prog); - fprintf(stderr, "available: ifup, ifdown, ifquery, ifreload, unosd, unosc\n"); - free(new_argv); + fprintf(stderr, "linkctl: unknown command: %s\n", argv[1]); + fprintf(stderr, "available: %s\n", AVAILABLE); return 1; } - int rc = fn(new_argc, new_argv); - free(new_argv); - return rc; + return dispatch_shifted(fn, argc, argv); } cli_main_fn fn = cli_find(prog); - if (fn) { - return fn(argc, argv); - } + if (fn) return fn(argc, argv); - // 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; - } + // Unrecognised name (build tree, wrapper script): try argv[1]. + if (argc > 1 && (fn = cli_find(argv[1]))) { + return dispatch_shifted(fn, argc, argv); } fprintf(stderr, "%s: unknown command: %s\n", argv[0], prog); - fprintf(stderr, "available commands: unosd, unosc, ifup, ifdown, ifquery, ifreload\n"); + fprintf(stderr, "available commands: %s\n", AVAILABLE); return 1; } diff --git a/src/netlink/filter.h b/src/netlink/filter.h @@ -1,5 +1,5 @@ -#ifndef __UNOS_NETLINK_FILTER_H__ -#define __UNOS_NETLINK_FILTER_H__ +#ifndef __LINKD_NETLINK_FILTER_H__ +#define __LINKD_NETLINK_FILTER_H__ #include <stdbool.h> #include <stdint.h> @@ -28,4 +28,4 @@ bool nl_filter_mgmt_name(const char *ifname); bool nl_table_allowed(uint32_t table); bool nl_route_allowed(int family, uint8_t type, uint8_t scope, uint32_t table); -#endif // __UNOS_NETLINK_FILTER_H__ +#endif // __LINKD_NETLINK_FILTER_H__ diff --git a/src/netlink/netlink.h b/src/netlink/netlink.h @@ -1,5 +1,5 @@ -#ifndef __UNOS_NETLINK_H__ -#define __UNOS_NETLINK_H__ +#ifndef __LINKD_NETLINK_H__ +#define __LINKD_NETLINK_H__ #include <stdint.h> @@ -15,4 +15,4 @@ void nl_request_stop(void); // async-signal-safe stop fl int nl_dump(int fd, uint16_t type, int family); // one GET*+DUMP, replies via handle_nlmsg int nl_resync(int fd); // filter_reset + dumps + dp resync; 0 ok, -1 fail -#endif // __UNOS_NETLINK_H__ +#endif // __LINKD_NETLINK_H__ diff --git a/src/netlink/rtnl.c b/src/netlink/rtnl.c @@ -101,8 +101,13 @@ static int addattr_l(struct nlmsghdr *n, int maxlen, int type, const void *data, return 0; } +// Returns the nest's byte offset for addattr_nested_end(), or -1 on overflow. static int addattr_nested_start(struct nlmsghdr *n, int maxlen, int type) { struct rtattr *rta = (struct rtattr*)(((char*)n) + NLMSG_ALIGN(n->nlmsg_len)); + if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(RTA_LENGTH(0)) > (unsigned)maxlen) { + log_error("rtnl: addattr_nested_start overflow"); + return -1; + } rta->rta_type = type; rta->rta_len = RTA_LENGTH(0); n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(sizeof(*rta)); @@ -377,8 +382,10 @@ int rtnl_vlan_create(const char *name, const char *rawdev, int vlan_id) { 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); + if (nest < 0) return -1; addattr_l(nh, sizeof(buf), IFLA_INFO_KIND, "vlan", 5); int nest2 = addattr_nested_start(nh, sizeof(buf), IFLA_INFO_DATA); + if (nest2 < 0) return -1; addattr_l(nh, sizeof(buf), IFLA_VLAN_ID, &vlan_id, sizeof(vlan_id)); addattr_nested_end(nh, nest2); addattr_nested_end(nh, nest); @@ -401,6 +408,7 @@ int rtnl_bridge_create(const char *name) { 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); + if (nest < 0) return -1; addattr_l(nh, sizeof(buf), IFLA_INFO_KIND, "bridge", 7); addattr_nested_end(nh, nest); return rtnl_talk(nh); @@ -429,8 +437,10 @@ int rtnl_vrf_create(const char *name, uint32_t table) { 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); + if (nest < 0) return -1; addattr_l(nh, sizeof(buf), IFLA_INFO_KIND, "vrf", 4); int nest2 = addattr_nested_start(nh, sizeof(buf), IFLA_INFO_DATA); + if (nest2 < 0) return -1; addattr_l(nh, sizeof(buf), IFLA_VRF_TABLE, &table, sizeof(table)); addattr_nested_end(nh, nest2); addattr_nested_end(nh, nest); @@ -476,8 +486,10 @@ int rtnl_bridge_set_stp(const char *name, bool on) { ifi->ifi_family = AF_UNSPEC; ifi->ifi_index = ifindex; int nest = addattr_nested_start(nh, sizeof(buf), IFLA_LINKINFO); + if (nest < 0) return -1; addattr_l(nh, sizeof(buf), IFLA_INFO_KIND, "bridge", 7); int nest2 = addattr_nested_start(nh, sizeof(buf), IFLA_INFO_DATA); + if (nest2 < 0) return -1; uint8_t stp = on ? 1 : 0; addattr_l(nh, sizeof(buf), IFLA_BR_STP_STATE, &stp, sizeof(stp)); addattr_nested_end(nh, nest2); @@ -499,8 +511,10 @@ int rtnl_bridge_set_vlan_aware(const char *name, bool on) { ifi->ifi_family = AF_UNSPEC; ifi->ifi_index = ifindex; int nest = addattr_nested_start(nh, sizeof(buf), IFLA_LINKINFO); + if (nest < 0) return -1; addattr_l(nh, sizeof(buf), IFLA_INFO_KIND, "bridge", 7); int nest2 = addattr_nested_start(nh, sizeof(buf), IFLA_INFO_DATA); + if (nest2 < 0) return -1; uint8_t vlan = on ? 1 : 0; addattr_l(nh, sizeof(buf), IFLA_BR_VLAN_FILTERING, &vlan, sizeof(vlan)); addattr_nested_end(nh, nest2); diff --git a/src/netlink/rtnl.h b/src/netlink/rtnl.h @@ -1,5 +1,5 @@ -#ifndef __UNOS_RTRNL_H__ -#define __UNOS_RTRNL_H__ +#ifndef __LINKD_RTRNL_H__ +#define __LINKD_RTRNL_H__ #include <stdbool.h> @@ -34,4 +34,4 @@ 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__ +#endif // __LINKD_RTRNL_H__ diff --git a/src/util/config.c b/src/util/config.c @@ -1,5 +1,6 @@ #include <glob.h> #include <libgen.h> +#include <stdarg.h> #include <stdlib.h> #include <string.h> #include <strings.h> @@ -23,6 +24,21 @@ static int ns_count = 0; static struct cnf_directive *cfg_handle_source_directory(FILE *fd, struct cnf_directive *dir, void *user); +static int cfg_errored = 0; + +void cfg_error(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + fprintf(stderr, "config: "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + va_end(ap); + cfg_errored = 1; +} + +int cfg_error_occurred(void) { return cfg_errored; } +void cfg_error_reset(void) { cfg_errored = 0; } + struct cfg_ns *cfg_ns_get(const char *name) { int i; for (i = 0; i < ns_count; i++) { @@ -210,6 +226,10 @@ int cfg_parse(struct cfg_ns *ns, const char *wd, FILE *fd, void *user) { for(i = 0 ; i < ns->count ; i++) { if (!strcasecmp(ns->h_name[i], dir->name)) { dir = ns->h_fn[i](fd, dir, user); + if (cfg_error_occurred()) { + if (dir) cnf_directive_free(dir); + return CFG_RET_ERROR; + } if (dir) goto cfg_parse_reparse; break; } @@ -218,6 +238,7 @@ int cfg_parse(struct cfg_ns *ns, const char *wd, FILE *fd, void *user) { if (i == ns->count) { // Here = not found fprintf(stderr, "Unknown directive: %s\n", dir->name); + cnf_directive_free(dir); return CFG_RET_ERROR; } } diff --git a/src/util/config.h b/src/util/config.h @@ -1,5 +1,5 @@ -#ifndef __UNOS_UTIL_CONFIG_H__ -#define __UNOS_UTIL_CONFIG_H__ +#ifndef __LINKD_UTIL_CONFIG_H__ +#define __LINKD_UTIL_CONFIG_H__ #include <stdio.h> @@ -13,6 +13,13 @@ // re-dispatch it. Returning NULL means end-of-input was reached. typedef struct cnf_directive * (*cfg_directive_fn)(FILE *fd, struct cnf_directive *dir, void *user); +// Report a malformed directive. The handler return type has no error channel +// (NULL means "consumed"), so this flag is how a handler rejects input without +// exit(1) -- which would kill the daemon on an ifreload. +void cfg_error(const char *fmt, ...); +int cfg_error_occurred(void); +void cfg_error_reset(void); + struct cfg_ns; // Create+get interned namespace (idempotent) @@ -23,4 +30,4 @@ void cfg_register_directive(struct cfg_ns *ns, const char *name, cfg_directive_f // `wd` is the directory relative `source` patterns resolve against. int cfg_parse(struct cfg_ns *ns, const char *wd, FILE *fd, void *user); -#endif // __UNOS_UTIL_CONFIG_H__ +#endif // __LINKD_UTIL_CONFIG_H__ diff --git a/target/common/Makefile b/target/common/Makefile @@ -1,6 +1,6 @@ TARGET?= -BIN=unosd +BIN=linkd SRC:= SRC+=$(wildcard src/*.c) diff --git a/tests/helpers.sh b/tests/helpers.sh @@ -0,0 +1,64 @@ +#!/bin/sh +# tests/helpers.sh - shared assertions, source with `. tests/helpers.sh` +# +# Assertions only. linkd is a standalone daemon and its tests build and run it +# directly, so none of the QEMU/SSH plumbing from the OS-assembly repo belongs +# here -- anything needing a booted system is an integration test and lives in +# a consuming OS repo. +set -eu + +assert_eq() { + # assert_eq <got> <expected> <msg> + if [ "$1" != "$2" ]; then + echo "FAIL: $3: expected '$2', got '$1'" >&2 + return 1 + fi + echo "PASS: $3" +} + +assert_contains() { + # assert_contains <haystack> <needle> <msg> + case "$1" in + *"$2"*) echo "PASS: $3" ;; + *) echo "FAIL: $3: expected to contain '$2', got '$1'" >&2; return 1 ;; + esac +} + +assert_file_contains() { + # assert_file_contains <file> <needle> <msg> + # `--` matters: without it a needle starting with '-' (any command-line flag) + # is parsed by grep as options and never matches. + if grep -qF -- "$2" "$1" 2>/dev/null; then + echo "PASS: $3" + else + echo "FAIL: $3: $1 does not contain '$2'" >&2 + return 1 + fi +} + +# Build linkd once, on demand. Every test needs the objects, and rebuilding per +# test would dominate the runtime. +ensure_built() { + # ensure_built <repo-root> + _root=$1 + _bd="${_root}/build/${LINKD_TARGET:-linux-glibc-amd64}" + if [ ! -x "${_bd}/linkd" ]; then + echo "building linkd" >&2 + make -C "${_root}" -j"$(nproc)" >/dev/null + fi + [ -x "${_bd}/linkd" ] || { echo "FAIL: linkd did not build" >&2; return 1; } + printf '%s\n' "${_bd}" +} + +# Guard against greps that silently stop matching. A pattern that finds nothing +# because the file moved is indistinguishable from one that finds nothing +# because the code is correct, and the second is the only one we want to pass. +assert_path_exists() { + # assert_path_exists <path> <msg> + if [ -e "$1" ]; then + echo "PASS: $2" + else + echo "FAIL: $2: no such path: $1" >&2 + return 1 + fi +} diff --git a/tests/run.sh b/tests/run.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# tests/run.sh - run the linkd test suite. +# +# ./tests/run.sh everything +# ./tests/run.sh --unit same (only unit tests exist here) +# +# linkd's own tests never boot a machine. Anything that needs a running system +# -- bridges, VLANs, VRFs, addressing against a real kernel -- is an +# integration test and lives in the consuming OS repo, because it needs a +# rootfs and a VM that this repo has no business building. +set -eu +HERE=$(cd "$(dirname "$0")" && pwd) + +fail=0 +run_one() { + echo + echo "=== $1 ===" + if sh "$1"; then + echo "--- PASS $1" + else + echo "--- FAIL $1" >&2 + fail=1 + fi +} + +for t in "${HERE}"/unit/*.sh; do + [ -e "$t" ] || continue + run_one "$t" +done + +echo +if [ "$fail" = "0" ]; then + echo "ALL TESTS PASSED" +else + echo "SOME TESTS FAILED" >&2 +fi +exit "$fail" diff --git a/tests/unit/test_ascii.sh b/tests/unit/test_ascii.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# Sources and docs stay pure ASCII. +# +# Typographic characters (em-dashes, curly quotes) arrive invisibly via copy +# paste and editor autocorrect, then show up as mojibake on a serial console +# with no locale. Checking untracked files too is deliberate: the usual way +# this regresses is a file that has not been committed yet. +set -eu +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "${HERE}/../.." && pwd) + +echo "==> test_ascii: source and docs are pure ASCII" +cd "${ROOT}" + +check() { + # check <label> <file-list-command> + bad=$(eval "$2" | while read -r f; do + [ -f "$f" ] || continue + case "$f" in build/*|*.png|*.gz|*.img|*.bin) continue ;; esac + if LC_ALL=C grep -nP '[^\x00-\x7F]' "$f" 2>/dev/null | head -3 | sed "s|^|${f}:|"; then :; fi + done) + if [ -n "${bad}" ]; then + echo "FAIL: non-ASCII bytes in $1:" >&2 + printf '%s\n' "${bad}" | head -10 >&2 + return 1 + fi + echo "PASS: no non-ASCII in $1" +} + +check "tracked sources" "git ls-files" +check "untracked sources" "git ls-files --others --exclude-standard" + +echo "==> test_ascii done" diff --git a/tests/unit/test_multicall.sh b/tests/unit/test_multicall.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# linkd is a single binary that dispatches on basename(argv[0]). +# The package ships ifup/ifdown/ifquery/ifreload/linkctl as symlinks to it, so +# dispatch breaking is indistinguishable from the commands not existing. +set -eu +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "${HERE}/../.." && pwd) +. "${HERE}/../helpers.sh" + +echo "==> test_multicall: argv[0] dispatch" + +BUILD_DIR=$(ensure_built "${ROOT}") +BIN="${BUILD_DIR}/linkd" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +for l in ifup ifdown ifquery ifreload linkctl; do + ln -s "$BIN" "${TMPDIR}/${l}" +done + +# ifup with no daemon: either usage, or a connect failure naming linkd. Both +# prove dispatch worked; what must not happen is linkd's own daemon output. +out=$("${TMPDIR}/ifup" 2>&1 || true) +case "${out}" in + *"usage: ifup"*) echo "PASS: ifup dispatches (usage)" ;; + *"Is linkd running"*) echo "PASS: ifup dispatches (connect refused, daemon not running)" ;; + *) + echo "FAIL: ifup via symlink did not dispatch" >&2 + printf '%s\n' "${out}" | head -5 >&2 + exit 1 ;; +esac + +# linkctl is a thin shim: it takes argv[1] and must NOT dispatch on argv[0]. +out=$("${TMPDIR}/linkctl" 2>&1 || true) +assert_contains "${out}" "usage:" "linkctl shows usage with no arguments" + +out=$("$BIN" --help 2>&1 || true) +assert_contains "${out}" "linkd" "linkd --help" + +# Every dispatch target must be reachable from the one binary. +for l in ifup ifdown ifquery ifreload linkctl; do + if ! strings "$BIN" | grep -qx "$l"; then + echo "FAIL: multicall binary has no '${l}' dispatch entry" >&2 + exit 1 + fi +done +echo "PASS: all five dispatch targets present in the binary" + +echo "==> test_multicall done" diff --git a/tests/unit/test_netlink.sh b/tests/unit/test_netlink.sh @@ -0,0 +1,78 @@ +#!/bin/sh +# Regression guards for the three defects that made the integration suite flaky. +# +# Every check here greps the source tree, which means a check can stop finding +# anything for two very different reasons: the code is correct, or the file +# moved. Only the first should pass, so each grep is preceded by an explicit +# existence assertion. Without that, this whole file degrades into a silent +# pass the moment the layout changes -- which is exactly what would have +# happened when these tests moved into their own repo. +set -eu +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "${HERE}/../.." && pwd) +. "${HERE}/../helpers.sh" + +echo "==> test_netlink: no ip shell-out, blocking accept, client half-close" + +SRC="${ROOT}/src" +assert_path_exists "${SRC}" "source tree present" + +# --- 1. linkd must never shell out to `ip` -------------------------------- +# All link/addr/route/vlan/bridge/VRF work goes through src/netlink/rtnl.c. A +# system("ip ...") reintroduces both the PATH dependency and the asynchronous +# behaviour that made tests need sleeps. +# +# Strip the grep file:line: prefix before deciding whether a hit is a comment, +# otherwise every match looks like code and the check cries wolf. +code_hits() { + grep -rn "$1" "${SRC}" 2>/dev/null \ + | sed 's/^[^:]*:[0-9]*://' \ + | sed 's/^[[:space:]]*//' \ + | grep -v '^//' \ + | grep -v '^\*' || true +} + +if [ -n "$(code_hits 'system(.*"[^"]*ip ')" ]; then + echo "FAIL: linkd shells out to ip:" >&2 + code_hits 'system(.*"[^"]*ip ' | head -5 >&2 + exit 1 +fi +echo "PASS: no ip shell-out in linkd sources" + +if [ -n "$(code_hits '/sbin/ip')" ]; then + echo "FAIL: /sbin/ip referenced in linkd sources" >&2 + code_hits '/sbin/ip' | head -5 >&2 + exit 1 +fi +echo "PASS: no /sbin/ip reference in linkd sources" + +# Prove the grep above is actually looking at real code. If rtnl.c ever stops +# containing netlink sends, the two checks above are meaningless. +assert_path_exists "${SRC}/netlink/rtnl.c" "rtnl.c present (the netlink path exists)" + +# --- 2. the accepted IPC connection must be blocking ---------------------- +# A non-blocking accepted fd made the server read EAGAIN, stdio report EOF, and +# cfg_parse see an empty stream -> empty reply. That was the root cause of the +# IPC flakiness, not timing. +IPC="${SRC}/ipc.c" +assert_path_exists "${IPC}" "ipc.c present" +if grep -q 'accept4(ipc_sock.*SOCK_NONBLOCK' "${IPC}"; then + echo "FAIL: accepted IPC conn is non-blocking (reintroduces empty-reply race)" >&2 + exit 1 +fi +echo "PASS: accepted IPC connection is blocking" + +# --- 3. clients must half-close after writing ----------------------------- +# Without shutdown(SHUT_WR) the server blocks waiting for a second command +# until its receive timeout expires. +for c in ifup ifdown ifquery ifreload linkctl; do + f="${SRC}/cli/${c}.c" + assert_path_exists "${f}" "cli/${c}.c present" + if ! grep -q 'shutdown(sock, SHUT_WR)' "${f}"; then + echo "FAIL: ${c} does not shutdown(SHUT_WR) after write" >&2 + exit 1 + fi +done +echo "PASS: all clients half-close after sending the command" + +echo "==> test_netlink done" diff --git a/tests/unit/test_parse.sh b/tests/unit/test_parse.sh @@ -0,0 +1,118 @@ +#!/bin/sh +# Parse the real config grammar through the real parser objects. +# +# This links the actual ifaces/ports/config objects out of the build tree +# rather than re-implementing the grammar, so it fails when the parser changes +# behaviour rather than when a test fixture drifts. +set -eu +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "${HERE}/../.." && pwd) +. "${HERE}/../helpers.sh" + +echo "==> test_parse: ports + interfaces, source, vlan, bridge, hooks, IPv6" + +BUILD_DIR=$(ensure_built "${ROOT}") + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +# Config lives under the temp dir, and the harness is told where via argv. +# The original version hardcoded a fixed /tmp path, which two +# concurrent runs would silently share. +CFGDIR="${TMPDIR}/etc/network" +mkdir -p "${CFGDIR}/interfaces.d" "${CFGDIR}/ports.d" + +cat > "${CFGDIR}/interfaces" <<'EOF' +source interfaces.d/*.cnf +auto lo +iface lo + address 127.0.0.1/8 + address ::1/128 + +auto br0 +iface br0 + address 10.20.0.1/24 + bridge-ports eth0 + bridge-stp off + pre-up echo pre-up-br0 + post-up echo post-up-br0 + +auto eth0.100 +iface eth0.100 + address 10.100.0.1/24 + address 2001:db8:100::1/64 + vlan-raw-device eth0 +EOF + +cat > "${CFGDIR}/interfaces.d/dummy.cnf" <<'EOF' +iface dummy1 + address 10.99.0.1/24 + pre-up touch /tmp/pre + post-up touch /tmp/post +EOF + +cat > "${TMPDIR}/test_parse.c" <<'C' +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include "util/config.h" +#include "config/ports.h" +#include "config/ifaces.h" + +/* argv[1] = interfaces file, argv[2] = its directory (for `source`) */ +int main(int argc, char **argv) { + if (argc < 3) { fprintf(stderr, "usage: test_parse <file> <dir>\n"); return 2; } + ports_register(); + ifaces_register(); + FILE *f = fopen(argv[1], "r"); + if (!f) { fprintf(stderr, "cannot open %s\n", argv[1]); return 2; } + int rc = cfg_parse(cfg_ns_get("interfaces"), argv[2], f, NULL); + fclose(f); + if (rc != 0) { fprintf(stderr, "cfg_parse rc=%d\n", rc); return 1; } + + struct linkd_iface *lo = ifaces_find("lo"); + struct linkd_iface *br0 = ifaces_find("br0"); + struct linkd_iface *vlan = ifaces_find("eth0.100"); + struct linkd_iface *dum = ifaces_find("dummy1"); + + if (!lo) { fprintf(stderr, "lo not found\n"); return 1; } + if (!lo->addrs) { fprintf(stderr, "lo addrs missing\n"); return 1; } + + int addrc = 0; + for (struct iface_addr *a = lo->addrs; a; a = a->next) addrc++; + if (addrc < 2) { fprintf(stderr, "lo addrc %d expected 2 (IPv6 first-class)\n", addrc); return 1; } + + if (!br0) { fprintf(stderr, "br0 not found\n"); return 1; } + if (!br0->bridge_ports) { fprintf(stderr, "br0 bridge-ports missing\n"); return 1; } + if (!br0->pre_up || !br0->post_up) { fprintf(stderr, "br0 hooks missing\n"); return 1; } + + if (!vlan || vlan->vlan_id != 100) { fprintf(stderr, "vlan missing or id != 100\n"); return 1; } + + /* proves `source interfaces.d/*.cnf` was followed */ + if (!dum) { fprintf(stderr, "dummy1 not found: source directive did not load interfaces.d\n"); return 1; } + + printf("parse ok: lo addrs %d, br0 ports %s, vlan %d, sourced dummy1\n", + addrc, br0->bridge_ports, vlan->vlan_id); + return 0; +} +C + +cc -I"${ROOT}/src" -I"${BUILD_DIR}/lib/.dep/include" -I"${BUILD_DIR}" \ + "${TMPDIR}/test_parse.c" \ + "${BUILD_DIR}/src/config/ifaces.o" \ + "${BUILD_DIR}/src/config/ports.o" \ + "${BUILD_DIR}/src/util/config.o" \ + "${BUILD_DIR}/lib/finwo/cnfparse/src/cnfparse.o" \ + "${BUILD_DIR}/lib/finwo/buf/src/buf.o" \ + "${BUILD_DIR}/lib/rxi/log/src/log.o" \ + -o "${TMPDIR}/test_parse" 2>&1 | head -20 + +[ -x "${TMPDIR}/test_parse" ] || { echo "FAIL: test_parse compile failed" >&2; exit 1; } + +if "${TMPDIR}/test_parse" "${CFGDIR}/interfaces" "${CFGDIR}"; then + echo "PASS: config parsing, source, vlan, bridge, hooks, IPv6" +else + echo "FAIL: config parsing" >&2; exit 1 +fi + +echo "==> test_parse done"