commit 89f9c70090c7d471b0f03692003711e121d06e44
Author: finwo <finwo@pm.me>
Date: Sun, 13 Sep 2026 01:32:12 +0200
Project init
Diffstat:
19 files changed, 1140 insertions(+), 0 deletions(-)
diff --git a/.editorconfig b/.editorconfig
@@ -0,0 +1,13 @@
+# 2d0e4a3f-ac19-430c-bf1b-46c68651ce21
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_size = 2
+indent_style = space
+trim_trailing_whitespace = true
+
+[Makefile*]
+indent_style = tab
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+/build/
+/lib/
+*.o
diff --git a/Makefile b/Makefile
@@ -0,0 +1,40 @@
+TARGET:=linux-glibc-amd64
+
+BIN:=unosd
+
+.PHONY: default
+default: build/${TARGET}/${BIN}
+
+FIND:=$(shell command -v gfind find)
+
+WATCH?=
+WATCH+=$(shell $(FIND) src -type f -name '*.c')
+WATCH+=$(shell $(FIND) src -type f -name '*.h')
+
+build/${TARGET}/${BIN}: build/${TARGET} $(WATCH)
+ cd build/${TARGET} && dep install
+ $(MAKE) --directory build/${TARGET} TARGET=${TARGET}
+
+build/${TARGET}: $(WATCH)
+ mkdir -p build/${TARGET}
+ cp -rT src/ build/${TARGET}/src
+ cp -rT target/common/ build/${TARGET}
+ cp -rT target/${TARGET}/ build/${TARGET}
+
+.PHONY: targets
+targets:
+ @ls -1 target | grep -v '^common$$'
+
+.PHONY: clean
+clean:
+ rm -rf build
+
+# Never let the catch-all try to rebuild the makefiles themselves
+Makefile: ;
+
+# Forward any other goal verbatim into the assembled target tree
+.PHONY: FORCE
+FORCE:
+%: build/${TARGET} FORCE
+ cd build/${TARGET} && dep install
+ $(MAKE) --directory build/${TARGET} TARGET=${TARGET} $@
diff --git a/plugins/bcm/Makefile b/plugins/bcm/Makefile
@@ -0,0 +1,49 @@
+BIN=bcm.so
+
+# Path to a built OpenBCM SDK tree. Supplied by the openbcm package build;
+# there is no sensible default, so the build fails loudly without it.
+SDK?=
+
+CFLAGS?=-Wall -Wextra -O2
+CFLAGS+=-fPIC
+
+# unosd's dataplane contract
+INCLUDES:=
+INCLUDES+=-I ../../src
+
+LDFLAGS?=
+LDFLAGS+=-shared
+
+ifneq ($(SDK),)
+INCLUDES+=-I $(SDK)/include
+LDFLAGS +=-L $(SDK)/build -lbcm
+endif
+
+CFLAGS+=$(INCLUDES)
+
+SRC:=$(wildcard src/*.c)
+OBJ:=$(SRC:.c=.o)
+
+.PHONY: default
+default: check $(BIN)
+
+.PHONY: check
+check:
+ @test -n "$(SDK)" || { \
+ echo "SDK= is required: point it at a built OpenBCM SDK tree"; \
+ exit 1; \
+ }
+
+%.o: %.c
+ $(CC) $(CFLAGS) $< -c -o $@
+
+$(BIN): $(OBJ)
+ $(CC) $(CFLAGS) $(OBJ) $(LDFLAGS) -o $@
+
+.PHONY: clean
+clean:
+ rm -f $(OBJ) $(BIN)
+
+.PHONY: install
+install: $(BIN)
+ install -Dm0755 $(BIN) $(DESTDIR)/usr/lib/unos/dataplane/$(BIN)
diff --git a/plugins/bcm/README.md b/plugins/bcm/README.md
@@ -0,0 +1,45 @@
+# plugins/bcm
+
+Broadcom XGS dataplane backend for `unosd`.
+
+This is **not** built by the `unosd` 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.
+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
+without an ASIC the directory is simply empty and the built-in kernel
+dataplane is used instead.
+
+## Contract
+
+A plugin is a shared object exporting exactly one symbol:
+
+```c
+struct dp_ops unos_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.
+
+`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`,
+which the openbcm kernel modules create once they have enumerated a switch ASIC
+over PCIe.
+
+## Building
+
+Requires a built OpenBCM SDK tree:
+
+```sh
+make SDK=/path/to/openbcm/sdk-6.5.27
+make SDK=/path/to/openbcm/sdk-6.5.27 install DESTDIR=/
+```
+
+## Status
+
+Skeleton. Every entry point past `probe()` returns `DP_RET_ERROR` with a TODO
+naming the SDK call that belongs there. Real implementation is gated on having
+the board configuration for the target switch, without which the SDK cannot map
+logical ports to SerDes lanes.
diff --git a/plugins/bcm/src/dataplane.c b/plugins/bcm/src/dataplane.c
@@ -0,0 +1,123 @@
+// Broadcom XGS dataplane backend for unosd.
+//
+// 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
+// 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.
+//
+// Building this requires the OpenBCM SDK headers and libbcm. See the README
+// next to this file.
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include "dataplane.h"
+
+// The BDE character device is created by the openbcm kernel modules once they
+// have enumerated a switch ASIC over PCIe. Its presence is the cheapest
+// reliable signal that this backend can drive the box.
+#define BCM_BDE_DEV "/dev/linux-kernel-bde"
+
+static int bcm_probe(void) {
+ if (access(BCM_BDE_DEV, F_OK) != 0) return DP_RET_ERROR;
+ return DP_RET_OK;
+}
+
+static int bcm_init(void) {
+ // TODO: SDK bring-up.
+ // - read the board config (config.bcm) for the port/SerDes map
+ // - soc_cm_device_create / attach unit 0
+ // - run the board init sequence
+ // - create KNET netdevs (swpN) for each front-panel port
+ fprintf(stderr, "dataplane/bcm: SDK bring-up not implemented\n");
+ return DP_RET_ERROR;
+}
+
+static void bcm_fini(void) {
+ // TODO: detach unit, tear down KNET netdevs
+}
+
+static int bcm_port_apply(const struct dp_port *port) {
+ // TODO: bcm_port_speed_set / bcm_port_autoneg_set, and
+ // bcm_port_phy_control_set for FEC
+ (void)port;
+ return DP_RET_ERROR;
+}
+
+static int bcm_port_admin(const char *ifname, bool up) {
+ // TODO: bcm_port_enable_set
+ (void)ifname;
+ (void)up;
+ return DP_RET_ERROR;
+}
+
+static int bcm_port_mtu(const char *ifname, uint32_t mtu) {
+ // TODO: bcm_port_frame_max_set. Driven by the netlink mirror so the ASIC
+ // frame limit tracks whatever ifupdown put on the netdev.
+ (void)ifname;
+ (void)mtu;
+ return DP_RET_ERROR;
+}
+
+static int bcm_rif_add(const struct dp_rif *rif) {
+ // TODO: bcm_l3_intf_create
+ (void)rif;
+ return DP_RET_ERROR;
+}
+
+static int bcm_rif_del(const struct dp_rif *rif) {
+ // TODO: bcm_l3_intf_delete
+ (void)rif;
+ return DP_RET_ERROR;
+}
+
+static int bcm_neigh_add(const struct dp_neigh *neigh) {
+ // TODO: bcm_l3_host_add
+ (void)neigh;
+ return DP_RET_ERROR;
+}
+
+static int bcm_neigh_del(const struct dp_neigh *neigh) {
+ // TODO: bcm_l3_host_delete
+ (void)neigh;
+ return DP_RET_ERROR;
+}
+
+static int bcm_route_add(const struct dp_route *route) {
+ // TODO: bcm_l3_route_add; for nh_count > 1 build a bcm_l3_egress_ecmp
+ // group and refcount it across routes sharing the same nexthop set
+ (void)route;
+ return DP_RET_ERROR;
+}
+
+static int bcm_route_del(const struct dp_route *route) {
+ // TODO: bcm_l3_route_delete, release ECMP group when refcount hits zero
+ (void)route;
+ return DP_RET_ERROR;
+}
+
+static int bcm_resync(void) {
+ // TODO: walk the ASIC tables, diff against the kernel, converge.
+ // Netlink drops messages under load, so this is the correctness backstop,
+ // not an optimisation.
+ return DP_RET_ERROR;
+}
+
+struct dp_ops unos_dataplane_ops = {
+ .abi = UNOS_DATAPLANE_ABI,
+ .name = "bcm",
+ .probe = bcm_probe,
+ .init = bcm_init,
+ .fini = bcm_fini,
+ .port_apply = bcm_port_apply,
+ .port_admin = bcm_port_admin,
+ .port_mtu = bcm_port_mtu,
+ .rif_add = bcm_rif_add,
+ .rif_del = bcm_rif_del,
+ .neigh_add = bcm_neigh_add,
+ .neigh_del = bcm_neigh_del,
+ .route_add = bcm_route_add,
+ .route_del = bcm_route_del,
+ .resync = bcm_resync,
+};
diff --git a/src/config/ports.c b/src/config/ports.c
@@ -0,0 +1,170 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+#include "finwo/cnfparse.h"
+
+#include "util/config.h"
+
+#include "ports.h"
+
+static struct unos_port *ports = NULL;
+static struct unos_port *ports_tail = NULL;
+
+struct unos_port * ports_list(void) {
+ return ports;
+}
+
+struct unos_port * ports_find(const char *name) {
+ struct unos_port *cur;
+ for ( cur = ports ; cur ; cur = cur->next ) {
+ if (!strcmp(cur->name, name)) return cur;
+ }
+ return NULL;
+}
+
+void ports_free(void) {
+ struct unos_port *cur = ports;
+ struct unos_port *next;
+
+ while (cur) {
+ next = cur->next;
+ if (cur->name) free(cur->name);
+ if (cur->breakout) free(cur->breakout);
+ free(cur);
+ cur = next;
+ }
+
+ ports = NULL;
+ ports_tail = NULL;
+}
+
+static enum dp_fec ports_parse_fec(const char *value) {
+ if (!strcasecmp("off" , value)) return DP_FEC_OFF;
+ if (!strcasecmp("none" , value)) return DP_FEC_OFF;
+ if (!strcasecmp("auto" , value)) return DP_FEC_AUTO;
+ if (!strcasecmp("rs" , value)) return DP_FEC_RS;
+ if (!strcasecmp("baser", value)) return DP_FEC_BASER;
+ if (!strcasecmp("fc" , value)) return DP_FEC_BASER;
+ return DP_FEC_UNSET;
+}
+
+static int ports_parse_bool(const char *value) {
+ if (!strcasecmp("on" , value)) return 1;
+ if (!strcasecmp("yes" , value)) return 1;
+ if (!strcasecmp("true", value)) return 1;
+ if (!strcasecmp("1" , value)) return 1;
+ return 0;
+}
+
+// Directives ifupdown already implements. Accepting them here would create a
+// second source of truth for the same value, so reject them loudly and point
+// at the right file rather than silently ignoring them.
+static const char *ports_reserved[] = {
+ "address", "gateway", "netmask", "broadcast", "metric",
+ "mtu", "hwaddress", "admin", "up", "down",
+ NULL,
+};
+
+static int ports_is_reserved(const char *name) {
+ int i;
+ for ( i = 0 ; ports_reserved[i] ; i++ ) {
+ if (!strcasecmp(ports_reserved[i], name)) return 1;
+ }
+ return 0;
+}
+
+struct cnf_directive * cfg_parse_port(FILE *fd, struct cnf_directive *dir, void *user) {
+ struct unos_port *port;
+ (void)user;
+
+ if (dir->argc != 1) {
+ fprintf(stderr, "`port` directive accepts only 1 argument\n");
+ exit(1);
+ }
+
+ if (ports_find(dir->argv[0])) {
+ fprintf(stderr, "duplicate `port` definition: %s\n", dir->argv[0]);
+ exit(1);
+ }
+
+ port = calloc(1, sizeof(struct unos_port));
+ port->name = strdup(dir->argv[0]);
+ port->fec = DP_FEC_UNSET;
+ port->autoneg = -1;
+
+ for(;;) {
+ if (dir) cnf_directive_free(dir);
+ dir = cnf_directive_read(fd);
+ if (!dir) break; // EOF = done
+
+ // Make following blocks identical in structure
+ if(0) {}
+
+ else if (!strcasecmp("speed", dir->name)) {
+ if (dir->argc != 1) {
+ fprintf(stderr, "`speed` directive accepts only 1 argument\n");
+ exit(1);
+ }
+ 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);
+ }
+ 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);
+ }
+ }
+
+ else if (!strcasecmp("autoneg", dir->name)) {
+ if (dir->argc != 1) {
+ fprintf(stderr, "`autoneg` directive accepts only 1 argument\n");
+ exit(1);
+ }
+ 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);
+ }
+ 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);
+ }
+
+ else {
+ // unknown directive
+ break;
+ }
+ }
+
+ // Store the completed port
+ if (ports_tail) {
+ ports_tail->next = port;
+ } else {
+ ports = port;
+ }
+ ports_tail = port;
+
+ // Return the unparsed dir if set
+ return dir;
+}
+
+void ports_register(void) {
+ cfg_register_directive("port", cfg_parse_port);
+}
diff --git a/src/config/ports.h b/src/config/ports.h
@@ -0,0 +1,35 @@
+#ifndef __UNOS_CONFIG_PORTS_H__
+#define __UNOS_CONFIG_PORTS_H__
+
+#include <stdint.h>
+#include <stdio.h>
+
+#include "finwo/cnfparse.h"
+
+#include "dataplane.h"
+
+// Switch port physical-layer configuration.
+//
+// Scope is deliberately narrow. Addresses, MTU, MAC and admin state are all
+// 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 {
+ 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;
+};
+
+// Registers the `port` directive with the config parser
+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);
+void ports_free(void);
+
+#endif // __UNOS_CONFIG_PORTS_H__
diff --git a/src/dataplane.h b/src/dataplane.h
@@ -0,0 +1,124 @@
+#ifndef __UNOS_DATAPLANE_H__
+#define __UNOS_DATAPLANE_H__
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#define DP_RET_ERROR -1
+#define DP_RET_OK 0
+
+// Bumped whenever struct dp_ops changes shape. Plugins declaring a different
+// ABI are refused at load time rather than crashing us later.
+#define UNOS_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"
+
+// Symbol every plugin must export
+#define UNOS_DATAPLANE_SYM "unos_dataplane_ops"
+
+enum dp_fec {
+ DP_FEC_UNSET = 0,
+ DP_FEC_OFF,
+ DP_FEC_AUTO,
+ DP_FEC_RS,
+ DP_FEC_BASER,
+};
+
+struct dp_addr {
+ int family; // AF_INET | AF_INET6
+ uint8_t addr[16];
+ uint8_t prefixlen;
+};
+
+// Physical-layer properties of a switch port, from /etc/unos/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
+// netlink instead. Only properties with no working kernel path on a KNET stub
+// netdev live here.
+struct dp_port {
+ const char *name; // swp1, ...
+ uint32_t speed; // Mbit/s, 0 = unset
+ enum dp_fec fec;
+ int autoneg; // -1 unset, 0 off, 1 on
+};
+
+struct dp_rif {
+ const char *ifname;
+ struct dp_addr addr;
+};
+
+struct dp_neigh {
+ const char *ifname;
+ struct dp_addr addr;
+ uint8_t mac[6];
+};
+
+struct dp_nexthop {
+ const char *ifname;
+ struct dp_addr gw;
+};
+
+struct dp_route {
+ struct dp_addr dst;
+ struct dp_nexthop *nh;
+ size_t nh_count; // >1 = ECMP group
+ uint32_t metric;
+};
+
+struct dp_ops {
+ uint32_t abi;
+ const char *name;
+
+ // Return DP_RET_OK if this backend can drive the hardware in this box.
+ // Must be cheap and must not have side effects; it is called on every
+ // backend before one is chosen.
+ int (*probe)(void);
+
+ int (*init)(void);
+ void (*fini)(void);
+
+ // Applies ports.cnf properties.
+ int (*port_apply)(const struct dp_port *port);
+
+ // Driven by the netlink mirror, not by configuration: ifupdown sets these
+ // on the netdev and we replicate them into the hardware.
+ int (*port_admin)(const char *ifname, bool up);
+ int (*port_mtu)(const char *ifname, uint32_t mtu);
+
+ int (*rif_add)(const struct dp_rif *rif);
+ int (*rif_del)(const struct dp_rif *rif);
+
+ int (*neigh_add)(const struct dp_neigh *neigh);
+ int (*neigh_del)(const struct dp_neigh *neigh);
+
+ int (*route_add)(const struct dp_route *route);
+ int (*route_del)(const struct dp_route *route);
+
+ // Drop all state and rebuild it from the kernel. Netlink can and does drop
+ // messages under load (ENOBUFS), so event-following alone drifts; every
+ // backend needs to support a full resync.
+ int (*resync)(void);
+};
+
+// 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
+// absent or empty.
+int dp_plugins_load(const char *dir);
+
+// Pick a backend. `force` may name one explicitly (skips probing), or be NULL
+// to probe every registered backend and take the first that claims the box.
+int dp_select(const char *force);
+
+const struct dp_ops * dp_active(void);
+
+int dp_init(void);
+void dp_fini(void);
+
+#endif // __UNOS_DATAPLANE_H__
diff --git a/src/dataplane/kernel.c b/src/dataplane/kernel.c
@@ -0,0 +1,70 @@
+// Built-in dataplane backend.
+//
+// This is the "there is no switching ASIC" case: generic x86_64, a VM, or a
+// soft-router. The Linux kernel is already the forwarding engine, so every
+// entry point here is a genuine no-op rather than an unimplemented stub:
+//
+// - addresses, MTU, MAC and admin state are applied by ifupdown directly to
+// the netdev, so there is nothing to replicate
+// - routes and neighbours are already in the FIB by the time we observe the
+// netlink message announcing them
+// - 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
+// hardware is therefore close to inert by design: it parses configuration,
+// validates it, and lets Linux do the work.
+
+#include <stdio.h>
+
+#include "dataplane.h"
+
+static int kernel_probe(void) {
+ // Always usable -- this is the fallback of last resort.
+ return DP_RET_OK;
+}
+
+static int kernel_init(void) {
+ fprintf(stderr, "dataplane/kernel: forwarding handled by the kernel\n");
+ return DP_RET_OK;
+}
+
+static void kernel_fini(void) {
+}
+
+static int kernel_port_apply(const struct dp_port *port) { (void)port; return DP_RET_OK; }
+static int kernel_port_admin(const char *ifname, bool up) { (void)ifname; (void)up; return DP_RET_OK; }
+static int kernel_port_mtu(const char *ifname, uint32_t mtu) { (void)ifname; (void)mtu; return DP_RET_OK; }
+
+static int kernel_rif_add(const struct dp_rif *rif) { (void)rif; return DP_RET_OK; }
+static int kernel_rif_del(const struct dp_rif *rif) { (void)rif; return DP_RET_OK; }
+static int kernel_neigh_add(const struct dp_neigh *neigh) { (void)neigh; return DP_RET_OK; }
+static int kernel_neigh_del(const struct dp_neigh *neigh) { (void)neigh; return DP_RET_OK; }
+static int kernel_route_add(const struct dp_route *route) { (void)route; return DP_RET_OK; }
+static int kernel_route_del(const struct dp_route *route) { (void)route; return DP_RET_OK; }
+
+static int kernel_resync(void) {
+ return DP_RET_OK;
+}
+
+static const struct dp_ops kernel_ops = {
+ .abi = UNOS_DATAPLANE_ABI,
+ .name = "kernel",
+ .probe = kernel_probe,
+ .init = kernel_init,
+ .fini = kernel_fini,
+ .port_apply = kernel_port_apply,
+ .port_admin = kernel_port_admin,
+ .port_mtu = kernel_port_mtu,
+ .rif_add = kernel_rif_add,
+ .rif_del = kernel_rif_del,
+ .neigh_add = kernel_neigh_add,
+ .neigh_del = kernel_neigh_del,
+ .route_add = kernel_route_add,
+ .route_del = kernel_route_del,
+ .resync = kernel_resync,
+};
+
+const struct dp_ops * dp_kernel_ops(void) {
+ return &kernel_ops;
+}
diff --git a/src/dataplane/plugin.c b/src/dataplane/plugin.c
@@ -0,0 +1,67 @@
+// Dataplane plugin loader.
+//
+// A UNOS 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
+// 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`.
+
+#include <dirent.h>
+#include <dlfcn.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "dataplane.h"
+
+#include "registry.h"
+
+int dp_plugins_load(const char *dir) {
+ struct dirent *ent;
+ DIR *dh;
+ char path[1024];
+ void *handle;
+ struct dp_ops *ops;
+ size_t len;
+ int loaded = 0;
+
+ if (!dir) dir = UNOS_DATAPLANE_DIR;
+
+ dh = opendir(dir);
+ if (!dh) {
+ // Not an error: no plugin directory simply means no hardware backends.
+ return 0;
+ }
+
+ while ((ent = readdir(dh))) {
+ len = strlen(ent->d_name);
+ if (len < 4) continue;
+ if (strcmp(ent->d_name + len - 3, ".so")) continue;
+
+ snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name);
+
+ handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
+ if (!handle) {
+ fprintf(stderr, "dataplane: %s: %s\n", path, dlerror());
+ continue;
+ }
+
+ ops = dlsym(handle, UNOS_DATAPLANE_SYM);
+ if (!ops) {
+ fprintf(stderr, "dataplane: %s: missing symbol `%s`\n", path, UNOS_DATAPLANE_SYM);
+ dlclose(handle);
+ continue;
+ }
+
+ if (dp_register(ops) != DP_RET_OK) {
+ dlclose(handle);
+ continue;
+ }
+
+ // Deliberately leaked: the ops table stays live for the process lifetime.
+ loaded++;
+ }
+
+ closedir(dh);
+ return loaded;
+}
diff --git a/src/dataplane/registry.c b/src/dataplane/registry.c
@@ -0,0 +1,79 @@
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+
+#include "dataplane.h"
+
+#include "registry.h"
+
+#define DP_MAX_BACKENDS 16
+
+static const struct dp_ops *backend[DP_MAX_BACKENDS];
+static int backend_count = 0;
+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) {
+ fprintf(stderr, "dataplane: refusing backend `%s`: abi %u, expected %u\n",
+ ops->name ? ops->name : "(unnamed)", ops->abi, UNOS_DATAPLANE_ABI);
+ return DP_RET_ERROR;
+ }
+
+ if (!ops->name || !ops->probe || !ops->init) {
+ fprintf(stderr, "dataplane: refusing incomplete backend\n");
+ return DP_RET_ERROR;
+ }
+
+ if (backend_count >= DP_MAX_BACKENDS) {
+ fprintf(stderr, "dataplane: backend table full, dropping `%s`\n", ops->name);
+ return DP_RET_ERROR;
+ }
+
+ backend[backend_count++] = ops;
+ return DP_RET_OK;
+}
+
+int dp_select(const char *force) {
+ int i;
+
+ if (force) {
+ for (i = 0; i < backend_count; i++) {
+ if (!strcasecmp(backend[i]->name, force)) {
+ active = backend[i];
+ fprintf(stderr, "dataplane: using `%s` (forced)\n", active->name);
+ return DP_RET_OK;
+ }
+ }
+ fprintf(stderr, "dataplane: no backend named `%s`\n", force);
+ return DP_RET_ERROR;
+ }
+
+ // Plugins are probed before the built-in kernel backend, which is
+ // registered first and always succeeds -- so iterate in reverse.
+ for (i = backend_count - 1; i >= 0; i--) {
+ if (backend[i]->probe() != DP_RET_OK) continue;
+ active = backend[i];
+ fprintf(stderr, "dataplane: using `%s`\n", active->name);
+ return DP_RET_OK;
+ }
+
+ fprintf(stderr, "dataplane: no usable backend\n");
+ return DP_RET_ERROR;
+}
+
+const struct dp_ops * dp_active(void) {
+ return active;
+}
+
+int dp_init(void) {
+ if (!active) return DP_RET_ERROR;
+ return active->init();
+}
+
+void dp_fini(void) {
+ if (!active) return;
+ if (active->fini) active->fini();
+ active = NULL;
+}
diff --git a/src/dataplane/registry.h b/src/dataplane/registry.h
@@ -0,0 +1,10 @@
+#ifndef __UNOS_DATAPLANE_REGISTRY_H__
+#define __UNOS_DATAPLANE_REGISTRY_H__
+
+#include "dataplane.h"
+
+// Add a backend to the selection table. Rejects backends whose ABI does not
+// match or which omit mandatory entry points.
+int dp_register(const struct dp_ops *ops);
+
+#endif // __UNOS_DATAPLANE_REGISTRY_H__
diff --git a/src/main.c b/src/main.c
@@ -0,0 +1,142 @@
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "cofyc/argparse.h"
+
+#include "config/ports.h"
+#include "dataplane.h"
+#include "dataplane/registry.h"
+#include "util/config.h"
+
+static const char *const usage[] = {
+ __NAME " [options]",
+ __NAME " --help",
+ NULL,
+};
+
+// Port configuration lives in either:
+// <dir>/ports.cnf a single file, or
+// <dir>/ports.d/*.cnf a drop-in directory
+//
+// Rather than growing the config API with a glob entry point, the drop-in case
+// is handed to the parser as an in-memory config consisting of nothing but an
+// `include` -- the parser already knows how to expand those.
+static int load_ports(const char *dir) {
+ char path[1024];
+ char stub[1024];
+ FILE *fd;
+ int rc;
+
+ snprintf(path, sizeof(path), "%s/ports.cnf", 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), "include 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(dir, fd, NULL);
+ if (rc < 0) {
+ fprintf(stderr, "Error during reading port configuration from %s\n", 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) {
+ fprintf(stderr, "%s: failed to apply port configuration\n", cur->name);
+ }
+ }
+
+ return 0;
+}
+
+int main(int argc, const char **argv) {
+ char *config_dir = "/etc/unos";
+ char *dataplane = NULL;
+ 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_END(),
+ };
+
+ // Initialize components
+ ports_register();
+
+ struct argparse argparse;
+ argparse_init(&argparse, options, usage, 0);
+ argparse_describe(&argparse, NULL,
+ "\n"
+ __NAME " programs the forwarding plane from the kernel's routing state.\n"
+ "Built for target " __TARGET ".\n"
+ );
+ argc = argparse_parse(&argparse, argc, argv);
+
+ if (load_ports(config_dir) < 0) {
+ return 1;
+ }
+
+ // 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) {
+ fprintf(stderr, "dataplane: initialisation failed\n");
+ return 1;
+ }
+
+ apply_ports();
+
+ // TODO: netlink event loop -- subscribe to RTNLGRP_LINK / NEIGH / IPV4_ROUTE
+ // / IPV6_ROUTE, mirror into the active dataplane, and periodically
+ // call dp_active()->resync() to recover from dropped messages.
+
+ dp_fini();
+ ports_free();
+
+ return rc;
+}
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
diff --git a/src/util/config.c b/src/util/config.c
@@ -0,0 +1,103 @@
+#include <glob.h>
+#include <libgen.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+#include "finwo/cnfparse.h"
+
+#include "config.h"
+
+#define CFG_MAX_REG 256
+
+static int handler_count = 0;
+static const char *handler_name[CFG_MAX_REG];
+static cfg_directive_fn handler_fn[CFG_MAX_REG];
+
+void cfg_register_directive(const char *name, cfg_directive_fn fn) {
+ if (handler_count >= CFG_MAX_REG) {
+ fprintf(stderr, "config: handler table full, dropping `%s`\n", name);
+ return;
+ }
+ handler_name[handler_count] = name;
+ handler_fn [handler_count] = fn;
+ handler_count++;
+}
+
+int cfg_parse(const char *wd, FILE *fd, void *user) {
+ struct cnf_directive *dir = NULL;
+ glob_t globbuf;
+ int globflags;
+ int i;
+
+ char *strtmp = NULL;
+ FILE *nfd;
+
+ for(;;) {
+ if (dir) cnf_directive_free(dir);
+ dir = cnf_directive_read(fd);
+ if (!dir) break; // EOF = done
+
+cfg_parse_reparse:
+
+ // Core handlers
+ if (!strcasecmp("include", dir->name)) {
+ globflags = GLOB_ERR;
+ for ( i = 0 ; i < (int)dir->argc ; i++ ) {
+ if (dir->argv[i][0] == '/') {
+ strtmp = calloc(strlen(dir->argv[i])+1, 1);
+ strcpy(strtmp, dir->argv[i]);
+ } else {
+ strtmp = malloc(snprintf(NULL, 0, "%s/%s", wd, dir->argv[i])+1);
+ sprintf(strtmp, "%s/%s", wd, dir->argv[i]);
+ }
+ glob(strtmp, globflags, NULL, &globbuf);
+ free(strtmp);
+ globflags = GLOB_ERR | GLOB_APPEND;
+ }
+ for( i = 0 ; i < (int)globbuf.gl_pathc ; i++ ) {
+ strtmp = calloc(strlen(globbuf.gl_pathv[i])+1, 1);
+ strcpy(strtmp, globbuf.gl_pathv[i]);
+ dirname(strtmp);
+ nfd = fopen(globbuf.gl_pathv[i], "r");
+ if (!nfd) {
+ perror("Could not open config file for reading");
+ free(strtmp);
+ globfree(&globbuf);
+ return CFG_RET_ERROR;
+ }
+ if (cfg_parse(strtmp, nfd, user) < 0) {
+ fprintf(stderr, "Error during reading configuration from %s\n", globbuf.gl_pathv[i]);
+ fclose(nfd);
+ free(strtmp);
+ globfree(&globbuf);
+ return CFG_RET_ERROR;
+ }
+ fclose(nfd);
+ free(strtmp);
+ }
+ globfree(&globbuf);
+ continue;
+ }
+
+ // Dynamic handlers
+ for(i = 0 ; i < handler_count ; i++) {
+ if (!strcasecmp(handler_name[i], dir->name)) {
+ dir = handler_fn[i](fd, dir, user);
+ if (dir) goto cfg_parse_reparse;
+ break;
+ }
+ }
+
+ if (i == handler_count) {
+ // Here = not found
+ fprintf(stderr, "Unknown directive: %s\n", dir->name);
+ return CFG_RET_ERROR;
+ }
+ }
+
+ // Cleanup here
+ if (dir) cnf_directive_free(dir);
+
+ return CFG_RET_OK;
+}
diff --git a/src/util/config.h b/src/util/config.h
@@ -0,0 +1,21 @@
+#ifndef __UNOS_UTIL_CONFIG_H__
+#define __UNOS_UTIL_CONFIG_H__
+
+#include <stdio.h>
+
+#include "finwo/cnfparse.h"
+
+#define CFG_RET_ERROR -1
+#define CFG_RET_OK 0
+
+// A directive handler consumes its own sub-directives straight off `fd`, then
+// returns the first directive it did not recognise so the caller can
+// 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);
+
+void cfg_register_directive(const char *name, cfg_directive_fn fn);
+
+// `wd` is the directory relative `include` patterns resolve against.
+int cfg_parse(const char *wd, FILE *fd, void *user);
+
+#endif // __UNOS_UTIL_CONFIG_H__
diff --git a/target/common/.dep b/target/common/.dep
@@ -0,0 +1,2 @@
+cofyc/argparse https://git.finwo.net/misc/dep-repository/archives/heads/pkg/cofyc/argparse.tar.gz
+finwo/cnfparse https://git.finwo.net/lib/cnfparse.c/archives/heads/main.tar.gz
diff --git a/target/common/Makefile b/target/common/Makefile
@@ -0,0 +1,44 @@
+TARGET?=
+
+BIN=unosd
+
+SRC:=
+SRC+=$(wildcard src/*.c)
+SRC+=$(wildcard src/*/*.c)
+SRC+=$(wildcard src/*/*/*.c)
+
+CFLAGS?=-Wall -Wextra -O2
+LDFLAGS?=
+
+# Dataplane plugins are dlopen'd at runtime
+LDFLAGS+=-ldl
+
+INCLUDES:=
+INCLUDES+=-I src
+
+include lib/.dep/config.mk
+
+# Target-specific overrides, optional.
+# Supplied by target/<target>/Makefile.target
+-include Makefile.target
+
+CFLAGS+=$(INCLUDES)
+
+OBJ:=$(SRC:.c=.o)
+
+.PHONY: default
+default: $(BIN)
+
+%.o: %.c $(LIBS)
+ $(CC) $(CFLAGS) $(@:.o=.c) -D__NAME=\"$(BIN)\" -D__TARGET=\"$(TARGET)\" -c -o $@
+
+$(BIN): $(OBJ)
+ $(CC) $(CFLAGS) $(OBJ) $(LDFLAGS) -o $@
+
+.PHONY: clean
+clean:
+ rm -f $(OBJ) $(BIN)
+
+.PHONY: install
+install: $(BIN)
+ install -Dm0755 $(BIN) $(DESTDIR)/usr/bin/$(BIN)
diff --git a/target/linux-glibc-amd64/.gitkeep b/target/linux-glibc-amd64/.gitkeep