linkd

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

commit 487ddce0c04e1e9401bfedd97821e8a70c516756
parent d56f938937aa14a04d88f755400c2bbccecd8aa8
Author: finwo <finwo@pm.me>
Date:   Thu, 24 Sep 2026 20:18:15 +0200

Convert to stdio plugins instead of C ABI

Diffstat:
MREADME.md | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Aplugins/bcm/.dep | 1+
Aplugins/bcm/.gitignore | 3+++
Mplugins/bcm/Makefile | 13++++++-------
Dplugins/bcm/src/dataplane.c | 125-------------------------------------------------------------------------------
Aplugins/bcm/src/main.c | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/cli/client.c | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/cli/client.h | 8++++++++
Msrc/cli/ifdown.c | 41++++-------------------------------------
Msrc/cli/ifquery.c | 45++++++++-------------------------------------
Msrc/cli/ifreload.c | 45++++-----------------------------------------
Msrc/cli/ifup.c | 46++++------------------------------------------
Msrc/cli/linkctl.c | 136++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/cli/linkd.c | 44++++++++++++++------------------------------
Msrc/config/daemon.c | 17++++++++++++++++-
Msrc/dataplane.h | 89+++++++++++++++++++++++++++++--------------------------------------------------
Dsrc/dataplane/kernel.c | 73-------------------------------------------------------------------------
Asrc/dataplane/ops.c | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/dataplane/plugin.c | 477+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Asrc/dataplane/plugin.h | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/dataplane/registry.c | 79-------------------------------------------------------------------------------
Dsrc/dataplane/registry.h | 10----------
Msrc/ipc.c | 908+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/ipc.h | 25++++++++++++++++++++-----
Msrc/main.c | 42+++++++++++++-----------------------------
Msrc/netlink/netlink.c | 67+++++++++++++++++++++----------------------------------------------
Asrc/util/auth.c | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/auth.h | 35+++++++++++++++++++++++++++++++++++
Mtarget/common/.dep | 2++
Mtarget/common/Makefile | 6++++--
Atests/fixtures/plugin-echo.sh | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/unit/test_auth.sh | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/unit/test_netlink.sh | 54+++++++++++++++++++++++++++++++++++-------------------
Atests/unit/test_plugin.sh | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/unit/test_resp.sh | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
35 files changed, 2631 insertions(+), 1102 deletions(-)

