commit d56f938937aa14a04d88f755400c2bbccecd8aa8
parent dacf038ab263016b646983a09539da397b2779ec
Author: finwo <finwo@pm.me>
Date: Thu, 24 Sep 2026 09:47:13 +0200
Rename to linkd
Diffstat:
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"