linkd

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

commit 89f9c70090c7d471b0f03692003711e121d06e44
Author: finwo <finwo@pm.me>
Date:   Sun, 13 Sep 2026 01:32:12 +0200

Project init

Diffstat:
A.editorconfig | 13+++++++++++++
A.gitignore | 3+++
AMakefile | 40++++++++++++++++++++++++++++++++++++++++
Aplugins/bcm/Makefile | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Aplugins/bcm/README.md | 45+++++++++++++++++++++++++++++++++++++++++++++
Aplugins/bcm/src/dataplane.c | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/config/ports.c | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/config/ports.h | 35+++++++++++++++++++++++++++++++++++
Asrc/dataplane.h | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/dataplane/kernel.c | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/dataplane/plugin.c | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/dataplane/registry.c | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/dataplane/registry.h | 10++++++++++
Asrc/main.c | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/config.c | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/config.h | 21+++++++++++++++++++++
Atarget/common/.dep | 2++
Atarget/common/Makefile | 44++++++++++++++++++++++++++++++++++++++++++++
Atarget/linux-glibc-amd64/.gitkeep | 0
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