diff --git a/README.md b/README.md @@ -27,7 +27,8 @@ 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. +There are no link-time dependencies: plugins are separate processes, so +nothing is dlopen'd. **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 @@ -97,16 +98,109 @@ because a file moved is indistinguishable from one that matches nothing because the code is correct, and only the second should pass. +Configuration +------------- + +`/etc/linkd.cnf`, same grammar as the rest: + + config_ports /etc/network/ports /etc/network/ports.d/*.cnf + config_iface /etc/network/interfaces /etc/network/interfaces.d/*.cnf + listen unix:///var/run/linkd.sock + listen tcp://127.0.0.1:6789 + authfile /etc/linkd.passwd + plugin /usr/lib/linkd/bcm + +A missing file is not an error; the defaults above are what you get. Patterns +are expanded by `source`, so one matching nothing is fine -- a system with no +drop-ins is normal. + + +Clients and authorization +------------------------- + +The daemon speaks RESP on every address it is told to `listen` on, so +`redis-cli` works as a client. `linkctl` sends whatever you give it: + + linkctl PING + linkctl COMMAND + linkctl IFQUERY lo + +`ifup`, `ifdown`, `ifquery` and `ifreload` are symlinks that send the +corresponding command. + +A connection has one of three roles, and every command names the minimum it +needs: + +| Role | Commands | +| ---------- | --------------------------------------------- | +| *none* | `PING`, `AUTH`, `QUIT` | +| `readonly` | + `IFQUERY`, `INFO`, `COMMAND` | +| `full` | + `IFUP`, `IFDOWN`, `IFRELOAD` | + +**A root peer on a unix socket starts with `full`**: the kernel vouches for the +uid via `SO_PEERCRED`, which beats any password. Everyone else starts at *none* +and authenticates with `AUTH <user> <password>`. + +Credentials live in the file named by `authfile`: + + admin:$pbkdf2-sha256$29000$<salt>$<hash>:full + watcher:$pbkdf2-sha256$29000$<salt>$<hash>:readonly + +Generate entries with `linkctl hash [user]`. The format is passlib's +`pbkdf2_sha256`, adapted base64 and all, so entries made by passlib or Django +work unchanged. A missing role field means `readonly`. + +The file is refused outright if it is world-writable, and warned about if it is +readable beyond its owner. **Binding a `tcp://` listener without an `authfile` +is a hard error**, so an unauthenticated network listener cannot be created by +forgetting something. + + 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. +A plugin is a **program**, not a shared library. linkd talks to it in RESP +(the Redis wire protocol), so a plugin can be written in anything that can read +and write a pipe. `tests/fixtures/plugin-echo.sh` is a working one in POSIX +shell, and the test suite drives it, so that claim is tested rather than +asserted. + +Plugins are declared in `/etc/linkd.cnf`: + + plugin /usr/lib/linkd/bcm spawned, RESP over stdin/stdout + plugin tcp://user:pass@host:6789 connected to, AUTH if credentials + optional failures do not fail operations + +On startup linkd asks each plugin `COMMAND` for the verbs it implements and +`INFO` for its identity. `INFO` must report `hardware_detected:0|1` in its +`# Stats` section; a plugin answering `0` is refused, which is how a driver for +absent hardware declines rather than failing later and opaquely. + +Operations are **broadcast** to every plugin advertising the verb, and all of +them are called even if an earlier one fails, so plugins cannot end up +disagreeing about what was applied: + + PORT ADMIN <if> <up|down> RIF ADD|DEL <if> <cidr> table <n> + PORT MTU <if> <mtu> NEIGH ADD|DEL <if> <ip> <mac> table <n> + PORT APPLY <if> [speed N] [fec X] [autoneg 0|1] + ROUTE ADD|DEL <dst> [via <gw> dev <if>]... [metric N] table <n> + RESYNC + +Any `-ERR` from a plugin fails the operation unless that plugin is `optional`. + +A plugin may also own configuration. When the `plugin` stanza hits a +sub-directive linkd does not recognise, it starts the plugin, asks `CONFIG +LIST` for the directives it accepts, and forwards matching ones as +`CONFIG SET <directive> <args...>`. A directive neither side claims ends the +stanza and is re-dispatched normally. + +A plugin that dies or stops answering is restarted with backoff (0.5s +doubling to 30s). While it is down its operations fail, so a switch never +silently accepts configuration it is not programming into hardware. -`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: +`plugins/bcm/` is the Broadcom XGS plugin, currently a skeleton that answers +`COMMAND`/`INFO` and errors on everything else. 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 diff --git a/plugins/bcm/.dep b/plugins/bcm/.dep @@ -0,0 +1 @@ +finwo/resp https://git.finwo.net/lib/resp.c/archives/heads/main.tar.gz diff --git a/plugins/bcm/.gitignore b/plugins/bcm/.gitignore @@ -0,0 +1,3 @@ +/lib/ +/bcm +*.o diff --git a/plugins/bcm/Makefile b/plugins/bcm/Makefile @@ -1,27 +1,26 @@ -BIN=bcm.so +BIN=bcm # 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 -# linkd's dataplane contract INCLUDES:= -INCLUDES+=-I ../../src +INCLUDES+=-I src LDFLAGS?= -LDFLAGS+=-shared ifneq ($(SDK),) INCLUDES+=-I $(SDK)/include LDFLAGS +=-L $(SDK)/build -lbcm endif +include lib/.dep/config.mk + CFLAGS+=$(INCLUDES) -SRC:=$(wildcard src/*.c) +SRC+=$(wildcard src/*.c) OBJ:=$(SRC:.c=.o) .PHONY: default @@ -46,4 +45,4 @@ clean: .PHONY: install install: $(BIN) - install -Dm0755 $(BIN) $(DESTDIR)/usr/lib/linkd/dataplane/$(BIN) + install -Dm0755 $(BIN) $(DESTDIR)/usr/lib/linkd/$(BIN) diff --git a/plugins/bcm/src/dataplane.c b/plugins/bcm/src/dataplane.c @@ -1,125 +0,0 @@ -// Broadcom XGS dataplane backend for linkd. -// -// 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 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. - -#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, keyed by rif->table for per-VRF LPM - (void)rif; - return DP_RET_ERROR; -} - -static int bcm_rif_del(const struct dp_rif *rif) { - // TODO: bcm_l3_intf_delete, keyed by rif->table - (void)rif; - return DP_RET_ERROR; -} - -static int bcm_neigh_add(const struct dp_neigh *neigh) { - // TODO: bcm_l3_host_add, keyed by neigh->table - (void)neigh; - return DP_RET_ERROR; -} - -static int bcm_neigh_del(const struct dp_neigh *neigh) { - // TODO: bcm_l3_host_delete, keyed by neigh->table - (void)neigh; - return DP_RET_ERROR; -} - -static int bcm_route_add(const struct dp_route *route) { - // TODO: bcm_l3_route_add with vrf context from route->table; for nh_count > 1 - // build a bcm_l3_egress_ecmp group and refcount it per table 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 with vrf context from route->table, 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 linkd_dataplane_ops = { - .abi = LINKD_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/plugins/bcm/src/main.c b/plugins/bcm/src/main.c @@ -0,0 +1,111 @@ +// Broadcom XGS plugin for linkd: a RESP server on stdin/stdout. +// +// Skeleton. Every operation below is unimplemented and answers with an error, +// which linkd surfaces rather than silently treating as applied. INFO reports +// hardware_detected, which is how linkd decides whether to use this plugin at +// all -- see the /dev/linux-kernel-bde probe in hardware_present(). +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <unistd.h> + +#include "finwo/resp.h" + +static int hardware_present(void) { + // The legacy BDE character device. A box with the module loaded and a + // supported ASIC has it; anything else does not. + // + // Note /proc/devices on captured hardware also lists linux_ngbde (the newer + // BDE) alongside this one. Which of the two bcm.so binds to is still an + // open question, so this check may need widening. + return access("/dev/linux-kernel-bde", F_OK) == 0; +} + +static void emit(resp_object *o) { + char *buf = NULL; + size_t len = 0; + if (resp_serialize(o, &buf, &len) == 0) { + fwrite(buf, 1, len, stdout); + fflush(stdout); + free(buf); + } + resp_free(o); +} + +static void reply_ok(void) { emit(resp_simple_init("OK")); } +static void reply_err(const char *msg) { emit(resp_error_init(msg)); } + +static void reply_commands(void) { + resp_object *a = resp_array_init(); + resp_array_append_bulk(a, "COMMAND"); + resp_array_append_bulk(a, "INFO"); + resp_array_append_bulk(a, "PORT"); + resp_array_append_bulk(a, "RIF"); + resp_array_append_bulk(a, "NEIGH"); + resp_array_append_bulk(a, "ROUTE"); + resp_array_append_bulk(a, "RESYNC"); + emit(a); +} + +// resp.c has no bulk constructor, only simple/error/array, so build it here. +static resp_object *bulk_init(const char *s) { + resp_object *o = calloc(1, sizeof(*o)); + if (!o) return NULL; + o->type = RESPT_BULK; + o->u.s = strdup(s); + return o; +} + +static void reply_info(void) { + char buf[256]; + snprintf(buf, sizeof(buf), + "# Server\r\n" + "name:bcm\r\n" + "version:0.1.0\r\n" + "\r\n" + "# Stats\r\n" + "hardware_detected:%d\r\n", + hardware_present() ? 1 : 0); + emit(bulk_init(buf)); +} + +static const char *arg(const resp_object *cmd, size_t i) { + if (!cmd || cmd->type != RESPT_ARRAY || i >= cmd->u.arr.n) return NULL; + const resp_object *e = &cmd->u.arr.elem[i]; + if (e->type != RESPT_BULK && e->type != RESPT_SIMPLE) return NULL; + return e->u.s; +} + +int main(void) { + for (;;) { + resp_object *cmd = resp_read(STDIN_FILENO); + if (!cmd) break; // EOF or malformed: linkd will restart us + + const char *verb = arg(cmd, 0); + if (!verb) { + reply_err("ERR malformed command"); + resp_free(cmd); + continue; + } + + if (!strcasecmp(verb, "COMMAND")) { + reply_commands(); + } else if (!strcasecmp(verb, "INFO")) { + reply_info(); + } else if (!strcasecmp(verb, "AUTH")) { + reply_ok(); + } else if (!strcasecmp(verb, "PORT") || !strcasecmp(verb, "RIF") || + !strcasecmp(verb, "NEIGH") || !strcasecmp(verb, "ROUTE") || + !strcasecmp(verb, "RESYNC")) { + // TODO: program the ASIC through the OpenBCM SDK. + reply_err("ERR not implemented"); + } else { + reply_err("ERR unknown command"); + } + + resp_free(cmd); + } + return 0; +} diff --git a/src/cli/client.c b/src/cli/client.c @@ -0,0 +1,152 @@ +#define _GNU_SOURCE +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/time.h> +#include <sys/un.h> +#include <unistd.h> + +#include "finwo/resp.h" + +#include "cli/client.h" +#include "config/daemon.h" + +#define CLIENT_TIMEOUT_S 10 + +static int connect_daemon(const char **path_out) { + const char *sockpath = linkd_client_socket(); + *path_out = sockpath; + + int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fd < 0) { + perror("socket"); + return -1; + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, sockpath, sizeof(addr.sun_path) - 1); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + fprintf(stderr, "connect %s failed: %s\nIs linkd running?\n", + sockpath, strerror(errno)); + close(fd); + return -1; + } + + struct timeval tv = { .tv_sec = CLIENT_TIMEOUT_S, .tv_usec = 0 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + return fd; +} + +static const char *map_get(const resp_object *m, const char *key) { + if (!m || m->type != RESPT_ARRAY) return NULL; + for (size_t i = 0; i + 1 < m->u.arr.n; i += 2) { + const resp_object *k = &m->u.arr.elem[i]; + if ((k->type == RESPT_BULK || k->type == RESPT_SIMPLE) && k->u.s && + !strcmp(k->u.s, key)) { + const resp_object *v = &m->u.arr.elem[i + 1]; + if (v->type == RESPT_BULK || v->type == RESPT_SIMPLE) return v->u.s; + } + } + return NULL; +} + +// An interface map renders as the config syntax it came from, so ifquery +// output can be pasted back into /etc/network/interfaces. +static int render_iface_map(const resp_object *m) { + const char *name = map_get(m, "name"); + if (!name) return 0; + + printf("iface %s\n", name); + for (size_t i = 0; i + 1 < m->u.arr.n; i += 2) { + const resp_object *k = &m->u.arr.elem[i]; + const resp_object *v = &m->u.arr.elem[i + 1]; + if (k->type != RESPT_BULK && k->type != RESPT_SIMPLE) continue; + if (!strcmp(k->u.s, "name")) continue; + if (v->type == RESPT_BULK || v->type == RESPT_SIMPLE) { + printf(" %s %s\n", k->u.s, v->u.s); + } else if (v->type == RESPT_INT) { + printf(" %s %lld\n", k->u.s, v->u.i); + } + } + return 1; +} + +static void render(const resp_object *o, int depth) { + if (!o) return; + switch (o->type) { + case RESPT_SIMPLE: + printf("%s\n", o->u.s ? o->u.s : ""); + break; + case RESPT_BULK: + if (o->u.s) fputs(o->u.s, stdout); + if (o->u.s && o->u.s[strlen(o->u.s) - 1] != '\n') fputc('\n', stdout); + break; + case RESPT_INT: + printf("%lld\n", o->u.i); + break; + case RESPT_ARRAY: + if (depth == 0 && render_iface_map(o)) break; + for (size_t i = 0; i < o->u.arr.n; i++) { + const resp_object *e = &o->u.arr.elem[i]; + if (e->type == RESPT_ARRAY && render_iface_map(e)) continue; + render(e, depth + 1); + } + break; + default: + break; + } +} + +int linkd_call(int argc, const char **argv) { + const char *sockpath = NULL; + int fd = connect_daemon(&sockpath); + if (fd < 0) return 1; + + resp_object *req = resp_array_init(); + if (!req) { close(fd); return 1; } + for (int i = 0; i < argc; i++) resp_array_append_bulk(req, argv[i]); + + char *buf = NULL; + size_t len = 0; + int rc = resp_serialize(req, &buf, &len); + resp_free(req); + if (rc != 0) { close(fd); return 1; } + + size_t sent = 0; + while (sent < len) { + ssize_t n = write(fd, buf + sent, len - sent); + if (n <= 0) { + if (n < 0 && errno == EINTR) continue; + fprintf(stderr, "write failed: %s\n", strerror(errno)); + free(buf); + close(fd); + return 1; + } + sent += (size_t)n; + } + free(buf); + + resp_object *reply = resp_read(fd); + close(fd); + + if (!reply) { + fprintf(stderr, "no reply from linkd\n"); + return 1; + } + + int status = 0; + if (reply->type == RESPT_ERROR) { + fprintf(stderr, "%s\n", reply->u.s ? reply->u.s : "error"); + status = 1; + } else { + render(reply, 0); + } + resp_free(reply); + return status; +} diff --git a/src/cli/client.h b/src/cli/client.h @@ -0,0 +1,8 @@ +#ifndef __LINKD_CLI_CLIENT_H__ +#define __LINKD_CLI_CLIENT_H__ + +// Connect to the daemon, send argv as a RESP command, render the reply. +// Returns a process exit code: 0 on success, 1 on error. +int linkd_call(int argc, const char **argv); + +#endif // __LINKD_CLI_CLIENT_H__ diff --git a/src/cli/ifdown.c b/src/cli/ifdown.c @@ -1,49 +1,16 @@ #define _GNU_SOURCE -#include <errno.h> #include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/socket.h> -#include <sys/un.h> -#include <unistd.h> -#include "config/daemon.h" -#include "registry.h" +#include "cli/client.h" +#include "registry.h" int main_ifdown(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "usage: ifdown <interface>\n"); return 1; } - char out[4096]; - int len = snprintf(out, sizeof(out), "ifdown"); - for (int i = 1; i < argc; i++) len += snprintf(out+len, sizeof(out)-len, " %s", argv[i]); - len += snprintf(out+len, sizeof(out)-len, "\n"); - 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, sockpath, sizeof(addr.sun_path)-1); - if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) { - 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 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]; - int saw_err = 0; - FILE *f = fdopen(sock, "r"); - if (!f) { perror("fdopen"); close(sock); return 1; } - while (fgets(buf, sizeof(buf), f)) { - if (!strncmp(buf, "ERR", 3)) saw_err = 1; - fputs(buf, stdout); - } - fclose(f); - return saw_err ? 1 : 0; + const char *cmd[] = { "IFDOWN", argv[1] }; + return linkd_call(2, cmd); } __attribute__((constructor)) diff --git a/src/cli/ifquery.c b/src/cli/ifquery.c @@ -1,45 +1,16 @@ #define _GNU_SOURCE -#include <errno.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/socket.h> -#include <sys/un.h> -#include <unistd.h> -#include "config/daemon.h" -#include "registry.h" +#include <stddef.h> +#include "cli/client.h" +#include "registry.h" 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, sockpath, sizeof(addr.sun_path)-1); - if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) { - 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 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]; - int saw_err = 0; - FILE *f = fdopen(sock, "r"); - if (!f) { perror("fdopen"); close(sock); return 1; } - while (fgets(buf, sizeof(buf), f)) { - if (!strncmp(buf, "ERR", 3)) saw_err = 1; - fputs(buf, stdout); + const char *cmd[2] = { "IFQUERY", NULL }; + if (argc > 1) { + cmd[1] = argv[1]; + return linkd_call(2, cmd); } - fclose(f); - return saw_err ? 1 : 0; + return linkd_call(1, cmd); } __attribute__((constructor)) diff --git a/src/cli/ifreload.c b/src/cli/ifreload.c @@ -1,48 +1,11 @@ #define _GNU_SOURCE -#include <errno.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/socket.h> -#include <sys/un.h> -#include <unistd.h> -#include "config/daemon.h" +#include "cli/client.h" #include "registry.h" - int main_ifreload(int argc, char *argv[]) { - if (argc != 1) { - fprintf(stderr, "usage: ifreload\n"); - return 1; - } - (void)argv; - char out[4096]; - int len = snprintf(out, sizeof(out), "ifreload\n"); - 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, sockpath, sizeof(addr.sun_path)-1); - if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) { - 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 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]; - int saw_err = 0; - FILE *f = fdopen(sock, "r"); - if (!f) { perror("fdopen"); close(sock); return 1; } - while (fgets(buf, sizeof(buf), f)) { - if (!strncmp(buf, "ERR", 3)) saw_err = 1; - fputs(buf, stdout); - } - fclose(f); - return saw_err ? 1 : 0; + (void)argc; (void)argv; + const char *cmd[] = { "IFRELOAD" }; + return linkd_call(1, cmd); } __attribute__((constructor)) diff --git a/src/cli/ifup.c b/src/cli/ifup.c @@ -1,54 +1,16 @@ #define _GNU_SOURCE -#include <errno.h> #include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/socket.h> -#include <sys/un.h> -#include <unistd.h> -#include "config/daemon.h" -#include "registry.h" +#include "cli/client.h" +#include "registry.h" int main_ifup(int argc, char *argv[]) { - // ifup <if> -- thin wrapper, same socket logic as linkctl but fixed command if (argc < 2) { fprintf(stderr, "usage: ifup <interface>\n"); return 1; } - // Build command: "ifup <if> [extra args]" - char out[4096]; - int len = snprintf(out, sizeof(out), "ifup"); - for (int i = 1; i < argc; i++) { - len += snprintf(out+len, sizeof(out)-len, " %s", argv[i]); - } - len += snprintf(out+len, sizeof(out)-len, "\n"); - - 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, sockpath, sizeof(addr.sun_path)-1); - if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) { - 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 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]; - int saw_err = 0; - FILE *f = fdopen(sock, "r"); - if (!f) { perror("fdopen"); close(sock); return 1; } - while (fgets(buf, sizeof(buf), f)) { - if (!strncmp(buf, "ERR", 3)) saw_err = 1; - fputs(buf, stdout); - } - fclose(f); - return saw_err ? 1 : 0; + const char *cmd[] = { "IFUP", argv[1] }; + return linkd_call(2, cmd); } __attribute__((constructor)) diff --git a/src/cli/linkctl.c b/src/cli/linkctl.c @@ -1,95 +1,97 @@ #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 <strings.h> +#include <termios.h> #include <unistd.h> -#include "config/daemon.h" +#include "cli/client.h" #include "registry.h" +#include "util/auth.h" +static void usage(const char *prog) { + fprintf(stderr, "usage: %s <command> [args...]\n", prog); + fprintf(stderr, " %s COMMAND list what the daemon accepts\n", prog); + fprintf(stderr, " %s hash [user] generate a password-file entry\n", prog); +} -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; +static char *read_password(const char *prompt) { + struct termios old, quiet; + int tty = isatty(STDIN_FILENO); + char *line = NULL; + size_t cap = 0; + + if (tty) { + fprintf(stderr, "%s", prompt); + fflush(stderr); + if (tcgetattr(STDIN_FILENO, &old) == 0) { + quiet = old; + quiet.c_lflag &= (tcflag_t)~ECHO; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &quiet); + } else { + tty = 0; + } } - 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; + ssize_t n = getline(&line, &cap, stdin); + + if (tty) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &old); + fprintf(stderr, "\n"); } - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, sockpath, sizeof(addr.sun_path)-1); + if (n <= 0) { free(line); return NULL; } + char *nl = strpbrk(line, "\r\n"); + if (nl) *nl = '\0'; + return line; +} + +static int cmd_hash(int argc, char *argv[]) { + const char *user = argc > 1 ? argv[1] : NULL; - 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); + char *pass = read_password("Password: "); + if (!pass || !*pass) { + fprintf(stderr, "linkctl: empty password\n"); + free(pass); 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); + if (isatty(STDIN_FILENO)) { + char *again = read_password("Again: "); + if (!again || strcmp(pass, again) != 0) { + fprintf(stderr, "linkctl: passwords do not match\n"); + free(pass); + free(again); 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); + free(again); } - // 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); + char *hash = auth_make_hash(pass, AUTH_DEFAULT_ITERATIONS); + memset(pass, 0, strlen(pass)); + free(pass); + + if (!hash) { + fprintf(stderr, "linkctl: could not generate hash\n"); 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; + + if (user) printf("%s:%s:readonly\n", user, hash); + else printf("%s\n", hash); + free(hash); return 0; } +int main_linkctl(int argc, char *argv[]) { + if (argc < 2) { + usage(argv[0]); + return 1; + } + if (!strcasecmp(argv[1], "hash")) return cmd_hash(argc - 1, argv + 1); + + return linkd_call(argc - 1, (const char **)argv + 1); +} + __attribute__((constructor)) static void register_linkctl(void) { cli_register("linkctl", main_linkctl); diff --git a/src/cli/linkd.c b/src/cli/linkd.c @@ -20,7 +20,7 @@ extern "C" { #include "config/ports.h" #include "config/ifaces.h" #include "dataplane.h" -#include "dataplane/registry.h" +#include "dataplane/plugin.h" #include "ipc.h" #include "netlink/netlink.h" #include "netlink/rtnl.h" @@ -66,11 +66,8 @@ static void stop_handler(int sig) { } 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; + struct linkd_port *cur; + struct dp_port port; for ( cur = ports_list() ; cur ; cur = cur->next ) { memset(&port, 0, sizeof(port)); @@ -79,7 +76,7 @@ static int apply_ports(void) { port.fec = cur->fec; port.autoneg = cur->autoneg; - if (dp->port_apply(&port) != DP_RET_OK) { + if (dp_port_apply(&port) != DP_RET_OK) { log_warn("%s: failed to apply port configuration", cur->name); } } @@ -219,7 +216,6 @@ 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 = ""; @@ -230,7 +226,6 @@ int main_linkd(int argc, char **argv) { 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), @@ -242,7 +237,6 @@ int main_linkd(int argc, char **argv) { daemon_cfg_register(); ports_register(); ifaces_register(); - ipc_register(); struct argparse argparse; argparse_init(&argparse, options, usage, 0); @@ -321,23 +315,13 @@ int main_linkd(int argc, char **argv) { } } - // 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. + // Startup order is load-bearing: ports -> plugins -> 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; - } + // A plugin that fails to start is not fatal: the backoff retries it, and + // with none configured every dataplane call is a no-op. + dp_start(); if (ipc_init() != 0) { log_warn("ipc: init failed, ifup/ifquery will not be available"); @@ -349,7 +333,7 @@ int main_linkd(int argc, char **argv) { nl_fd = nl_open(); if (nl_fd < 0) { - dp_fini(); + dp_stop(); ports_free(); ifaces_free(); ipc_fini(); @@ -364,7 +348,7 @@ int main_linkd(int argc, char **argv) { log_fatal("netlink: initial resync failed"); close(nl_fd); ipc_fini(); - dp_fini(); + dp_stop(); ports_free(); ifaces_free(); return 1; @@ -374,7 +358,7 @@ int main_linkd(int argc, char **argv) { if (ready_publish(ready_file) != 0) { close(nl_fd); ipc_fini(); - dp_fini(); + dp_stop(); ports_free(); ifaces_free(); return 1; @@ -387,7 +371,7 @@ int main_linkd(int argc, char **argv) { ipc_fini(); ready_remove(ready_file); - dp_fini(); + dp_stop(); ports_free(); ifaces_free(); diff --git a/src/config/daemon.c b/src/config/daemon.c @@ -9,6 +9,7 @@ #include "finwo/cnfparse.h" #include "config/daemon.h" +#include "dataplane/plugin.h" #include "util/config.h" static struct linkd_daemon_cfg cfg; @@ -147,7 +148,21 @@ static struct cnf_directive *h_plugin(FILE *fd, struct cnf_directive *dir, void continue; } - // Not ours: hand it back so the caller re-dispatches it. + // Unknown to us: the plugin gets first refusal. Starting it here is what + // makes its config vocabulary discoverable at all -- CONFIG LIST cannot + // be asked of a process that is not running. + struct dp_plugin *dp = dp_plugin_for(p->target); + if (dp && dp_plugin_config_supports(dp, sub->name)) { + if (dp_plugin_config_set(dp, sub->name, sub->argv, sub->argc) != 0) { + cfg_error("plugin %s rejected `%s'", p->target, sub->name); + cnf_directive_free(sub); + return NULL; + } + cnf_directive_free(sub); + continue; + } + + // Neither side claims it: ends the stanza, hand it back for re-dispatch. return sub; } } diff --git a/src/dataplane.h b/src/dataplane.h @@ -8,19 +8,14 @@ #define DP_RET_ERROR -1 #define DP_RET_OK 0 -// Bumped whenever struct dp_ops changes shape once a real plugin has shipped. -// 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 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 LINKD_DATAPLANE_DIR "/usr/lib/linkd/dataplane" - -// Symbol every plugin must export -#define LINKD_DATAPLANE_SYM "linkd_dataplane_ops" +// Plugins are separate processes speaking RESP, declared in /etc/linkd.cnf: +// +// plugin /usr/lib/linkd/bcm spawned, RESP over stdio +// plugin tcp://user:pass@host:6789 connected to, RESP over the socket +// +// They advertise what they implement via COMMAND, and each operation is +// broadcast to every plugin advertising its verb. There is no in-process ABI +// and nothing is dlopen'd, so a plugin can be written in any language. enum dp_fec { DP_FEC_UNSET = 0, @@ -75,55 +70,35 @@ struct dp_route { uint32_t table; // kernel route-table id; RT_TABLE_MAIN = default VRF }; -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); +// Start every plugin declared in the config. Plugins that fail to start are +// retried with backoff; this is not an error in itself. +int dp_start(void); +void dp_stop(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); -}; +// True when at least one plugin is configured. Without any, every operation +// below is a successful no-op -- the kernel has already done the work by the +// time these are called. +int dp_have_plugins(void); -// Built-in backend. Always available, always probes successfully. -const struct dp_ops * dp_kernel_ops(void); +// Operations. Each is broadcast to every plugin advertising the verb; all of +// them are called even if an earlier one fails, so plugins cannot end up in +// states that disagree with each other. Returns DP_RET_ERROR if any +// non-optional plugin failed. +int dp_port_apply(const struct dp_port *port); +int dp_port_admin(const char *ifname, bool up); +int dp_port_mtu(const char *ifname, uint32_t mtu); -// Load plugins from LINKD_DATAPLANE_DIR. Safe to call when the directory is -// absent or empty. -int dp_plugins_load(const char *dir); +int dp_rif_add(const struct dp_rif *rif); +int dp_rif_del(const struct dp_rif *rif); -// 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); +int dp_neigh_add(const struct dp_neigh *neigh); +int dp_neigh_del(const struct dp_neigh *neigh); -const struct dp_ops * dp_active(void); +int dp_route_add(const struct dp_route *route); +int dp_route_del(const struct dp_route *route); -int dp_init(void); -void dp_fini(void); +// Netlink drops messages under load (ENOBUFS), so following events alone +// drifts; plugins are told to rebuild from scratch periodically. +int dp_resync(void); #endif // __LINKD_DATAPLANE_H__ diff --git a/src/dataplane/kernel.c b/src/dataplane/kernel.c @@ -1,73 +0,0 @@ -// 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. linkd on generic -// hardware is therefore close to inert by design: it parses configuration, -// validates it, and lets Linux do the work. -// -// The table field on rif/neigh/route is accepted and ignored here: there is -// nothing per-VRF to replicate when the kernel itself forwards. - -#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 = LINKD_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/ops.c b/src/dataplane/ops.c @@ -0,0 +1,185 @@ +#define _GNU_SOURCE +#include <arpa/inet.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> + +#include "finwo/resp.h" +#include "rxi/log.h" + +#include "dataplane.h" +#include "dataplane/plugin.h" + +#define MAX_ARGV 24 + +// Every supporter is called even after one fails, so plugins cannot end up +// disagreeing about what was applied. `optional` failures do not fail the op. +static int broadcast(const char **argv, int argc) { + int failed = 0; + int sent = 0; + + for (struct dp_plugin *p = dp_plugin_list(); p; p = p->next) { + // Retry a down plugin here: an operation is exactly when we want it back. + if (!p->up) dp_plugin_up(p); + if (!dp_plugin_supports(p, argv[0])) continue; + + sent++; + resp_object *r = dp_plugin_call(p, argv, argc); + if (!r) { + if (!p->optional) failed = 1; + continue; + } + if (r->type == RESPT_ERROR) { + log_error("plugin %s: %s %s: %s", p->target, argv[0], + argc > 1 ? argv[1] : "", r->u.s ? r->u.s : "error"); + if (!p->optional) failed = 1; + } + resp_free(r); + } + + (void)sent; + return failed ? DP_RET_ERROR : DP_RET_OK; +} + +static const char *fec_str(enum dp_fec f) { + switch (f) { + case DP_FEC_OFF: return "off"; + case DP_FEC_AUTO: return "auto"; + case DP_FEC_RS: return "rs"; + case DP_FEC_BASER: return "baser"; + default: return NULL; + } +} + +// dp_addr -> "10.0.0.1/24" +static int addr_str(const struct dp_addr *a, char *out, size_t len) { + char ip[INET6_ADDRSTRLEN]; + if (!inet_ntop(a->family, a->addr, ip, sizeof(ip))) return -1; + snprintf(out, len, "%s/%u", ip, a->prefixlen); + return 0; +} + +static int addr_plain(const struct dp_addr *a, char *out, size_t len) { + return inet_ntop(a->family, a->addr, out, (socklen_t)len) ? 0 : -1; +} + +int dp_port_apply(const struct dp_port *port) { + if (!dp_have_plugins()) return DP_RET_OK; + + const char *argv[MAX_ARGV]; + char speed[16], autoneg[12]; + int argc = 0; + + argv[argc++] = "PORT"; + argv[argc++] = "APPLY"; + argv[argc++] = port->name; + + if (port->speed) { + snprintf(speed, sizeof(speed), "%u", port->speed); + argv[argc++] = "speed"; + argv[argc++] = speed; + } + const char *f = fec_str(port->fec); + if (f) { + argv[argc++] = "fec"; + argv[argc++] = f; + } + if (port->autoneg >= 0) { + snprintf(autoneg, sizeof(autoneg), "%d", port->autoneg); + argv[argc++] = "autoneg"; + argv[argc++] = autoneg; + } + return broadcast(argv, argc); +} + +int dp_port_admin(const char *ifname, bool up) { + if (!dp_have_plugins()) return DP_RET_OK; + const char *argv[] = { "PORT", "ADMIN", ifname, up ? "up" : "down" }; + return broadcast(argv, 4); +} + +int dp_port_mtu(const char *ifname, uint32_t mtu) { + if (!dp_have_plugins()) return DP_RET_OK; + char m[16]; + snprintf(m, sizeof(m), "%u", mtu); + const char *argv[] = { "PORT", "MTU", ifname, m }; + return broadcast(argv, 4); +} + +static int rif_op(const char *sub, const struct dp_rif *rif) { + if (!dp_have_plugins()) return DP_RET_OK; + char addr[INET6_ADDRSTRLEN + 8], table[16]; + if (addr_str(&rif->addr, addr, sizeof(addr)) != 0) return DP_RET_ERROR; + snprintf(table, sizeof(table), "%u", rif->table); + const char *argv[] = { "RIF", sub, rif->ifname, addr, "table", table }; + return broadcast(argv, 6); +} + +int dp_rif_add(const struct dp_rif *rif) { return rif_op("ADD", rif); } +int dp_rif_del(const struct dp_rif *rif) { return rif_op("DEL", rif); } + +static int neigh_op(const char *sub, const struct dp_neigh *n) { + if (!dp_have_plugins()) return DP_RET_OK; + char ip[INET6_ADDRSTRLEN], mac[18], table[16]; + if (addr_plain(&n->addr, ip, sizeof(ip)) != 0) return DP_RET_ERROR; + snprintf(mac, sizeof(mac), "%02x:%02x:%02x:%02x:%02x:%02x", + n->mac[0], n->mac[1], n->mac[2], n->mac[3], n->mac[4], n->mac[5]); + snprintf(table, sizeof(table), "%u", n->table); + const char *argv[] = { "NEIGH", sub, n->ifname, ip, mac, "table", table }; + return broadcast(argv, 7); +} + +int dp_neigh_add(const struct dp_neigh *n) { return neigh_op("ADD", n); } +int dp_neigh_del(const struct dp_neigh *n) { return neigh_op("DEL", n); } + +// ROUTE ADD <dst> [via <gw> dev <if>]... [metric N] [table N] +// Nexthops are flattened rather than nested so a shell plugin can parse them. +static int route_op(const char *sub, const struct dp_route *r) { + if (!dp_have_plugins()) return DP_RET_OK; + + const char *argv[MAX_ARGV]; + char dst[INET6_ADDRSTRLEN + 8]; + char gw[4][INET6_ADDRSTRLEN]; + char metric[16], table[16]; + int argc = 0; + + if (addr_str(&r->dst, dst, sizeof(dst)) != 0) return DP_RET_ERROR; + + argv[argc++] = "ROUTE"; + argv[argc++] = sub; + argv[argc++] = dst; + + size_t nh = r->nh_count; + if (nh > 4) nh = 4; // argv budget; ECMP wider than this is truncated + for (size_t i = 0; i < nh; i++) { + if (argc + 4 >= MAX_ARGV) break; + if (addr_plain(&r->nh[i].gw, gw[i], sizeof(gw[i])) == 0) { + argv[argc++] = "via"; + argv[argc++] = gw[i]; + } + if (r->nh[i].ifname) { + argv[argc++] = "dev"; + argv[argc++] = r->nh[i].ifname; + } + } + if (r->metric) { + snprintf(metric, sizeof(metric), "%u", r->metric); + argv[argc++] = "metric"; + argv[argc++] = metric; + } + snprintf(table, sizeof(table), "%u", r->table); + argv[argc++] = "table"; + argv[argc++] = table; + + return broadcast(argv, argc); +} + +int dp_route_add(const struct dp_route *r) { return route_op("ADD", r); } +int dp_route_del(const struct dp_route *r) { return route_op("DEL", r); } + +int dp_resync(void) { + if (!dp_have_plugins()) return DP_RET_OK; + const char *argv[] = { "RESYNC" }; + return broadcast(argv, 1); +} diff --git a/src/dataplane/plugin.c b/src/dataplane/plugin.c @@ -1,67 +1,452 @@ -// Dataplane plugin loader. -// -// 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 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 linkd_dataplane_ops`. - -#include <dirent.h> -#include <dlfcn.h> +#define _GNU_SOURCE +#include <errno.h> +#include <netdb.h> +#include <signal.h> #include <stdio.h> +#include <stdlib.h> #include <string.h> +#include <strings.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> +#include "finwo/resp.h" +#include "rxi/log.h" + +#include "config/daemon.h" #include "dataplane.h" +#include "dataplane/plugin.h" + +#define PLUGIN_TIMEOUT_S 5 +#define BACKOFF_MIN_MS 500 +#define BACKOFF_MAX_MS 30000 +#define MAX_CONFIG_ARGV 32 -#include "registry.h" +static struct dp_plugin *plugins = NULL; -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; +static void strlist_free(char ***list, size_t *n) { + for (size_t i = 0; i < *n; i++) free((*list)[i]); + free(*list); + *list = NULL; + *n = 0; +} - if (!dir) dir = LINKD_DATAPLANE_DIR; +static int strlist_add(char ***list, size_t *n, const char *s) { + char **g = realloc(*list, sizeof(char *) * (*n + 1)); + if (!g) return -1; + *list = g; + (*list)[*n] = strdup(s); + if (!(*list)[*n]) return -1; + (*n)++; + return 0; +} - dh = opendir(dir); - if (!dh) { - // Not an error: no plugin directory simply means no hardware backends. - return 0; +static int strlist_has(char **list, size_t n, const char *s) { + for (size_t i = 0; i < n; i++) { + if (!strcasecmp(list[i], s)) return 1; } + return 0; +} - while ((ent = readdir(dh))) { - len = strlen(ent->d_name); - if (len < 4) continue; - if (strcmp(ent->d_name + len - 3, ".so")) continue; +static void now_plus_ms(struct timespec *out, int ms) { + clock_gettime(CLOCK_MONOTONIC, out); + out->tv_sec += ms / 1000; + out->tv_nsec += (long)(ms % 1000) * 1000000L; + if (out->tv_nsec >= 1000000000L) { out->tv_sec++; out->tv_nsec -= 1000000000L; } +} - snprintf(path, sizeof(path), "%s/%s", dir, ent->d_name); +static int deadline_passed(const struct timespec *t) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec != t->tv_sec) return now.tv_sec > t->tv_sec; + return now.tv_nsec >= t->tv_nsec; +} + +static int set_timeouts(int fd) { + struct timeval tv = { .tv_sec = PLUGIN_TIMEOUT_S, .tv_usec = 0 }; + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) return -1; + if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) != 0) return -1; + return 0; +} + +// socketpair, not pipes: SO_RCVTIMEO works on it, so a wedged plugin cannot +// block the daemon forever. +static int spawn_child(struct dp_plugin *p) { + int sv[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv) != 0) { + log_error("plugin %s: socketpair: %s", p->target, strerror(errno)); + return -1; + } + + pid_t pid = fork(); + if (pid < 0) { + log_error("plugin %s: fork: %s", p->target, strerror(errno)); + close(sv[0]); + close(sv[1]); + return -1; + } - handle = dlopen(path, RTLD_NOW | RTLD_LOCAL); - if (!handle) { - fprintf(stderr, "dataplane: %s: %s\n", path, dlerror()); - continue; + if (pid == 0) { + // stderr is inherited, so plugin diagnostics reach the daemon log. + dup2(sv[1], STDIN_FILENO); + dup2(sv[1], STDOUT_FILENO); + if (sv[1] > STDERR_FILENO) close(sv[1]); + close(sv[0]); + signal(SIGPIPE, SIG_DFL); + execl(p->target, p->target, (char *)NULL); + fprintf(stderr, "plugin %s: exec failed: %s\n", p->target, strerror(errno)); + _exit(127); + } + + close(sv[1]); + if (set_timeouts(sv[0]) != 0) { + log_error("plugin %s: setsockopt: %s", p->target, strerror(errno)); + close(sv[0]); + return -1; + } + p->fd = sv[0]; + p->pid = pid; + return 0; +} + +// tcp://[user:pass@]host[:port] +static int parse_tcp(const char *url, char **host, char **port, char **user, char **pass) { + const char *rest = url + 6; // skip tcp:// + const char *at = strrchr(rest, '@'); + *user = *pass = NULL; + + if (at) { + size_t clen = (size_t)(at - rest); + char *cred = strndup(rest, clen); + if (!cred) return -1; + char *colon = strchr(cred, ':'); + if (colon) { + *colon = '\0'; + *user = strdup(cred); + *pass = strdup(colon + 1); + } else { + *user = strdup(cred); } + free(cred); + rest = at + 1; + } + + const char *colon = strrchr(rest, ':'); + if (colon) { + *host = strndup(rest, (size_t)(colon - rest)); + *port = strdup(colon + 1); + } else { + *host = strdup(rest); + *port = strdup("6789"); + } + return (*host && *port) ? 0 : -1; +} + +static int connect_tcp(struct dp_plugin *p) { + char *host = NULL, *port = NULL; + free(p->user); free(p->pass); + p->user = p->pass = NULL; + + if (parse_tcp(p->target, &host, &port, &p->user, &p->pass) != 0) { + log_error("plugin %s: malformed address", p->target); + free(host); free(port); + return -1; + } + + struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM }; + struct addrinfo *res = NULL; + int gai = getaddrinfo(host, port, &hints, &res); + free(host); free(port); + if (gai != 0) { + log_error("plugin %s: resolve: %s", p->target, gai_strerror(gai)); + return -1; + } - ops = dlsym(handle, LINKD_DATAPLANE_SYM); - if (!ops) { - fprintf(stderr, "dataplane: %s: missing symbol `%s`\n", path, LINKD_DATAPLANE_SYM); - dlclose(handle); - continue; + int fd = -1; + for (struct addrinfo *ai = res; ai; ai = ai->ai_next) { + fd = socket(ai->ai_family, ai->ai_socktype | SOCK_CLOEXEC, ai->ai_protocol); + if (fd < 0) continue; + if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) break; + close(fd); + fd = -1; + } + freeaddrinfo(res); + + if (fd < 0) { + log_error("plugin %s: connect failed: %s", p->target, strerror(errno)); + return -1; + } + if (set_timeouts(fd) != 0) { + close(fd); + return -1; + } + p->fd = fd; + p->pid = 0; + return 0; +} + +static int write_all(int fd, const char *buf, size_t len) { + while (len) { + ssize_t n = write(fd, buf, len); + if (n <= 0) { + if (n < 0 && errno == EINTR) continue; + return -1; } + buf += n; + len -= (size_t)n; + } + return 0; +} + +resp_object *dp_plugin_call(struct dp_plugin *p, const char **argv, int argc) { + if (!p->up || p->fd < 0) return NULL; + + resp_object *req = resp_array_init(); + if (!req) return NULL; + for (int i = 0; i < argc; i++) resp_array_append_bulk(req, argv[i]); - if (dp_register(ops) != DP_RET_OK) { - dlclose(handle); - continue; + char *buf = NULL; + size_t len = 0; + if (resp_serialize(req, &buf, &len) != 0) { + resp_free(req); + return NULL; + } + resp_free(req); + + int rc = write_all(p->fd, buf, len); + free(buf); + if (rc != 0) { + dp_plugin_down(p, "write failed"); + return NULL; + } + + // resp_read returns NULL for timeout, EOF and parse errors alike; all three + // mean the plugin is unusable. + resp_object *reply = resp_read(p->fd); + if (!reply) { + dp_plugin_down(p, "no reply (timeout, EOF or malformed)"); + return NULL; + } + return reply; +} + +static int reply_is_error(const resp_object *o) { + return o && o->type == RESPT_ERROR; +} + +static void load_commands(struct dp_plugin *p) { + strlist_free(&p->commands, &p->commands_n); + const char *argv[] = { "COMMAND" }; + resp_object *r = dp_plugin_call(p, argv, 1); + if (!r) return; + + if (r->type == RESPT_ARRAY) { + for (size_t i = 0; i < r->u.arr.n; i++) { + resp_object *e = &r->u.arr.elem[i]; + // Accept a flat list of names or redis' array-per-command form. + const char *name = NULL; + if (e->type == RESPT_BULK || e->type == RESPT_SIMPLE) { + name = e->u.s; + } else if (e->type == RESPT_ARRAY && e->u.arr.n > 0) { + resp_object *f = &e->u.arr.elem[0]; + if (f->type == RESPT_BULK || f->type == RESPT_SIMPLE) name = f->u.s; + } + if (name) strlist_add(&p->commands, &p->commands_n, name); } + } + resp_free(r); +} - // Deliberately leaked: the ops table stays live for the process lifetime. - loaded++; +static void load_config_keys(struct dp_plugin *p) { + strlist_free(&p->config_keys, &p->config_keys_n); + if (!dp_plugin_supports(p, "CONFIG")) return; + + const char *argv[] = { "CONFIG", "LIST" }; + resp_object *r = dp_plugin_call(p, argv, 2); + if (!r) return; + + if (r->type == RESPT_ARRAY) { + for (size_t i = 0; i < r->u.arr.n; i++) { + resp_object *e = &r->u.arr.elem[i]; + if (e->type == RESPT_BULK || e->type == RESPT_SIMPLE) { + strlist_add(&p->config_keys, &p->config_keys_n, e->u.s); + } + } + } + resp_free(r); +} + +// redis INFO format; hardware_detected is the only field we interpret. +static void load_info(struct dp_plugin *p) { + p->hardware = -1; + const char *argv[] = { "INFO" }; + resp_object *r = dp_plugin_call(p, argv, 1); + if (!r) return; + + if ((r->type == RESPT_BULK || r->type == RESPT_SIMPLE) && r->u.s) { + const char *k = strstr(r->u.s, "hardware_detected:"); + if (k) p->hardware = atoi(k + strlen("hardware_detected:")) ? 1 : 0; + } + resp_free(r); +} + +static int authenticate(struct dp_plugin *p) { + if (!p->pass && !p->user) return 0; + const char *argv[3]; + int argc = 0; + argv[argc++] = "AUTH"; + if (p->user && p->pass) { + argv[argc++] = p->user; + argv[argc++] = p->pass; + } else { + argv[argc++] = p->user ? p->user : p->pass; + } + resp_object *r = dp_plugin_call(p, argv, argc); + if (!r) return -1; + int bad = reply_is_error(r); + if (bad) log_error("plugin %s: AUTH rejected: %s", p->target, r->u.s ? r->u.s : ""); + resp_free(r); + return bad ? -1 : 0; +} + +void dp_plugin_down(struct dp_plugin *p, const char *why) { + if (p->fd >= 0) { close(p->fd); p->fd = -1; } + if (p->pid > 0) { + kill(p->pid, SIGTERM); + waitpid(p->pid, NULL, 0); + p->pid = 0; } + if (p->up) log_error("plugin %s: down (%s)", p->target, why ? why : "unknown"); + p->up = 0; + + p->backoff_ms = p->backoff_ms ? p->backoff_ms * 2 : BACKOFF_MIN_MS; + if (p->backoff_ms > BACKOFF_MAX_MS) p->backoff_ms = BACKOFF_MAX_MS; + now_plus_ms(&p->retry_at, p->backoff_ms); +} + +int dp_plugin_up(struct dp_plugin *p) { + if (p->up) return 0; + if (p->backoff_ms && !deadline_passed(&p->retry_at)) return -1; + + int rc = !strncmp(p->target, "tcp://", 6) ? connect_tcp(p) : spawn_child(p); + if (rc != 0) { + dp_plugin_down(p, "start failed"); + return -1; + } + + // Provisionally up so dp_plugin_call() will talk to it during handshake. + p->up = 1; + + if (authenticate(p) != 0) { + dp_plugin_down(p, "authentication failed"); + return -1; + } + + load_commands(p); + if (!p->up) return -1; + load_info(p); + if (!p->up) return -1; + load_config_keys(p); + if (!p->up) return -1; + + if (p->hardware == 0) { + log_error("plugin %s: reports no supported hardware, not using it", p->target); + dp_plugin_down(p, "no hardware"); + return -1; + } + + p->backoff_ms = 0; + log_info("plugin %s: up, %zu commands", p->target, p->commands_n); + return 0; +} + +int dp_plugin_supports(const struct dp_plugin *p, const char *verb) { + return p->up && strlist_has(p->commands, p->commands_n, verb); +} + +int dp_plugin_config_supports(const struct dp_plugin *p, const char *key) { + return strlist_has(p->config_keys, p->config_keys_n, key); +} + +struct dp_plugin *dp_plugin_list(void) { + return plugins; +} + +static struct dp_plugin *plugin_entry(const char *target) { + for (struct dp_plugin *p = plugins; p; p = p->next) { + if (!strcmp(p->target, target)) return p; + } + struct dp_plugin *p = calloc(1, sizeof(*p)); + if (!p) return NULL; + p->target = strdup(target); + if (!p->target) { free(p); return NULL; } + p->fd = -1; + struct dp_plugin **tail = &plugins; + while (*tail) tail = &(*tail)->next; + *tail = p; + return p; +} + +struct dp_plugin *dp_plugin_for(const char *target) { + struct dp_plugin *p = plugin_entry(target); + if (!p) return NULL; + dp_plugin_up(p); + return p; +} + +int dp_plugin_config_set(struct dp_plugin *p, const char *key, char **argv, size_t argc) { + const char *a[MAX_CONFIG_ARGV]; + size_t n = 0; + a[n++] = "CONFIG"; + a[n++] = "SET"; + a[n++] = key; + for (size_t i = 0; i < argc && n < MAX_CONFIG_ARGV; i++) a[n++] = argv[i]; + + resp_object *r = dp_plugin_call(p, a, (int)n); + if (!r) return -1; + int bad = reply_is_error(r); + if (bad) { + log_error("plugin %s: CONFIG SET %s: %s", p->target, key, r->u.s ? r->u.s : "error"); + } + resp_free(r); + return bad ? -1 : 0; +} + +int dp_start(void) { + const struct linkd_daemon_cfg *cfg = daemon_cfg(); + + for (struct linkd_plugin_cfg *c = cfg->plugins; c; c = c->next) { + struct dp_plugin *p = plugin_entry(c->target); + if (!p) return DP_RET_ERROR; + p->optional = c->optional; + // A plugin that cannot start yet is not fatal; the backoff retries it. + dp_plugin_up(p); + } + return DP_RET_OK; +} + +void dp_stop(void) { + struct dp_plugin *p = plugins; + while (p) { + struct dp_plugin *next = p->next; + if (p->fd >= 0) close(p->fd); + if (p->pid > 0) { + kill(p->pid, SIGTERM); + waitpid(p->pid, NULL, 0); + } + strlist_free(&p->commands, &p->commands_n); + strlist_free(&p->config_keys, &p->config_keys_n); + free(p->target); + free(p->user); + free(p->pass); + free(p); + p = next; + } + plugins = NULL; +} - closedir(dh); - return loaded; +int dp_have_plugins(void) { + return plugins != NULL; } diff --git a/src/dataplane/plugin.h b/src/dataplane/plugin.h @@ -0,0 +1,56 @@ +#ifndef __LINKD_DATAPLANE_PLUGIN_H__ +#define __LINKD_DATAPLANE_PLUGIN_H__ + +#include <sys/types.h> +#include <time.h> + +#include "finwo/resp.h" + +struct dp_plugin { + char *target; + int optional; + + int fd; // socketpair to the child, or the tcp socket + pid_t pid; // 0 when connected over tcp + char *user; // tcp:// credentials, if any + char *pass; + + int up; + int hardware; // hardware_detected from INFO + + char **commands; // verbs from COMMAND, upper-cased + size_t commands_n; + char **config_keys; // directives from CONFIG LIST + size_t config_keys_n; + + int backoff_ms; // 0 until the first failure + struct timespec retry_at; + + struct dp_plugin *next; +}; + +// Send argv as a RESP array and read one reply. NULL on transport failure, +// which also marks the plugin down. Caller owns the result. +resp_object *dp_plugin_call(struct dp_plugin *p, const char **argv, int argc); + +// True if COMMAND advertised this verb. +int dp_plugin_supports(const struct dp_plugin *p, const char *verb); + +// True if CONFIG LIST advertised this directive. +int dp_plugin_config_supports(const struct dp_plugin *p, const char *key); + +// Start, or reconnect after backoff. Returns 0 when usable. +int dp_plugin_up(struct dp_plugin *p); +void dp_plugin_down(struct dp_plugin *p, const char *why); + +// Plugin list, in declaration order. +struct dp_plugin *dp_plugin_list(void); + +// Look up or create the entry for a target, starting it if needed. Called +// during config parsing (to ask about directives) and by dp_start(). +struct dp_plugin *dp_plugin_for(const char *target); + +// CONFIG SET <key> <argv...>. Returns 0 on +OK. +int dp_plugin_config_set(struct dp_plugin *p, const char *key, char **argv, size_t argc); + +#endif // __LINKD_DATAPLANE_PLUGIN_H__ diff --git a/src/dataplane/registry.c b/src/dataplane/registry.c @@ -1,79 +0,0 @@ -#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 != LINKD_DATAPLANE_ABI) { - fprintf(stderr, "dataplane: refusing backend `%s`: abi %u, expected %u\n", - ops->name ? ops->name : "(unnamed)", ops->abi, LINKD_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 @@ -1,10 +0,0 @@ -#ifndef __LINKD_DATAPLANE_REGISTRY_H__ -#define __LINKD_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 // __LINKD_DATAPLANE_REGISTRY_H__ diff --git a/src/ipc.c b/src/ipc.c @@ -1,283 +1,96 @@ #define _GNU_SOURCE -#include <libgen.h> +#include <arpa/inet.h> #include <errno.h> +#include <libgen.h> #include <net/if.h> +#include <netdb.h> +#include <netinet/in.h> +#include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <sys/socket.h> #include <sys/stat.h> +#include <sys/types.h> #include <sys/un.h> #include <unistd.h> -#include <sys/types.h> +#include "finwo/resp.h" #include "rxi/log.h" -#include "util/config.h" #include "config/daemon.h" #include "config/ifaces.h" #include "config/ports.h" #include "ipc.h" #include "netlink/netlink.h" #include "netlink/rtnl.h" - -static int ipc_sock = -1; -static const char *ipc_path = NULL; - -int ipc_fd(void) { - return ipc_sock; -} - -int ipc_init(void) { - struct sockaddr_un addr; - - 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) { - log_error("ipc: socket failed: %s", strerror(errno)); - return -1; - } - - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - 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", ipc_path, strerror(errno)); - close(ipc_sock); - ipc_sock = -1; - return -1; - } - - 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(ipc_path); - return -1; - } - - log_info("ipc: listening on %s", ipc_path); +#include "util/auth.h" + +#define RBUF_MAX (1024 * 1024) + +struct listener { + int fd; + int is_unix; + char *path; // unix only, for unlink on shutdown +}; + +struct ipc_conn { + int fd; + int is_unix; + int role; + uid_t uid; + + char *rbuf; size_t rlen, rcap; + char *wbuf; size_t wlen, wcap, wsent; + + int close_after_write; + struct ipc_conn *next; +}; + +static struct listener listeners[IPC_MAX_LISTENERS]; +static int listener_n = 0; +static struct ipc_conn *conns = NULL; +static int conn_n = 0; + +static int wbuf_append(struct ipc_conn *c, const char *data, size_t len) { + if (c->wlen + len > c->wcap) { + size_t cap = c->wcap ? c->wcap : 1024; + while (cap < c->wlen + len) cap *= 2; + char *g = realloc(c->wbuf, cap); + if (!g) return -1; + c->wbuf = g; + c->wcap = cap; + } + memcpy(c->wbuf + c->wlen, data, len); + c->wlen += len; return 0; } -void ipc_fini(void) { - if (ipc_sock >= 0) { - close(ipc_sock); - ipc_sock = -1; +static void reply_obj(struct ipc_conn *c, resp_object *o) { + if (!o) return; + char *buf = NULL; + size_t len = 0; + if (resp_serialize(o, &buf, &len) == 0) { + wbuf_append(c, buf, len); + free(buf); } - unlink(ipc_path); + resp_free(o); } -// Helpers to send response -static void ipc_send_ok(FILE *out, const char *msg) { - if (msg) fprintf(out, "OK %s\n", msg); - else fprintf(out, "OK\n"); +static void reply_ok(struct ipc_conn *c) { + reply_obj(c, resp_simple_init("OK")); } -static void ipc_send_err(FILE *out, const char *msg) { - fprintf(out, "ERR %s\n", msg ? msg : "unknown error"); +static void reply_err(struct ipc_conn *c, const char *fmt, ...) { + char msg[512]; + va_list ap; + va_start(ap, fmt); + vsnprintf(msg, sizeof(msg), fmt, ap); + va_end(ap); + reply_obj(c, resp_error_init(msg)); } -// Forward declarations for handlers (implemented below, using netlink helpers) -static int handle_ifup(FILE *out, const char *ifname, uid_t uid); -static int handle_ifdown(FILE *out, const char *ifname, uid_t uid); -static int handle_ifreload(FILE *out, uid_t uid); -static int handle_ifquery(FILE *out, const char *ifname); - -// IPC namespace handlers -- these are registered with cfg_ns_get("ipc") -// They are invoked via cfg_parse on the connected socket's FILE* -static struct cnf_directive *cfg_handle_ifup(FILE *fd, struct cnf_directive *dir, void *user) { - FILE *out = (FILE*)user; - (void)fd; - log_info("ipc: cfg_handle_ifup argc=%d", dir->argc); - uid_t uid = 0; - extern uid_t ipc_current_uid; - uid = ipc_current_uid; - if (uid != 0) { - ipc_send_err(out, "permission denied: ifup requires root"); - cnf_directive_free(dir); - return NULL; - } - if (dir->argc != 1) { - ipc_send_err(out, "ifup expects 1 arg: <ifname>"); - cnf_directive_free(dir); - return NULL; - } - log_info("ipc: cfg_handle_ifup %s uid=%d", dir->argv[0], uid); - if (handle_ifup(out, dir->argv[0], uid) == 0) ipc_send_ok(out, NULL); - else log_info("ipc: handle_ifup %s failed", dir->argv[0]); - cnf_directive_free(dir); - return NULL; -} - -static struct cnf_directive *cfg_handle_ifdown(FILE *fd, struct cnf_directive *dir, void *user) { - FILE *out = (FILE*)user; - (void)fd; - extern uid_t ipc_current_uid; - uid_t uid = ipc_current_uid; - if (uid != 0) { - ipc_send_err(out, "permission denied: ifdown requires root"); - cnf_directive_free(dir); - return NULL; - } - if (dir->argc != 1) { - ipc_send_err(out, "ifdown expects 1 arg: <ifname>"); - cnf_directive_free(dir); - return NULL; - } - if (handle_ifdown(out, dir->argv[0], uid) == 0) ipc_send_ok(out, NULL); - cnf_directive_free(dir); - return NULL; -} - -static struct cnf_directive *cfg_handle_ifreload(FILE *fd, struct cnf_directive *dir, void *user) { - FILE *out = (FILE*)user; - (void)fd; - extern uid_t ipc_current_uid; - uid_t uid = ipc_current_uid; - if (uid != 0) { - ipc_send_err(out, "permission denied: ifreload requires root"); - cnf_directive_free(dir); - return NULL; - } - if (dir->argc != 0) { - ipc_send_err(out, "ifreload expects no args"); - cnf_directive_free(dir); - return NULL; - } - if (handle_ifreload(out, uid) == 0) ipc_send_ok(out, NULL); - cnf_directive_free(dir); - return NULL; -} - -static struct cnf_directive *cfg_handle_ifquery(FILE *fd, struct cnf_directive *dir, void *user) { - FILE *out = (FILE*)user; - (void)fd; - log_info("ipc: ifquery argc=%d", dir->argc); - const char *ifname = NULL; - if (dir->argc > 1) { - ipc_send_err(out, "ifquery expects 0 or 1 arg"); - cnf_directive_free(dir); - return NULL; - } - if (dir->argc == 1) ifname = dir->argv[0]; - log_info("ipc: ifquery %s", ifname ? ifname : "(all)"); - handle_ifquery(out, ifname); - cnf_directive_free(dir); - return NULL; -} - -// Global for current uid (single-threaded, one client at a time) -uid_t ipc_current_uid = 0; - -int ipc_handle(void) { - int cfd; - struct sockaddr_un addr; - socklen_t alen = sizeof(addr); - struct ucred cred; - socklen_t clen = sizeof(cred); - FILE *fin = NULL; - FILE *fout = NULL; - int fd_dup; - - // The LISTENING socket is non-blocking (it lives in the nl_run poll set), but - // the ACCEPTED connection must be blocking: the client connects and writes its - // command as two separate syscalls, so a non-blocking read here returns EAGAIN - // whenever the bytes have not landed yet. stdio maps that to EOF, cfg_parse - // sees an empty stream, no handler runs and the client gets an empty reply. - // That is a race with no upper bound in wall-clock terms - it is not fixable - // by sleeping in the caller, only by blocking here. - cfd = accept4(ipc_sock, (struct sockaddr*)&addr, &alen, SOCK_CLOEXEC); - if (cfd < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) return 0; - log_error("ipc: accept failed: %s", strerror(errno)); - return -1; - } - - // Blocking, but never indefinitely: 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. - { - struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; - setsockopt(cfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - setsockopt(cfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); - } - - // Get peer credentials - if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &clen) == 0) { - ipc_current_uid = cred.uid; - } else { - ipc_current_uid = 99999; - } - - // Use fdopen for reading and writing -- need separate FILE* for read and write to handle duplex? - // We'll use one FILE* for reading commands, and the fd directly for writing responses. - // Simpler: use fdopen for reading, and use fdopen dup for writing. - fd_dup = dup(cfd); - if (fd_dup < 0) { - close(cfd); - return -1; - } - fin = fdopen(cfd, "r"); - fout = fdopen(fd_dup, "w"); - if (!fin || !fout) { - if (fin) fclose(fin); else close(cfd); - if (fout) fclose(fout); else close(fd_dup); - return -1; - } - - // Set line buffering - setvbuf(fout, NULL, _IONBF, 0); - - // Parse one or more commands from this connection (each line is a command) - // Use cfg_parse on the ipc namespace - struct cfg_ns *ns = cfg_ns_get("ipc"); - // cfg_parse will read directives from fin and dispatch to handlers, which write to fout - // It handles multiple lines until EOF. - cfg_parse(ns, NULL, fin, fout); - - // Ensure OK is sent if handler didn't already? Our handlers send OK/ERR themselves. - // Flush and close - fflush(fout); - fclose(fin); // also closes cfd - fclose(fout); - ipc_current_uid = 0; - return 0; -} - -// Register IPC commands -void ipc_register(void) { - struct cfg_ns *ns = cfg_ns_get("ipc"); - cfg_register_directive(ns, "ifup", cfg_handle_ifup); - cfg_register_directive(ns, "ifdown", cfg_handle_ifdown); - cfg_register_directive(ns, "ifreload", cfg_handle_ifreload); - cfg_register_directive(ns, "ifquery", cfg_handle_ifquery); -} - -// Stubs for netlink helpers (to be implemented in netlink.c or here) -// For now, provide minimal implementations that just log and return OK -// The real implementation will be wired in the next commit (Wire netlink apply) - -#include <sys/wait.h> - static int run_hook(const char *cmd) { if (!cmd || !*cmd) return 0; log_info("ipc: running hook: %s", cmd); @@ -289,30 +102,29 @@ static int run_hook(const char *cmd) { return 0; } -static int handle_ifup(FILE *out, const char *ifname, uid_t uid) { - (void)uid; +static int do_ifup(struct ipc_conn *c, const char *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) { - fprintf(out, "ERR ifup %s failed\n", ifname); + reply_err(c, "ifup %s failed", ifname); return -1; } return 0; } log_info("ipc: ifup %s", ifname); if (iface->pre_up && run_hook(iface->pre_up) != 0) { - fprintf(out, "ERR pre-up hook failed for %s\n", ifname); + reply_err(c, "pre-up hook failed for %s", ifname); return -1; } - // VLAN creation first (needs rawdev) + if (iface->vlan_id >= 0) { const char *raw = iface->vlan_raw_device; char tmp[IF_NAMESIZE]; if (!raw) { char *dot = strrchr(iface->name, '.'); if (dot) { - size_t len = dot - iface->name; + size_t len = (size_t)(dot - iface->name); if (len < sizeof(tmp)) { memcpy(tmp, iface->name, len); tmp[len] = '\0'; @@ -325,16 +137,16 @@ static int handle_ifup(FILE *out, const char *ifname, uid_t uid) { rtnl_vlan_create(ifname, raw, iface->vlan_id); } } - // VRF device: create before anything else touches it. `ifup <vrf>` on its - // own must work, not only a full apply. + + // `ifup <vrf>` alone must work, not only a full apply. if (iface_is_vrf(iface)) { log_info("ipc: ifup %s vrf table %d", ifname, iface->vrf_table); rtnl_vrf_create(ifname, (uint32_t)iface->vrf_table); } - // Bridge creation -- classification shared with apply_interfaces, see - // config/ifaces.c:iface_is_bridge(). + if (iface_is_bridge(iface)) { - log_info("ipc: ifup %s bridge (ports %s)", ifname, iface->bridge_ports ? iface->bridge_ports : "(none)"); + log_info("ipc: ifup %s bridge (ports %s)", ifname, + iface->bridge_ports ? iface->bridge_ports : "(none)"); rtnl_bridge_create(ifname); if (iface->bridge_stp) { bool on = !strcasecmp(iface->bridge_stp, "on") || !strcasecmp(iface->bridge_stp, "yes"); @@ -356,18 +168,15 @@ static int handle_ifup(FILE *out, const char *ifname, uid_t uid) { free(ports); } } - // Bring link up - // Enslave before the link comes up and addresses land: moving an interface - // into a VRF flushes its addresses, so doing it afterwards would silently - // discard what we just configured. + + // Enslave before addresses land: entering a VRF flushes them. if (iface->vrf_master) { rtnl_vrf_add_port(iface->vrf_master, ifname); } if (rtnl_link_up(ifname) != 0) { - fprintf(out, "ERR ifup %s: link up failed\n", ifname); + reply_err(c, "ifup %s: link up failed", ifname); return -1; } - // Addresses for (struct iface_addr *a = iface->addrs; a; a = a->next) { log_info("ipc: ifup %s addr %s", ifname, a->address); rtnl_addr_add(ifname, a->address, a->netmask); @@ -376,48 +185,42 @@ static int handle_ifup(FILE *out, const char *ifname, uid_t uid) { log_info("ipc: ifup %s gateway %s", ifname, iface->gateway); rtnl_route_add_default(iface->gateway, ifname); } - if (iface->mtu) { - if (rtnl_link_set_mtu(ifname, iface->mtu) != 0) { - fprintf(out, "ERR ifup %s: mtu failed\n", ifname); - return -1; - } + if (iface->mtu && rtnl_link_set_mtu(ifname, iface->mtu) != 0) { + reply_err(c, "ifup %s: mtu failed", ifname); + return -1; } - if (iface->hwaddress) { - if (rtnl_link_set_hwaddr(ifname, iface->hwaddress) != 0) { - fprintf(out, "ERR ifup %s: hwaddress failed\n", ifname); - return -1; - } + if (iface->hwaddress && rtnl_link_set_hwaddr(ifname, iface->hwaddress) != 0) { + reply_err(c, "ifup %s: hwaddress failed", ifname); + return -1; } if (iface->post_up && run_hook(iface->post_up) != 0) { - fprintf(out, "ERR post-up hook failed for %s\n", ifname); + reply_err(c, "post-up hook failed for %s", ifname); return -1; } return 0; } -static int handle_ifdown(FILE *out, const char *ifname, uid_t uid) { - (void)uid; +static int do_ifdown(struct ipc_conn *c, const char *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) { - fprintf(out, "ERR ifdown %s failed\n", ifname); + reply_err(c, "ifdown %s failed", ifname); return -1; } return 0; } log_info("ipc: ifdown %s", ifname); if (iface->pre_down && run_hook(iface->pre_down) != 0) { - fprintf(out, "ERR pre-down hook failed for %s\n", ifname); + reply_err(c, "pre-down hook failed for %s", ifname); return -1; } if (rtnl_link_down(ifname) != 0) { - fprintf(out, "ERR ifdown %s failed\n", ifname); + reply_err(c, "ifdown %s failed", ifname); return -1; } - // Flush addresses (keep for now) if (iface->post_down && run_hook(iface->post_down) != 0) { - fprintf(out, "ERR post-down hook failed for %s\n", ifname); + reply_err(c, "post-down hook failed for %s", ifname); return -1; } return 0; @@ -429,82 +232,503 @@ static int count_ifaces(void) { return n; } -static int handle_ifreload(FILE *out, uid_t uid) { - (void)uid; - 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 +static int do_ifreload(struct ipc_conn *c) { + log_info("ipc: ifreload start, old list has %d", count_ifaces()); + // TODO: real diff-apply; this frees and re-parses. ifaces_free(); ports_free(); - 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"); + reply_err(c, "ifreload: failed to reload interface configuration"); return -1; } if (load_namespace("ports", dcfg->ports, dcfg->ports_n) < 0) { - ipc_send_err(out, "ifreload: failed to reload port configuration"); + reply_err(c, "ifreload: failed to reload port configuration"); return -1; } - { - int n = 0; - for (struct linkd_iface *c = ifaces_list(); c; c = c->next) n++; - log_info("ipc: ifreload done, new list has %d", n); - for (struct linkd_iface *c = ifaces_list(); c; c = c->next) { - log_info("ipc: ifreload iface %s", c->name); + log_info("ipc: ifreload done, new list has %d", count_ifaces()); + return 0; +} + +// Flat key/value pairs (RESP2 map). Keys may repeat: several addresses. +static void iface_to_map(resp_object *m, const struct linkd_iface *i) { + char num[16]; + resp_array_append_bulk(m, "name"); + resp_array_append_bulk(m, i->name); + if (i->method) { + resp_array_append_bulk(m, "method"); + resp_array_append_bulk(m, i->method); + } + resp_array_append_bulk(m, "auto"); + resp_array_append_bulk(m, i->auto_flag ? "yes" : "no"); + for (struct iface_addr *a = i->addrs; a; a = a->next) { + resp_array_append_bulk(m, "address"); + resp_array_append_bulk(m, a->address); + if (a->netmask) { + resp_array_append_bulk(m, "netmask"); + resp_array_append_bulk(m, a->netmask); } } - return 0; + if (i->gateway) { resp_array_append_bulk(m, "gateway"); resp_array_append_bulk(m, i->gateway); } + if (i->mtu) { snprintf(num, sizeof(num), "%d", i->mtu); + resp_array_append_bulk(m, "mtu"); resp_array_append_bulk(m, num); } + if (i->hwaddress) { resp_array_append_bulk(m, "hwaddress"); resp_array_append_bulk(m, i->hwaddress); } + if (i->vlan_raw_device) { resp_array_append_bulk(m, "vlan-raw-device"); resp_array_append_bulk(m, i->vlan_raw_device); } + if (i->vlan_id >= 0) { snprintf(num, sizeof(num), "%d", i->vlan_id); + resp_array_append_bulk(m, "vlan-id"); resp_array_append_bulk(m, num); } + if (i->bridge_ports) { resp_array_append_bulk(m, "bridge-ports"); resp_array_append_bulk(m, i->bridge_ports); } + if (i->bridge_stp) { resp_array_append_bulk(m, "bridge-stp"); resp_array_append_bulk(m, i->bridge_stp); } + if (i->bridge_vlan_aware >= 0) { + resp_array_append_bulk(m, "bridge-vlan-aware"); + resp_array_append_bulk(m, i->bridge_vlan_aware ? "yes" : "no"); + } + if (i->vrf_table >= 0) { snprintf(num, sizeof(num), "%d", i->vrf_table); + resp_array_append_bulk(m, "vrf-table"); resp_array_append_bulk(m, num); } + if (i->vrf_master) { resp_array_append_bulk(m, "vrf"); resp_array_append_bulk(m, i->vrf_master); } + if (i->pre_up) { resp_array_append_bulk(m, "pre-up"); resp_array_append_bulk(m, i->pre_up); } + if (i->post_up) { resp_array_append_bulk(m, "post-up"); resp_array_append_bulk(m, i->post_up); } + if (i->pre_down) { resp_array_append_bulk(m, "pre-down"); resp_array_append_bulk(m, i->pre_down); } + if (i->post_down) { resp_array_append_bulk(m, "post-down"); resp_array_append_bulk(m, i->post_down); } } -static int handle_ifquery(FILE *out, const char *ifname) { - 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); +static int do_ifquery(struct ipc_conn *c, const char *ifname) { if (ifname) { - cur = ifaces_find(ifname); - log_info("ipc: ifquery find %s -> %s", ifname, cur ? "found" : "not found"); - if (!cur) { - fprintf(out, "ERR no such interface: %s\n", ifname); + struct linkd_iface *i = ifaces_find(ifname); + if (!i) { + reply_err(c, "no such interface: %s", ifname); return -1; } - // Dump this iface in cnf style - fprintf(out, "iface %s\n", cur->name); - for (struct iface_addr *a = cur->addrs; a; a = a->next) { - fprintf(out, " address %s\n", a->address); - if (a->netmask) fprintf(out, " netmask %s\n", a->netmask); + resp_object *m = resp_array_init(); + iface_to_map(m, i); + reply_obj(c, m); + return 0; + } + + resp_object *outer = resp_array_init(); + for (struct linkd_iface *i = ifaces_list(); i; i = i->next) { + resp_object *m = resp_array_init(); + iface_to_map(m, i); + resp_array_append_obj(outer, m); + } + reply_obj(c, outer); + return 0; +} + +struct command { + const char *name; + int min_args; // including the verb + int max_args; // -1 = unlimited + int role; // minimum role +}; + +static const struct command commands[] = { + { "PING", 1, 2, IPC_ROLE_NONE }, + { "INFO", 1, 1, IPC_ROLE_READONLY }, + { "COMMAND", 1, 2, IPC_ROLE_READONLY }, + { "IFQUERY", 1, 2, IPC_ROLE_READONLY }, + { "IFUP", 2, 2, IPC_ROLE_FULL }, + { "IFDOWN", 2, 2, IPC_ROLE_FULL }, + { "IFRELOAD", 1, 1, IPC_ROLE_FULL }, + { "AUTH", 2, 3, IPC_ROLE_NONE }, + { "QUIT", 1, 1, IPC_ROLE_NONE }, +}; +static const size_t commands_n = sizeof(commands) / sizeof(commands[0]); + +static const char *arg(const resp_object *cmd, size_t i) { + if (!cmd || cmd->type != RESPT_ARRAY || i >= cmd->u.arr.n) return NULL; + const resp_object *e = &cmd->u.arr.elem[i]; + if (e->type != RESPT_BULK && e->type != RESPT_SIMPLE) return NULL; + return e->u.s; +} + +static void do_info(struct ipc_conn *c) { + char buf[512]; + snprintf(buf, sizeof(buf), + "# Server\r\n" + "name:linkd\r\n" + "version:" __TARGET "\r\n" + "\r\n" + "# Clients\r\n" + "connected_clients:%d\r\n" + "\r\n" + "# Config\r\n" + "interfaces:%d\r\n", + conn_n, count_ifaces()); + resp_object *o = calloc(1, sizeof(*o)); + if (!o) return; + o->type = RESPT_BULK; + o->u.s = strdup(buf); + reply_obj(c, o); +} + +static void do_command_list(struct ipc_conn *c) { + resp_object *a = resp_array_init(); + for (size_t i = 0; i < commands_n; i++) { + resp_object *e = resp_array_init(); + resp_array_append_bulk(e, commands[i].name); + resp_array_append_int(e, commands[i].max_args < 0 + ? -commands[i].min_args : commands[i].max_args); + resp_array_append_obj(a, e); + } + reply_obj(c, a); +} + +static void do_auth(struct ipc_conn *c, const resp_object *cmd) { + const struct linkd_daemon_cfg *cfg = daemon_cfg(); + if (!cfg->authfile) { + reply_err(c, "Client sent AUTH, but no password file is configured"); + return; + } + + const char *user = arg(cmd, 1); + const char *pass = arg(cmd, 2); + if (!pass) { pass = user; user = "default"; } + if (!user || !pass) { + reply_err(c, "wrong number of arguments for 'AUTH'"); + return; + } + + int role = auth_check(cfg->authfile, user, pass); + if (role == AUTH_ROLE_NONE) { + log_warn("ipc: failed AUTH for user '%s'", user); + reply_err(c, "WRONGPASS invalid username-password pair"); + return; + } + + c->role = (role == AUTH_ROLE_FULL) ? IPC_ROLE_FULL : IPC_ROLE_READONLY; + log_info("ipc: user '%s' authenticated (%s)", user, + c->role == IPC_ROLE_FULL ? "full" : "readonly"); + reply_ok(c); +} + +static void dispatch(struct ipc_conn *c, const resp_object *cmd) { + const char *verb = arg(cmd, 0); + if (!verb) { + reply_err(c, "ERR malformed command"); + return; + } + + size_t argc = (cmd->type == RESPT_ARRAY) ? cmd->u.arr.n : 0; + + const struct command *spec = NULL; + for (size_t i = 0; i < commands_n; i++) { + if (!strcasecmp(verb, commands[i].name)) { spec = &commands[i]; break; } + } + if (!spec) { + reply_err(c, "unknown command '%s'", verb); + return; + } + + if ((int)argc < spec->min_args || + (spec->max_args >= 0 && (int)argc > spec->max_args)) { + reply_err(c, "wrong number of arguments for '%s'", verb); + return; + } + + if (c->role < spec->role) { + reply_err(c, c->role == IPC_ROLE_NONE + ? "NOAUTH Authentication required" + : "NOPERM this command requires full access"); + return; + } + + if (!strcasecmp(verb, "PING")) { + const char *msg = arg(cmd, 1); + if (msg) { + resp_object *o = calloc(1, sizeof(*o)); + if (o) { o->type = RESPT_BULK; o->u.s = strdup(msg); reply_obj(c, o); } + } else { + reply_obj(c, resp_simple_init("PONG")); + } + } else if (!strcasecmp(verb, "QUIT")) { + reply_ok(c); + c->close_after_write = 1; + } else if (!strcasecmp(verb, "INFO")) { + do_info(c); + } else if (!strcasecmp(verb, "COMMAND")) { + do_command_list(c); + } else if (!strcasecmp(verb, "AUTH")) { + do_auth(c, cmd); + } else if (!strcasecmp(verb, "IFQUERY")) { + do_ifquery(c, arg(cmd, 1)); + } else if (!strcasecmp(verb, "IFUP")) { + if (do_ifup(c, arg(cmd, 1)) == 0) reply_ok(c); + } else if (!strcasecmp(verb, "IFDOWN")) { + if (do_ifdown(c, arg(cmd, 1)) == 0) reply_ok(c); + } else if (!strcasecmp(verb, "IFRELOAD")) { + if (do_ifreload(c) == 0) reply_ok(c); + } +} + +static void conn_close(struct ipc_conn *c) { + struct ipc_conn **p = &conns; + while (*p && *p != c) p = &(*p)->next; + if (*p) *p = c->next; + if (c->fd >= 0) close(c->fd); + free(c->rbuf); + free(c->wbuf); + free(c); + conn_n--; +} + +static void conn_accept(struct listener *l) { + int fd = accept4(l->fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC); + if (fd < 0) return; + + if (conn_n >= IPC_MAX_CONNS) { + log_warn("ipc: connection limit reached, rejecting"); + close(fd); + return; + } + + struct ipc_conn *c = calloc(1, sizeof(*c)); + if (!c) { close(fd); return; } + c->fd = fd; + c->is_unix = l->is_unix; + c->role = IPC_ROLE_NONE; + + // Root over unix is pre-authenticated; the kernel vouches for the uid. + if (l->is_unix) { + struct ucred cred; + socklen_t len = sizeof(cred); + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == 0) { + c->uid = cred.uid; + if (cred.uid == 0) c->role = IPC_ROLE_FULL; + } + } + + c->next = conns; + conns = c; + conn_n++; +} + +static void conn_read(struct ipc_conn *c) { + char buf[4096]; + for (;;) { + ssize_t n = read(c->fd, buf, sizeof(buf)); + if (n == 0) { conn_close(c); return; } + if (n < 0) { + if (errno == EINTR) continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) break; + conn_close(c); + return; + } + if (c->rlen + (size_t)n > RBUF_MAX) { + log_warn("ipc: request too large, dropping client"); + conn_close(c); + return; + } + if (c->rlen + (size_t)n > c->rcap) { + size_t cap = c->rcap ? c->rcap : 4096; + while (cap < c->rlen + (size_t)n) cap *= 2; + char *g = realloc(c->rbuf, cap); + if (!g) { conn_close(c); return; } + c->rbuf = g; + c->rcap = cap; + } + memcpy(c->rbuf + c->rlen, buf, (size_t)n); + c->rlen += (size_t)n; + if ((size_t)n < sizeof(buf)) break; + } + + // Drain every complete command; this is what makes pipelining work. + for (;;) { + resp_object *cmd = NULL; + int used = resp_read_buf(c->rbuf, c->rlen, &cmd); + if (used <= 0) { + // 0 = nothing yet, <0 = partial; wait for more data. + if (cmd) resp_free(cmd); + break; + } + dispatch(c, cmd); + resp_free(cmd); + + memmove(c->rbuf, c->rbuf + used, c->rlen - (size_t)used); + c->rlen -= (size_t)used; + if (c->rlen == 0) break; + } +} + +static void conn_write(struct ipc_conn *c) { + while (c->wsent < c->wlen) { + ssize_t n = write(c->fd, c->wbuf + c->wsent, c->wlen - c->wsent); + if (n < 0) { + if (errno == EINTR) continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) return; + conn_close(c); + return; + } + c->wsent += (size_t)n; + } + c->wlen = c->wsent = 0; + if (c->close_after_write) conn_close(c); +} + +static int listen_unix(const char *path) { + struct sockaddr_un addr; + + { + char *d = strdup(path); + if (d) { mkdir(dirname(d), 0755); free(d); } + } + unlink(path); + + int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd < 0) { + log_error("ipc: socket: %s", strerror(errno)); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + log_error("ipc: bind %s: %s", path, strerror(errno)); + close(fd); + return -1; + } + if (chmod(path, 0600) != 0) { + log_warn("ipc: chmod %s: %s", path, strerror(errno)); + } + if (listen(fd, 8) != 0) { + log_error("ipc: listen %s: %s", path, strerror(errno)); + close(fd); + unlink(path); + return -1; + } + return fd; +} + +static int listen_tcp(const char *hostport) { + char *copy = strdup(hostport); + if (!copy) return -1; + + char *host = copy; + char *port = strrchr(copy, ':'); + if (port) { *port++ = '\0'; } else { port = (char *)"6789"; } + if (*host == '\0') host = NULL; + + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_flags = AI_PASSIVE, + }; + struct addrinfo *res = NULL; + int gai = getaddrinfo(host, port, &hints, &res); + if (gai != 0) { + log_error("ipc: resolve %s: %s", hostport, gai_strerror(gai)); + free(copy); + return -1; + } + + int fd = -1; + for (struct addrinfo *ai = res; ai; ai = ai->ai_next) { + fd = socket(ai->ai_family, ai->ai_socktype | SOCK_CLOEXEC | SOCK_NONBLOCK, ai->ai_protocol); + if (fd < 0) continue; + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + if (bind(fd, ai->ai_addr, ai->ai_addrlen) == 0 && listen(fd, 8) == 0) break; + close(fd); + fd = -1; + } + freeaddrinfo(res); + free(copy); + + if (fd < 0) log_error("ipc: cannot listen on %s", hostport); + return fd; +} + +int ipc_init(void) { + const struct linkd_daemon_cfg *cfg = daemon_cfg(); + int ok = 0; + + for (size_t i = 0; i < cfg->listen_n && listener_n < IPC_MAX_LISTENERS; i++) { + const char *a = cfg->listen[i]; + int is_unix = !strncmp(a, "unix://", 7); + int fd; + + if (is_unix) { + fd = listen_unix(a + 7); + } else { + // No way to authorise a tcp client without one; SO_PEERCRED is + // unix-only. + if (!cfg->authfile) { + log_error("ipc: %s needs `authfile` configured; refusing to listen", a); + continue; + } + fd = listen_tcp(a + 6); + } + if (fd < 0) continue; + + listeners[listener_n].fd = fd; + listeners[listener_n].is_unix = is_unix; + listeners[listener_n].path = is_unix ? strdup(a + 7) : NULL; + listener_n++; + ok++; + log_info("ipc: listening on %s", a); + } + + return ok ? 0 : -1; +} + +void ipc_fini(void) { + while (conns) conn_close(conns); + for (int i = 0; i < listener_n; i++) { + if (listeners[i].fd >= 0) close(listeners[i].fd); + if (listeners[i].path) { + unlink(listeners[i].path); + free(listeners[i].path); } - if (cur->gateway) fprintf(out, " gateway %s\n", cur->gateway); - if (cur->mtu) fprintf(out, " mtu %d\n", cur->mtu); - if (cur->hwaddress) fprintf(out, " hwaddress %s\n", cur->hwaddress); - if (cur->vlan_raw_device) fprintf(out, " vlan-raw-device %s\n", cur->vlan_raw_device); - if (cur->vlan_id >= 0) fprintf(out, " vlan-id %d\n", cur->vlan_id); - if (cur->bridge_ports) fprintf(out, " bridge-ports %s\n", cur->bridge_ports); - if (cur->bridge_stp) fprintf(out, " bridge-stp %s\n", cur->bridge_stp); - if (cur->bridge_vlan_aware >= 0) - fprintf(out, " bridge-vlan-aware %s\n", cur->bridge_vlan_aware ? "yes" : "no"); - if (cur->vrf_table >= 0) fprintf(out, " vrf-table %d\n", cur->vrf_table); - if (cur->vrf_master) fprintf(out, " vrf %s\n", cur->vrf_master); - if (cur->pre_up) fprintf(out, " pre-up %s\n", cur->pre_up); - if (cur->post_up) fprintf(out, " post-up %s\n", cur->post_up); - if (cur->pre_down) fprintf(out, " pre-down %s\n", cur->pre_down); - if (cur->post_down) fprintf(out, " post-down %s\n", cur->post_down); - fprintf(out, "OK\n"); - } else { - for (cur = ifaces_list(); cur; cur = cur->next) { - fprintf(out, "iface %s", cur->name); - if (cur->method) fprintf(out, " %s", cur->method); - fprintf(out, "\n"); - for (struct iface_addr *a = cur->addrs; a; a = a->next) { - fprintf(out, " address %s\n", a->address); + } + listener_n = 0; +} + +int ipc_pollfds(struct pollfd *pfds, int max) { + int n = 0; + for (int i = 0; i < listener_n && n < max; i++) { + pfds[n].fd = listeners[i].fd; + pfds[n].events = POLLIN; + pfds[n].revents = 0; + n++; + } + for (struct ipc_conn *c = conns; c && n < max; c = c->next) { + pfds[n].fd = c->fd; + pfds[n].events = POLLIN | (c->wsent < c->wlen ? POLLOUT : 0); + pfds[n].revents = 0; + n++; + } + return n; +} + +void ipc_dispatch(struct pollfd *pfds, int n) { + for (int i = 0; i < n; i++) { + if (!pfds[i].revents) continue; + + int is_listener = 0; + for (int l = 0; l < listener_n; l++) { + if (listeners[l].fd == pfds[i].fd) { + conn_accept(&listeners[l]); + is_listener = 1; + break; } - if (cur->gateway) fprintf(out, " gateway %s\n", cur->gateway); } - fprintf(out, "OK\n"); + if (is_listener) continue; + + // Look up by fd: an earlier fd may have closed and freed this one. + struct ipc_conn *c = conns; + while (c && c->fd != pfds[i].fd) c = c->next; + if (!c) continue; + + if (pfds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) { conn_close(c); continue; } + if (pfds[i].revents & POLLIN) conn_read(c); + + // conn_read may have closed it; re-find before writing. + struct ipc_conn *still = conns; + while (still && still != c) still = still->next; + if (!still) continue; + + if (c->wsent < c->wlen) conn_write(c); } - return 0; } diff --git a/src/ipc.h b/src/ipc.h @@ -1,12 +1,27 @@ #ifndef __LINKD_IPC_H__ #define __LINKD_IPC_H__ +#include <poll.h> -// 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 +// RESP server. Speaks the Redis wire protocol over any number of unix and tcp +// listeners, as configured by `listen` in /etc/linkd.cnf. + +#define IPC_ROLE_NONE 0 // connected, not authenticated +#define IPC_ROLE_READONLY 1 // IFQUERY, INFO, COMMAND, PING +#define IPC_ROLE_FULL 2 // + IFUP, IFDOWN, IFRELOAD + +// Upper bound on fds this module contributes to the poll set. +#define IPC_MAX_LISTENERS 8 +#define IPC_MAX_CONNS 64 +#define IPC_MAX_POLLFDS (IPC_MAX_LISTENERS + IPC_MAX_CONNS) + +int ipc_init(void); void ipc_fini(void); -void ipc_register(void); + +// Fill `pfds` with listeners and live connections. Returns the count. +int ipc_pollfds(struct pollfd *pfds, int max); + +// Service whatever `ipc_pollfds` filled in, after poll() has run. +void ipc_dispatch(struct pollfd *pfds, int n); #endif // __LINKD_IPC_H__ diff --git a/src/main.c b/src/main.c @@ -7,45 +7,29 @@ #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]. +// from constructors in src/cli/. 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]); - // linkctl <command> [args...] - if (!strcmp(prog, "linkctl") && argc > 1) { - cli_main_fn fn = cli_find(argv[1]); - if (!fn) { - fprintf(stderr, "linkctl: unknown command: %s\n", argv[1]); - fprintf(stderr, "available: %s\n", AVAILABLE); - return 1; - } - return dispatch_shifted(fn, argc, argv); - } - cli_main_fn fn = cli_find(prog); if (fn) return fn(argc, argv); - // Unrecognised name (build tree, wrapper script): try argv[1]. + // Unrecognised name (build tree, wrapper script): try argv[1], shifting so + // the callee sees its own name in argv[0]. if (argc > 1 && (fn = cli_find(argv[1]))) { - return dispatch_shifted(fn, argc, 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; } fprintf(stderr, "%s: unknown command: %s\n", argv[0], prog); diff --git a/src/netlink/netlink.c b/src/netlink/netlink.c @@ -24,7 +24,7 @@ #include "rxi/log.h" #include "dataplane.h" -#include "dataplane/registry.h" +#include "dataplane/plugin.h" #include "filter.h" #include "netlink.h" @@ -175,7 +175,6 @@ static int handle_link(struct nlmsghdr *nh) { int master = 0; int is_vrf = 0; uint32_t vrf_table = 0; - const struct dp_ops *dp; int len; len = (int)(nh->nlmsg_len - NLMSG_LENGTH(sizeof(*ifi))); @@ -216,11 +215,8 @@ static int handle_link(struct nlmsghdr *nh) { vrf_master_remove(ifi->ifi_index); log_info("netlink: vrf %s removed", ifname); } - dp = dp_active(); - if (dp && dp->port_admin) { - if (dp->port_admin(ifname, false) != DP_RET_OK) { - log_error("netlink: port_admin %s down failed", ifname); - } + if (dp_port_admin(ifname, false) != DP_RET_OK) { + log_error("netlink: port_admin %s down failed", ifname); } return 0; } @@ -237,18 +233,12 @@ static int handle_link(struct nlmsghdr *nh) { } } - dp = dp_active(); - if (!dp) return 0; log_debug("netlink: link %s %s mtu %u", ifname, (ifi->ifi_flags & IFF_UP) ? "up" : "down", mtu); - if (dp->port_admin) { - if (dp->port_admin(ifname, (ifi->ifi_flags & IFF_UP) ? true : false) != DP_RET_OK) { - log_error("netlink: port_admin %s failed", ifname); - } + if (dp_port_admin(ifname, (ifi->ifi_flags & IFF_UP) ? true : false) != DP_RET_OK) { + log_error("netlink: port_admin %s failed", ifname); } - if (mtu && dp->port_mtu) { - if (dp->port_mtu(ifname, mtu) != DP_RET_OK) { - log_error("netlink: port_mtu %s %u failed", ifname, mtu); - } + if (mtu && dp_port_mtu(ifname, mtu) != DP_RET_OK) { + log_error("netlink: port_mtu %s %u failed", ifname, mtu); } return 0; } @@ -259,7 +249,6 @@ static int handle_addr(struct nlmsghdr *nh) { char ifname[IF_NAMESIZE] = ""; struct dp_rif rif; struct dp_addr addr; - const struct dp_ops *dp; void *bytes = NULL; size_t want = 0; int len; @@ -293,14 +282,12 @@ static int handle_addr(struct nlmsghdr *nh) { return 0; } - dp = dp_active(); - if (!dp) return 0; del = (nh->nlmsg_type == RTM_DELADDR); log_debug("netlink: rif %s %s table %u", ifname, del ? "del" : "add", rif.table); if (del) { - if (dp->rif_del && dp->rif_del(&rif) != DP_RET_OK) log_error("netlink: rif_del %s failed", ifname); + if (dp_rif_del(&rif) != DP_RET_OK) log_error("netlink: rif_del %s failed", ifname); } else { - if (dp->rif_add && dp->rif_add(&rif) != DP_RET_OK) log_error("netlink: rif_add %s failed", ifname); + if (dp_rif_add(&rif) != DP_RET_OK) log_error("netlink: rif_add %s failed", ifname); } return 0; } @@ -310,7 +297,6 @@ static int handle_neigh(struct nlmsghdr *nh) { struct rtattr *tb[NDA_MAX + 1]; char ifname[IF_NAMESIZE] = ""; struct dp_neigh neigh; - const struct dp_ops *dp; int len; int del; @@ -346,13 +332,11 @@ static int handle_neigh(struct nlmsghdr *nh) { return 0; } - dp = dp_active(); - if (!dp) return 0; log_debug("netlink: neigh %s %s table %u", ifname, del ? "del" : "add", neigh.table); if (del) { - if (dp->neigh_del && dp->neigh_del(&neigh) != DP_RET_OK) log_error("netlink: neigh_del failed"); + if (dp_neigh_del(&neigh) != DP_RET_OK) log_error("netlink: neigh_del failed"); } else { - if (dp->neigh_add && dp->neigh_add(&neigh) != DP_RET_OK) log_error("netlink: neigh_add failed"); + if (dp_neigh_add(&neigh) != DP_RET_OK) log_error("netlink: neigh_add failed"); } return 0; } @@ -363,7 +347,6 @@ static int handle_route(struct nlmsghdr *nh) { struct dp_route route; struct dp_nexthop nhops[NL_MAX_NH]; char nhnames[NL_MAX_NH][IF_NAMESIZE]; - const struct dp_ops *dp; uint32_t table; uint32_t metric = 0; int len; @@ -441,14 +424,12 @@ static int handle_route(struct nlmsghdr *nh) { route.nh = nh_count ? nhops : NULL; route.nh_count = nh_count; - dp = dp_active(); - if (!dp) return 0; del = (nh->nlmsg_type == RTM_DELROUTE); log_debug("netlink: route %s table %u nh %zu", del ? "del" : "add", table, nh_count); if (del) { - if (dp->route_del && dp->route_del(&route) != DP_RET_OK) log_error("netlink: route_del table %u failed", table); + if (dp_route_del(&route) != DP_RET_OK) log_error("netlink: route_del table %u failed", table); } else { - if (dp->route_add && dp->route_add(&route) != DP_RET_OK) log_error("netlink: route_add table %u failed", table); + if (dp_route_add(&route) != DP_RET_OK) log_error("netlink: route_add table %u failed", table); } return 0; } @@ -570,7 +551,6 @@ int nl_dump(int fd, uint16_t type, int family) { // Full converge: VRF map first (routes resolve against it), then RIFs, // neighbours, routes, then the backend's own resync backstop. int nl_resync(int fd) { - const struct dp_ops *dp; int fails = 0; nl_filter_reset(); @@ -584,9 +564,8 @@ int nl_resync(int fd) { if (nl_dump(fd, RTM_GETROUTE, AF_INET) != 0) fails++; if (nl_dump(fd, RTM_GETROUTE, AF_INET6) != 0) fails++; - dp = dp_active(); - if (dp && dp->resync && dp->resync() != DP_RET_OK) { - log_error("netlink: backend resync failed"); + if (dp_resync() != DP_RET_OK) { + log_error("netlink: plugin resync failed"); fails++; } if (fails) { @@ -607,7 +586,9 @@ int nl_run(int fd, int resync_interval_s) { clock_gettime(CLOCK_MONOTONIC, &last_resync); while (!nl_stop) { - struct pollfd pfds[2]; + // Netlink occupies slot 0; the IPC server contributes its listeners and + // every live connection after it. + struct pollfd pfds[1 + IPC_MAX_POLLFDS]; int nfds = 1; struct timespec now; long wait_ms = 30000; @@ -615,12 +596,8 @@ int nl_run(int fd, int resync_interval_s) { pfds[0].fd = fd; pfds[0].events = POLLIN; - int ipcfd = ipc_fd(); - if (ipcfd >= 0) { - pfds[1].fd = ipcfd; - pfds[1].events = POLLIN; - nfds = 2; - } + pfds[0].revents = 0; + nfds += ipc_pollfds(&pfds[1], IPC_MAX_POLLFDS); clock_gettime(CLOCK_MONOTONIC, &now); if (resync_interval_s > 0) { @@ -646,9 +623,7 @@ int nl_run(int fd, int resync_interval_s) { } if (!pr) continue; - if (nfds > 1 && (pfds[1].revents & POLLIN)) { - ipc_handle(); - } + if (nfds > 1) ipc_dispatch(&pfds[1], nfds - 1); if (!(pfds[0].revents & POLLIN)) continue; for (;;) { diff --git a/src/util/auth.c b/src/util/auth.c @@ -0,0 +1,187 @@ +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "finwo/pbkdf2.h" +#include "rxi/log.h" + +#include "util/auth.h" + +#define HASH_LEN 32 +#define SALT_LEN 16 + +static const char AB64[] = + "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +int ab64_encode(const unsigned char *in, size_t len, char *out, size_t out_sz) { + size_t need = (len * 8 + 5) / 6; + if (out_sz < need + 1) return -1; + + size_t o = 0; + for (size_t i = 0; i < len; i += 3) { + unsigned v = (unsigned)in[i] << 16; + if (i + 1 < len) v |= (unsigned)in[i + 1] << 8; + if (i + 2 < len) v |= (unsigned)in[i + 2]; + + out[o++] = AB64[(v >> 18) & 0x3f]; + out[o++] = AB64[(v >> 12) & 0x3f]; + if (i + 1 < len) out[o++] = AB64[(v >> 6) & 0x3f]; + if (i + 2 < len) out[o++] = AB64[v & 0x3f]; + } + out[o] = '\0'; + return 0; +} + +static int ab64_val(char c) { + const char *p = strchr(AB64, c); + return (p && c) ? (int)(p - AB64) : -1; +} + +size_t ab64_decode(const char *in, unsigned char *out, size_t out_sz) { + size_t len = strlen(in); + size_t o = 0; + + for (size_t i = 0; i < len; i += 4) { + int c0 = ab64_val(in[i]); + int c1 = (i + 1 < len) ? ab64_val(in[i + 1]) : -1; + int c2 = (i + 2 < len) ? ab64_val(in[i + 2]) : -1; + int c3 = (i + 3 < len) ? ab64_val(in[i + 3]) : -1; + if (c0 < 0 || c1 < 0) break; + + unsigned v = ((unsigned)c0 << 18) | ((unsigned)c1 << 12); + if (c2 >= 0) v |= (unsigned)c2 << 6; + if (c3 >= 0) v |= (unsigned)c3; + + if (o < out_sz) out[o++] = (unsigned char)((v >> 16) & 0xff); + if (c2 >= 0 && o < out_sz) out[o++] = (unsigned char)((v >> 8) & 0xff); + if (c3 >= 0 && o < out_sz) out[o++] = (unsigned char)(v & 0xff); + } + return o; +} + +// Comparison time must not depend on how much of the hash matched. +static int ct_equal(const unsigned char *a, const unsigned char *b, size_t n) { + unsigned char diff = 0; + for (size_t i = 0; i < n; i++) diff |= (unsigned char)(a[i] ^ b[i]); + return diff == 0; +} + +int auth_verify_hash(const char *stored, const char *pass) { + if (!stored || !pass) return 0; + + const char *prefix = "$pbkdf2-sha256$"; + if (strncmp(stored, prefix, strlen(prefix)) != 0) return 0; + + char *copy = strdup(stored + strlen(prefix)); + if (!copy) return 0; + + char *iter_s = copy; + char *salt_s = strchr(iter_s, '$'); + if (!salt_s) { free(copy); return 0; } + *salt_s++ = '\0'; + char *hash_s = strchr(salt_s, '$'); + if (!hash_s) { free(copy); return 0; } + *hash_s++ = '\0'; + + char *end = NULL; + unsigned long iterations = strtoul(iter_s, &end, 10); + if (!end || *end || iterations == 0) { free(copy); return 0; } + + unsigned char salt[64], want[HASH_LEN], got[HASH_LEN]; + size_t salt_len = ab64_decode(salt_s, salt, sizeof(salt)); + size_t want_len = ab64_decode(hash_s, want, sizeof(want)); + if (salt_len == 0 || want_len != HASH_LEN) { free(copy); return 0; } + + pbkdf2((const uint8_t *)pass, strlen(pass), salt, salt_len, + iterations, PBKDF2_SHA256, got, HASH_LEN); + + int ok = ct_equal(got, want, HASH_LEN); + free(copy); + return ok; +} + +char *auth_make_hash(const char *pass, unsigned iterations) { + if (!pass) return NULL; + if (!iterations) iterations = AUTH_DEFAULT_ITERATIONS; + + unsigned char salt[SALT_LEN]; + FILE *rnd = fopen("/dev/urandom", "rb"); + if (!rnd) return NULL; + size_t got = fread(salt, 1, sizeof(salt), rnd); + fclose(rnd); + if (got != sizeof(salt)) return NULL; + + unsigned char hash[HASH_LEN]; + pbkdf2((const uint8_t *)pass, strlen(pass), salt, sizeof(salt), + iterations, PBKDF2_SHA256, hash, sizeof(hash)); + + char salt_b[64], hash_b[64]; + if (ab64_encode(salt, sizeof(salt), salt_b, sizeof(salt_b)) != 0) return NULL; + if (ab64_encode(hash, sizeof(hash), hash_b, sizeof(hash_b)) != 0) return NULL; + + char *out = NULL; + if (asprintf(&out, "$pbkdf2-sha256$%u$%s$%s", iterations, salt_b, hash_b) < 0) { + return NULL; + } + return out; +} + +int auth_check_permissions(const char *path) { + struct stat st; + if (stat(path, &st) != 0) return -1; + + if (st.st_mode & S_IWOTH) { + log_error("auth: %s is world-writable; refusing to use it", path); + return -1; + } + if (st.st_mode & (S_IRGRP | S_IROTH)) { + log_warn("auth: %s is readable beyond its owner", path); + } + return 0; +} + +static int role_from_string(const char *s) { + if (!s || !*s) return AUTH_ROLE_READONLY; + if (!strcasecmp(s, "full")) return AUTH_ROLE_FULL; + if (!strcasecmp(s, "readonly")) return AUTH_ROLE_READONLY; + log_warn("auth: unknown role `%s', treating as readonly", s); + return AUTH_ROLE_READONLY; +} + +int auth_check(const char *path, const char *user, const char *pass) { + if (!path || !user || !pass) return AUTH_ROLE_NONE; + if (auth_check_permissions(path) != 0) return AUTH_ROLE_NONE; + + FILE *fd = fopen(path, "r"); + if (!fd) { + log_error("auth: cannot open %s", path); + return AUTH_ROLE_NONE; + } + + char line[1024]; + int role = AUTH_ROLE_NONE; + + while (fgets(line, sizeof(line), fd)) { + char *nl = strpbrk(line, "\r\n"); + if (nl) *nl = '\0'; + if (!*line || *line == '#') continue; + + char *hash = strchr(line, ':'); + if (!hash) continue; + *hash++ = '\0'; + if (strcmp(line, user) != 0) continue; + + char *role_s = strchr(hash, ':'); + if (role_s) *role_s++ = '\0'; + + if (auth_verify_hash(hash, pass)) role = role_from_string(role_s); + break; + } + + fclose(fd); + return role; +} diff --git a/src/util/auth.h b/src/util/auth.h @@ -0,0 +1,35 @@ +#ifndef __LINKD_UTIL_AUTH_H__ +#define __LINKD_UTIL_AUTH_H__ + +#include <stddef.h> + +// Password file, one entry per line: +// +// user:$pbkdf2-sha256$<iterations>$<salt>$<hash>:role +// +// role is `full` or `readonly`, and defaults to readonly when omitted. +// salt and hash use passlib's adapted base64 ("." for "+", no padding), so +// entries generated by passlib or Django interoperate. + +#define AUTH_ROLE_NONE 0 +#define AUTH_ROLE_READONLY 1 +#define AUTH_ROLE_FULL 2 + +#define AUTH_DEFAULT_ITERATIONS 29000 + +// Returns the role granted, or AUTH_ROLE_NONE on any failure. +int auth_check(const char *path, const char *user, const char *pass); + +// Verify one `$pbkdf2-sha256$...` string against a password. +int auth_verify_hash(const char *stored, const char *pass); + +// Build a hash string. Caller frees. NULL on failure. +char *auth_make_hash(const char *pass, unsigned iterations); + +// Refuses a world-writable file; warns when group- or world-readable. +int auth_check_permissions(const char *path); + +int ab64_encode(const unsigned char *in, size_t len, char *out, size_t out_sz); +size_t ab64_decode(const char *in, unsigned char *out, size_t out_sz); + +#endif // __LINKD_UTIL_AUTH_H__ diff --git a/target/common/.dep b/target/common/.dep @@ -1,3 +1,5 @@ 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 +finwo/pbkdf2 https://git.finwo.net/lib/pbkdf2.c/archives/heads/main.tar.gz +finwo/resp https://git.finwo.net/lib/resp.c/archives/heads/main.tar.gz rxi/log https://git.finwo.net/misc/dep-repository/archives/heads/pkg/rxi/log.tar.gz diff --git a/target/common/Makefile b/target/common/Makefile @@ -10,8 +10,10 @@ SRC+=$(wildcard src/*/*/*.c) CFLAGS?=-Wall -Wextra -O2 LDFLAGS?= -# Dataplane plugins are dlopen'd at runtime -LDFLAGS+=-ldl +# Drop unreferenced code: dependencies export whole libraries, of which we use +# a part (pbkdf2 also ships SHA-1). +CFLAGS +=-ffunction-sections -fdata-sections +LDFLAGS+=-Wl,--gc-sections INCLUDES:= INCLUDES+=-I src diff --git a/tests/fixtures/plugin-echo.sh b/tests/fixtures/plugin-echo.sh @@ -0,0 +1,77 @@ +#!/bin/sh +# A linkd plugin in POSIX shell: RESP over stdin/stdout. +# +# Exists to prove a plugin needs no C, no library and no linking -- and to give +# the test suite something to drive. It logs what it receives to +# $PLUGIN_LOG so tests can assert on the commands linkd actually sent. +# +# Only the subset of RESP linkd sends is parsed: arrays of bulk strings. + +LOG="${PLUGIN_LOG:-/dev/null}" +HARDWARE="${PLUGIN_HARDWARE:-1}" +FAIL_VERB="${PLUGIN_FAIL_VERB:-}" + +# RESP replies. printf with \r\n; the protocol is CRLF-delimited. +ok() { printf '+OK\r\n'; } +err() { printf -- '-ERR %s\r\n' "$1"; } +bulk() { printf '$%s\r\n%s\r\n' "${#1}" "$1"; } + +array() { + printf '*%s\r\n' "$#" + for a in "$@"; do printf '$%s\r\n%s\r\n' "${#a}" "$a"; done +} + +# Read one RESP array into $ARGV (newline-separated). Returns 1 at EOF. +read_command() { + ARGV='' + read -r line || return 1 + line=${line%$(printf '\r')} + case "$line" in + '*'*) count=${line#\*} ;; + *) return 1 ;; + esac + + i=0 + while [ "$i" -lt "$count" ]; do + read -r hdr || return 1 # $<len> + read -r val || return 1 # payload + val=${val%$(printf '\r')} + ARGV="${ARGV}${val} +" + i=$((i + 1)) + done + return 0 +} + +while read_command; do + verb=$(printf '%s' "$ARGV" | sed -n '1p' | tr '[:lower:]' '[:upper:]') + sub=$(printf '%s' "$ARGV" | sed -n '2p' | tr '[:lower:]' '[:upper:]') + printf '%s' "$ARGV" | tr '\n' ' ' | sed 's/ *$//' >> "$LOG" + printf '\n' >> "$LOG" + + if [ -n "$FAIL_VERB" ] && [ "$verb" = "$FAIL_VERB" ]; then + err "deliberate failure" + continue + fi + + case "$verb" in + COMMAND) array PORT RIF NEIGH ROUTE RESYNC CONFIG INFO ;; + INFO) bulk "# Server +name:echo +version:0.1.0 + +# Stats +hardware_detected:${HARDWARE} +" ;; + CONFIG) + case "$sub" in + LIST) array echo-setting echo-other ;; + SET) ok ;; + *) err "unknown CONFIG subcommand" ;; + esac + ;; + PORT|RIF|NEIGH|ROUTE|RESYNC) ok ;; + AUTH) ok ;; + *) err "unknown command $verb" ;; + esac +done diff --git a/tests/unit/test_auth.sh b/tests/unit/test_auth.sh @@ -0,0 +1,228 @@ +#!/bin/sh +# Password hashing: PBKDF2 correctness against OpenSSL, adapted-base64 +# round-trips, and the role model end to end. +set -eu +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "${HERE}/../.." && pwd) +. "${HERE}/../helpers.sh" + +echo "==> test_auth: password hashing and roles" + +BUILD_DIR=$(ensure_built "${ROOT}") +BIN="${BUILD_DIR}/linkd" + +T=$(mktemp -d) +trap 'rm -rf "${T}"' EXIT + +DEPINC="${BUILD_DIR}/lib/.dep/include" + +# Harness exposing the internals, so PBKDF2 output can be compared against a +# reference implementation rather than only against itself. +cat > "${T}/h.c" <<'EOF' +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include "finwo/pbkdf2.h" +#include "util/auth.h" + +static void hex(const unsigned char *b, size_t n) { + for (size_t i = 0; i < n; i++) printf("%02x", b[i]); + printf("\n"); +} + +int main(int argc, char **argv) { + if (argc > 1 && !strcmp(argv[1], "kdf")) { + /* kdf <pass> <salt-ascii> <iters> -> hex of 32-byte key */ + unsigned char out[32]; + pbkdf2((const uint8_t*)argv[2], strlen(argv[2]), + (const uint8_t*)argv[3], strlen(argv[3]), + (unsigned long)atoi(argv[4]), PBKDF2_SHA256, out, sizeof(out)); + hex(out, sizeof(out)); + return 0; + } + if (argc > 1 && !strcmp(argv[1], "ab64")) { + /* ab64 <ascii> -> encoded, then decoded back as hex */ + char enc[256]; + unsigned char dec[256]; + ab64_encode((const unsigned char*)argv[2], strlen(argv[2]), enc, sizeof(enc)); + size_t n = ab64_decode(enc, dec, sizeof(dec)); + printf("%s\n", enc); + hex(dec, n); + return 0; + } + if (argc > 1 && !strcmp(argv[1], "verify")) { + printf("%d\n", auth_verify_hash(argv[2], argv[3])); + return 0; + } + if (argc > 1 && !strcmp(argv[1], "make")) { + char *h = auth_make_hash(argv[2], (unsigned)atoi(argv[3])); + printf("%s\n", h ? h : "(null)"); + return 0; + } + return 1; +} +EOF + +cc -O2 -I"${ROOT}/src" -I"${DEPINC}" -o "${T}/h" "${T}/h.c" \ + "${ROOT}/src/util/auth.c" \ + "${BUILD_DIR}/lib/finwo/pbkdf2/src/pbkdf2.o" \ + "${BUILD_DIR}/lib/rxi/log/src/log.o" 2>"${T}/cc.log" || { + echo "SKIP: harness did not build"; sed -n '1,5p' "${T}/cc.log"; exit 0; +} + +# --- PBKDF2 against OpenSSL ------------------------------------------------ +# Self-consistency proves nothing; a wrong HMAC is perfectly self-consistent. +if command -v openssl >/dev/null 2>&1; then + for vec in "password:salt:1000" "secret:NaCl:4096" "x:0123456789abcdef:29000"; do + pass=$(printf '%s' "${vec}" | cut -d: -f1) + salt=$(printf '%s' "${vec}" | cut -d: -f2) + iter=$(printf '%s' "${vec}" | cut -d: -f3) + + mine=$("${T}/h" kdf "${pass}" "${salt}" "${iter}") + ref=$(openssl kdf -keylen 32 -kdfopt digest:SHA256 \ + -kdfopt "pass:${pass}" -kdfopt "salt:${salt}" -kdfopt "iter:${iter}" \ + PBKDF2 2>/dev/null | tr -d ':' | tr 'A-F' 'a-f') + if [ -z "${ref}" ]; then + echo "SKIP: openssl kdf unavailable for cross-check" + break + fi + assert_eq "${mine}" "${ref}" "PBKDF2-SHA256 matches OpenSSL (${pass}/${salt}/${iter})" + done +else + echo "SKIP: openssl not available" +fi + +# --- adapted base64 -------------------------------------------------------- +out=$("${T}/h" ab64 "hello world") +enc=$(printf '%s' "${out}" | sed -n 1p) +dec=$(printf '%s' "${out}" | sed -n 2p) +want=$(printf 'hello world' | od -An -tx1 | tr -d ' \n') +assert_eq "${dec}" "${want}" "adapted base64 round-trips" + +case "${enc}" in + *=*) echo "FAIL: adapted base64 must not emit padding" >&2; exit 1 ;; + *+*) echo "FAIL: adapted base64 must use '.' not '+'" >&2; exit 1 ;; + *) echo "PASS: adapted base64 has no padding and no '+'" ;; +esac + +# --- hash round-trip ------------------------------------------------------- +h=$("${T}/h" make "correct horse" 1000) +case "${h}" in + '$pbkdf2-sha256$1000$'*) echo "PASS: hash string carries scheme and iterations" ;; + *) echo "FAIL: unexpected hash format: ${h}" >&2; exit 1 ;; +esac + +assert_eq "$("${T}/h" verify "${h}" "correct horse")" "1" "correct password verifies" +assert_eq "$("${T}/h" verify "${h}" "wrong")" "0" "wrong password rejected" +assert_eq "$("${T}/h" verify "${h}" "")" "0" "empty password rejected" + +# Two hashes of the same password must differ: the salt has to be random. +h2=$("${T}/h" make "correct horse" 1000) +if [ "${h}" = "${h2}" ]; then + echo "FAIL: identical hashes for the same password; salt is not random" >&2 + exit 1 +fi +echo "PASS: salt is random across invocations" + +# Tampering with the stored iteration count must not validate. +assert_eq "$("${T}/h" verify "\$pbkdf2-sha256\$1\$abc\$def" "correct horse")" "0" \ + "malformed hash rejected" + +# --- roles over a live daemon ---------------------------------------------- +command -v unshare >/dev/null 2>&1 || { echo "SKIP: unshare unavailable"; exit 0; } +unshare -Urn true 2>/dev/null || { echo "SKIP: user namespaces unavailable"; exit 0; } + +mkdir -p "${T}/bin" +for l in linkd linkctl ifquery; do ln -sf "${BIN}" "${T}/bin/${l}"; done + +RO=$("${T}/h" make "ropass" 1000) +FU=$("${T}/h" make "fupass" 1000) +printf 'watcher:%s:readonly\nadmin:%s:full\nnorole:%s\n' "${RO}" "${FU}" "${RO}" > "${T}/passwd" +chmod 600 "${T}/passwd" + +cat > "${T}/interfaces" <<EOF +auto lo +iface lo + address 127.0.0.1/8 +EOF + +cat > "${T}/linkd.cnf" <<EOF +config_iface ${T}/interfaces +authfile ${T}/passwd +listen unix://${T}/linkd.sock +listen tcp://127.0.0.1:16797 +EOF + +cat > "${T}/raw.c" <<'EOF' +#include <stdio.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <sys/socket.h> +#include <unistd.h> +int main(int argc, char **argv) { + (void)argc; + char req[8192]; + size_t len = fread(req, 1, sizeof(req), stdin); + int fd = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in a; memset(&a, 0, sizeof(a)); + a.sin_family = AF_INET; a.sin_port = htons((uint16_t)atoi(argv[1])); + a.sin_addr.s_addr = inet_addr("127.0.0.1"); + if (connect(fd, (struct sockaddr*)&a, sizeof(a))) return 1; + write(fd, req, len); + shutdown(fd, SHUT_WR); + char b[8192]; ssize_t n, t = 0; + while ((n = read(fd, b+t, sizeof(b)-t-1)) > 0) t += n; + b[t] = 0; fwrite(b, 1, t, stdout); + return 0; +} +EOF +cc -O2 -o "${T}/raw" "${T}/raw.c" 2>/dev/null || { echo "SKIP: no raw client"; exit 0; } + +OUT="${T}/out" +timeout 60 unshare -Urn sh -c " + '${T}/bin/linkd' --config '${T}/linkd.cnf' --ready-file '${T}/ready' --resync-interval 0 >'${T}/daemon.log' 2>&1 & + for i in \$(seq 1 80); do [ -e '${T}/ready' ] && break; sleep 0.25; done + + echo '--badpass--' + printf '*3\r\n\$4\r\nAUTH\r\n\$5\r\nadmin\r\n\$5\r\nWRONG\r\n' | '${T}/raw' 16797 + + echo '--readonly-query--' + printf '*3\r\n\$4\r\nAUTH\r\n\$7\r\nwatcher\r\n\$6\r\nropass\r\n*2\r\n\$7\r\nIFQUERY\r\n\$2\r\nlo\r\n' | '${T}/raw' 16797 | head -3 + + echo '--readonly-mutate--' + printf '*3\r\n\$4\r\nAUTH\r\n\$7\r\nwatcher\r\n\$6\r\nropass\r\n*2\r\n\$4\r\nIFUP\r\n\$2\r\nlo\r\n' | '${T}/raw' 16797 | tail -1 + + echo '--full-mutate--' + printf '*3\r\n\$4\r\nAUTH\r\n\$5\r\nadmin\r\n\$6\r\nfupass\r\n*2\r\n\$4\r\nIFUP\r\n\$2\r\nlo\r\n' | '${T}/raw' 16797 | tail -1 + + echo '--norole-defaults-readonly--' + printf '*3\r\n\$4\r\nAUTH\r\n\$6\r\nnorole\r\n\$6\r\nropass\r\n*2\r\n\$4\r\nIFUP\r\n\$2\r\nlo\r\n' | '${T}/raw' 16797 | tail -1 + + echo '--end--' + kill %1 2>/dev/null +" > "${OUT}" 2>&1 || true + +section() { sed -n "/^--$1--\$/,/^--/p" "${OUT}" | sed '1d;$d'; } + +assert_contains "$(section badpass)" "WRONGPASS" "wrong password rejected by daemon" +assert_contains "$(section readonly-query)" "+OK" "readonly user authenticates" +assert_contains "$(section readonly-mutate)" "NOPERM" "readonly user cannot mutate" +assert_contains "$(section full-mutate)" "+OK" "full user can mutate" +assert_contains "$(section norole-defaults-readonly)" "NOPERM" \ + "entry without a role field defaults to readonly" + +# A world-writable password file lets anyone grant themselves access. +chmod 666 "${T}/passwd" +out=$(timeout 30 unshare -Urn sh -c " + '${T}/bin/linkd' --config '${T}/linkd.cnf' --ready-file '${T}/ready2' --resync-interval 0 >'${T}/d2.log' 2>&1 & + for i in \$(seq 1 60); do [ -e '${T}/ready2' ] && break; sleep 0.25; done + printf '*3\r\n\$4\r\nAUTH\r\n\$5\r\nadmin\r\n\$6\r\nfupass\r\n' | '${T}/raw' 16797 + kill %1 2>/dev/null +" 2>&1 || true) +assert_contains "${out}" "WRONGPASS" "world-writable password file is not honoured" +assert_file_contains "${T}/d2.log" "world-writable" "world-writable file is reported" + +echo "==> test_auth done" diff --git a/tests/unit/test_netlink.sh b/tests/unit/test_netlink.sh @@ -50,29 +50,45 @@ echo "PASS: no /sbin/ip reference in linkd sources" # 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. +# --- 2. accepted connections must be non-blocking ------------------------- +# This is the inverse of what it once asserted. Under the line protocol the +# server read a connection to EOF with stdio, so a non-blocking fd returned +# EAGAIN, stdio reported EOF, and the reply came back empty. +# +# RESP is length-prefixed, so a partial read is detectable rather than +# indistinguishable from end-of-input: resp_read_buf reports "incomplete" and +# the connection waits for more data. Blocking accepts would now let one +# client stall netlink for the whole daemon. 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 +if ! grep -q 'accept4(.*SOCK_NONBLOCK' "${IPC}"; then + echo "FAIL: accepted connections are blocking; one slow client stalls netlink" >&2 exit 1 fi -echo "PASS: accepted IPC connection is blocking" +echo "PASS: accepted connections are non-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" +# Non-blocking is only correct with incremental parsing behind it. +if ! grep -q 'resp_read_buf' "${IPC}"; then + echo "FAIL: no resp_read_buf; non-blocking reads need incremental parsing" >&2 + exit 1 +fi +echo "PASS: reads are buffered through resp_read_buf" + +# Partial writes must be buffered too, or a slow reader stalls the daemon +# just as effectively as a slow writer would. +if ! grep -q 'POLLOUT' "${IPC}"; then + echo "FAIL: no POLLOUT handling; a slow reader would block the daemon" >&2 + exit 1 +fi +echo "PASS: writes are buffered and drained on POLLOUT" + +# --- 3. one client must not be able to wedge the daemon ------------------- +# The old server serviced a single connection per poll wakeup, synchronously. +if grep -q 'IPC_MAX_CONNS' "${SRC}/ipc.h"; then + echo "PASS: server keeps a bounded connection table" +else + echo "FAIL: no connection limit; an unbounded fd table is a DoS" >&2 + exit 1 +fi echo "==> test_netlink done" diff --git a/tests/unit/test_plugin.sh b/tests/unit/test_plugin.sh @@ -0,0 +1,113 @@ +#!/bin/sh +# Plugin subsystem: handshake, config delivery, broadcast, failure semantics. +# +# Driven by tests/fixtures/plugin-echo.sh, a plugin written in POSIX shell. +# That is deliberate: if the suite passes with a shell plugin, the claim that +# plugins need no particular language is tested rather than asserted. +set -eu +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "${HERE}/../.." && pwd) +. "${HERE}/../helpers.sh" + +echo "==> test_plugin: RESP plugin subsystem" + +BUILD_DIR=$(ensure_built "${ROOT}") +BIN="${BUILD_DIR}/linkd" +FIXTURE="${ROOT}/tests/fixtures/plugin-echo.sh" +assert_path_exists "${FIXTURE}" "shell plugin fixture present" + +command -v unshare >/dev/null 2>&1 || { echo "SKIP: unshare unavailable"; exit 0; } +unshare -Urn true 2>/dev/null || { echo "SKIP: user namespaces unavailable"; exit 0; } + +T=$(mktemp -d) +trap 'rm -rf "${T}"' EXIT +mkdir -p "${T}/net" + +cat > "${T}/net/interfaces" <<EOF +auto lo +iface lo + address 127.0.0.1/8 +EOF + +cat > "${T}/net/ports" <<EOF +port swp1 + speed 25000 + fec rs + autoneg 0 +EOF + +# Runs linkd to completion against a config, returning its log. +run_linkd() { + # run_linkd <config> [env assignments...] + _cfg=$1; shift + env "$@" PLUGIN_LOG="${T}/plugin.log" \ + timeout 15 unshare -Urn "${BIN}" --config "${_cfg}" --resync-interval 0 \ + >"${T}/linkd.log" 2>&1 || true + cat "${T}/linkd.log" +} + +# --- handshake + config delivery ------------------------------------------- +cat > "${T}/full.cnf" <<EOF +config_iface ${T}/net/interfaces +config_ports ${T}/net/ports +listen unix://${T}/linkd.sock +plugin ${FIXTURE} + echo-setting value1 value2 +EOF + +: > "${T}/plugin.log" +run_linkd "${T}/full.cnf" >/dev/null + +assert_file_contains "${T}/plugin.log" "COMMAND" "plugin asked for COMMAND" +assert_file_contains "${T}/plugin.log" "INFO" "plugin asked for INFO" +assert_file_contains "${T}/plugin.log" "CONFIG LIST" "plugin asked for CONFIG LIST" +assert_file_contains "${T}/plugin.log" "CONFIG SET echo-setting value1 value2" \ + "unknown sub-directive delivered as CONFIG SET" + +# Operations reach the plugin in the documented subcommand form. +assert_file_contains "${T}/plugin.log" "PORT APPLY swp1 speed 25000 fec rs autoneg 0" \ + "ports config broadcast as PORT APPLY" +assert_file_contains "${T}/plugin.log" "PORT ADMIN lo up" "netlink link event broadcast" +assert_file_contains "${T}/plugin.log" "RIF ADD lo 127.0.0.1/8" "address event broadcast as RIF ADD" + +# --- hardware_detected gates the plugin ------------------------------------ +: > "${T}/plugin.log" +out=$(run_linkd "${T}/full.cnf" PLUGIN_HARDWARE=0) +assert_contains "${out}" "no supported hardware" "plugin reporting no hardware is refused" + +# --- failure semantics ------------------------------------------------------ +# A required plugin failing an operation fails that operation. +: > "${T}/plugin.log" +out=$(run_linkd "${T}/full.cnf" PLUGIN_FAIL_VERB=PORT) +assert_contains "${out}" "port_admin lo failed" "required plugin failure fails the operation" + +# The same plugin marked optional does not. +cat > "${T}/opt.cnf" <<EOF +config_iface ${T}/net/interfaces +listen unix://${T}/linkd.sock +plugin ${FIXTURE} + optional +EOF +: > "${T}/plugin.log" +out=$(run_linkd "${T}/opt.cnf" PLUGIN_FAIL_VERB=PORT) +case "${out}" in + *"port_admin lo failed"*) + echo "FAIL: optional plugin failure still failed the operation" >&2; exit 1 ;; + *"deliberate failure"*) + echo "PASS: optional plugin failure logged but not fatal" ;; + *) + echo "FAIL: optional plugin was not consulted at all" >&2; exit 1 ;; +esac + +# --- no plugins configured -------------------------------------------------- +cat > "${T}/none.cnf" <<EOF +config_iface ${T}/net/interfaces +listen unix://${T}/linkd.sock +EOF +out=$(run_linkd "${T}/none.cnf") +case "${out}" in + *"ERROR"*plugin*) echo "FAIL: plugin errors without any plugin configured" >&2; exit 1 ;; + *) echo "PASS: no plugins configured is not an error" ;; +esac + +echo "==> test_plugin done" diff --git a/tests/unit/test_resp.sh b/tests/unit/test_resp.sh @@ -0,0 +1,125 @@ +#!/bin/sh +# RESP server: protocol, pipelining, concurrency and the authorization model. +set -eu +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "${HERE}/../.." && pwd) +. "${HERE}/../helpers.sh" + +echo "==> test_resp: RESP server" + +BUILD_DIR=$(ensure_built "${ROOT}") +BIN="${BUILD_DIR}/linkd" + +command -v unshare >/dev/null 2>&1 || { echo "SKIP: unshare unavailable"; exit 0; } +unshare -Urn true 2>/dev/null || { echo "SKIP: user namespaces unavailable"; exit 0; } + +T=$(mktemp -d) +trap 'rm -rf "${T}"' EXIT +mkdir -p "${T}/bin" + +for l in linkd linkctl ifup ifdown ifquery ifreload; do ln -sf "${BIN}" "${T}/bin/${l}"; done + +cat > "${T}/interfaces" <<EOF +auto lo +iface lo + address 127.0.0.1/8 + address ::1/128 +EOF + +cat > "${T}/linkd.cnf" <<EOF +config_iface ${T}/interfaces +authfile ${T}/linkd.passwd +listen unix://${T}/linkd.sock +listen tcp://127.0.0.1:16793 +EOF +: > "${T}/linkd.passwd" + +# Request comes from stdin, not argv: shell quoting mangles CRLF escapes. +cat > "${T}/raw.c" <<'EOF' +#include <stdio.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/un.h> +#include <unistd.h> +int main(int argc, char **argv) { + (void)argc; + char req[65536]; + size_t len = fread(req, 1, sizeof(req), stdin); + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + struct sockaddr_un a; memset(&a, 0, sizeof(a)); a.sun_family = AF_UNIX; + strncpy(a.sun_path, argv[1], sizeof(a.sun_path)-1); + if (connect(fd, (struct sockaddr*)&a, sizeof(a))) return 1; + write(fd, req, len); + shutdown(fd, SHUT_WR); + char b[65536]; ssize_t n, t = 0; + while ((n = read(fd, b+t, sizeof(b)-t-1)) > 0) t += n; + b[t] = 0; fwrite(b, 1, t, stdout); + return 0; +} +EOF +cc -O2 -o "${T}/raw" "${T}/raw.c" 2>/dev/null || { echo "SKIP: no compiler for raw client"; exit 0; } + +OUT="${T}/out" +timeout 60 unshare -Urn sh -c " + '${T}/bin/linkd' --config '${T}/linkd.cnf' --ready-file '${T}/ready' --resync-interval 0 >'${T}/daemon.log' 2>&1 & + for i in \$(seq 1 80); do [ -e '${T}/ready' ] && break; sleep 0.25; done + export LINKD_CONFIG='${T}/linkd.cnf' + + echo '--ping--'; '${T}/bin/linkctl' PING + echo '--echo--'; '${T}/bin/linkctl' PING hello + echo '--query--'; '${T}/bin/ifquery' lo + echo '--unknown--'; '${T}/bin/linkctl' FROBNICATE 2>&1 || true + echo '--missing--'; '${T}/bin/ifquery' nosuch 2>&1 || true + echo '--arity--'; '${T}/bin/linkctl' IFUP 2>&1 || true + echo '--mutate--'; '${T}/bin/ifup' lo + + echo '--pipeline--' + printf '*1\r\n\$4\r\nPING\r\n*2\r\n\$4\r\nPING\r\n\$3\r\ntwo\r\n*1\r\n\$4\r\nPING\r\n' \ + | '${T}/raw' '${T}/linkd.sock' + + echo '--concurrent--' + pids='' + for i in \$(seq 1 8); do '${T}/bin/linkctl' PING & pids=\"\$pids \$!\"; done + for p in \$pids; do wait \$p; done + + echo '--tcp-ping--' + printf '*1\r\n\$4\r\nPING\r\n' | timeout 5 nc 127.0.0.1 16793 2>/dev/null | head -1 + echo '--tcp-noauth--' + printf '*2\r\n\$7\r\nIFQUERY\r\n\$2\r\nlo\r\n' | timeout 5 nc 127.0.0.1 16793 2>/dev/null | head -1 + echo '--end--' + + kill %1 2>/dev/null +" > "${OUT}" 2>&1 || true + +section() { sed -n "/^--$1--\$/,/^--/p" "${OUT}" | sed '1d;$d'; } + +assert_contains "$(section ping)" "PONG" "PING replies PONG" +assert_contains "$(section echo)" "hello" "PING <msg> echoes it" +assert_contains "$(section query)" "iface lo" "IFQUERY renders as config syntax" +assert_contains "$(section query)" "address 127.0.0.1/8" "IFQUERY returns addresses" +assert_contains "$(section query)" "address ::1/128" "IFQUERY returns IPv6 addresses" +assert_contains "$(section unknown)" "unknown command" "unknown command rejected" +assert_contains "$(section missing)" "no such interface" "missing interface rejected" +assert_contains "$(section arity)" "wrong number of arguments" "arity is enforced" +assert_contains "$(section mutate)" "OK" "root over unix may mutate" + +pipeline=$(section pipeline) +n=$(printf '%s' "${pipeline}" | grep -c 'PONG' || true) +assert_eq "${n}" "2" "pipelined PINGs answered (2 bare PONG + 1 echo)" +assert_contains "${pipeline}" "two" "pipelined echo answered in order" + +n=$(section concurrent | grep -c 'PONG' || true) +assert_eq "${n}" "8" "8 concurrent clients all served" + +assert_contains "$(section tcp-ping)" "PONG" "PING is allowed before AUTH" +assert_contains "$(section tcp-noauth)" "NOAUTH" "other commands require AUTH over tcp" + +cat > "${T}/noauth.cnf" <<EOF +config_iface ${T}/interfaces +listen unix://${T}/linkd2.sock +listen tcp://127.0.0.1:16794 +EOF +out=$(timeout 15 unshare -Urn "${BIN}" --config "${T}/noauth.cnf" 2>&1 || true) +assert_contains "${out}" "refusing to listen" "tcp without authfile is refused" + +echo "==> test_resp done"