commit 6d253db83e32a58928c3cf15a2daa1ef638e9a9b
parent 89f9c70090c7d471b0f03692003711e121d06e44
Author: finwo <finwo@pm.me>
Date: Sun, 13 Sep 2026 03:32:19 +0200
Add NL loop, unosd package stub, vfr filtering beginnings
Diffstat:
9 files changed, 997 insertions(+), 18 deletions(-)
diff --git a/plugins/bcm/src/dataplane.c b/plugins/bcm/src/dataplane.c
@@ -61,38 +61,40 @@ static int bcm_port_mtu(const char *ifname, uint32_t mtu) {
}
static int bcm_rif_add(const struct dp_rif *rif) {
- // TODO: bcm_l3_intf_create
+ // 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
+ // 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
+ // 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
+ // 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; for nh_count > 1 build a bcm_l3_egress_ecmp
- // group and refcount it across routes sharing the same nexthop set
+ // 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, release ECMP group when refcount hits zero
+ // TODO: bcm_l3_route_delete with vrf context from route->table, release ECMP
+ // group when refcount hits zero
(void)route;
return DP_RET_ERROR;
}
diff --git a/src/dataplane.h b/src/dataplane.h
@@ -8,8 +8,10 @@
#define DP_RET_ERROR -1
#define DP_RET_OK 0
-// Bumped whenever struct dp_ops changes shape. Plugins declaring a different
-// ABI are refused at load time rather than crashing us later.
+// 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 UNOS_DATAPLANE_ABI 1
// Where dlopen'd backends live. The openbcm package drops its backend here;
@@ -50,12 +52,14 @@ struct dp_port {
struct dp_rif {
const char *ifname;
struct dp_addr addr;
+ uint32_t table; // kernel route-table id; RT_TABLE_MAIN = default VRF
};
struct dp_neigh {
const char *ifname;
struct dp_addr addr;
uint8_t mac[6];
+ uint32_t table; // kernel route-table id; RT_TABLE_MAIN = default VRF
};
struct dp_nexthop {
@@ -67,7 +71,8 @@ struct dp_route {
struct dp_addr dst;
struct dp_nexthop *nh;
size_t nh_count; // >1 = ECMP group
- uint32_t metric;
+ uint32_t metric; // RTA_PRIORITY, 0 = unset
+ uint32_t table; // kernel route-table id; RT_TABLE_MAIN = default VRF
};
struct dp_ops {
diff --git a/src/dataplane/kernel.c b/src/dataplane/kernel.c
@@ -14,6 +14,9 @@
// Mirroring any of it back into the kernel would be circular. unosd on generic
// hardware is therefore close to inert by design: it parses configuration,
// validates it, and lets Linux do the work.
+//
+// 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>
diff --git a/src/main.c b/src/main.c
@@ -1,8 +1,11 @@
#define _GNU_SOURCE
+#include <signal.h>
+#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <strings.h>
#include <unistd.h>
#ifdef __cplusplus
@@ -10,10 +13,12 @@ extern "C" {
#endif
#include "cofyc/argparse.h"
+#include "rxi/log.h"
#include "config/ports.h"
#include "dataplane.h"
#include "dataplane/registry.h"
+#include "netlink/netlink.h"
#include "util/config.h"
static const char *const usage[] = {
@@ -22,6 +27,38 @@ static const char *const usage[] = {
NULL,
};
+static FILE *log_file;
+static char *log_path;
+static volatile sig_atomic_t sighup_received;
+
+static void logfile_callback(log_Event *ev) {
+ if (sighup_received) {
+ sighup_received = 0;
+ if (log_path && log_file) {
+ fclose(log_file);
+ log_file = fopen(log_path, "a");
+ }
+ }
+ if (log_file) {
+ char buf[64];
+ buf[strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", ev->time)] = '\0';
+ fprintf(log_file, "%s %-5s %s:%d: ", buf, log_level_string(ev->level), ev->file, ev->line);
+ vfprintf(log_file, ev->fmt, ev->ap);
+ fprintf(log_file, "\n");
+ fflush(log_file);
+ }
+}
+
+static void sighup_handler(int sig) {
+ (void)sig;
+ sighup_received = 1;
+}
+
+static void stop_handler(int sig) {
+ (void)sig;
+ nl_request_stop();
+}
+
// Port configuration lives in either:
// <dir>/ports.cnf a single file, or
// <dir>/ports.d/*.cnf a drop-in directory
@@ -54,7 +91,7 @@ static int load_ports(const char *dir) {
rc = cfg_parse(dir, fd, NULL);
if (rc < 0) {
- fprintf(stderr, "Error during reading port configuration from %s\n", dir);
+ log_error("Error during reading port configuration from %s", dir);
}
fclose(fd);
@@ -76,22 +113,58 @@ static int apply_ports(void) {
port.autoneg = cur->autoneg;
if (dp->port_apply(&port) != DP_RET_OK) {
- fprintf(stderr, "%s: failed to apply port configuration\n", cur->name);
+ log_warn("%s: failed to apply port configuration", cur->name);
}
}
return 0;
}
+// Atomic publish: write tmp + rename so frr never observes a half file.
+static int ready_publish(const char *path) {
+ char tmp[1024];
+ FILE *fd;
+
+ if (!path || !*path) return 0;
+ snprintf(tmp, sizeof(tmp), "%s.tmp.%d", path, (int)getpid());
+ fd = fopen(tmp, "w");
+ if (!fd) {
+ log_error("ready-file %s: %s", tmp, strerror(errno));
+ return -1;
+ }
+ fprintf(fd, "%d\n", (int)getpid());
+ fclose(fd);
+ if (rename(tmp, path) != 0) {
+ log_error("ready-file rename: %s", strerror(errno));
+ unlink(tmp);
+ return -1;
+ }
+ return 0;
+}
+
+static void ready_remove(const char *path) {
+ if (!path || !*path) return;
+ unlink(path);
+}
+
int main(int argc, const char **argv) {
char *config_dir = "/etc/unos";
char *dataplane = NULL;
- int rc = 0;
+ char *loglevel = "info";
+ char *logfile_path = NULL;
+ char *ready_file = "";
+ int resync_interval = 60;
+ int nl_fd = -1;
+ int rc = 0;
struct argparse_option options[] = {
OPT_HELP(),
OPT_STRING('c', "config", &config_dir, "Configuration directory", NULL, 0, 0),
OPT_STRING('d', "dataplane", &dataplane, "Force a dataplane backend", NULL, 0, 0),
+ OPT_STRING('v', "verbosity", &loglevel, "log verbosity: fatal,error,warn,info,debug,trace (default: info)", NULL, 0, 0),
+ OPT_STRING(0, "log", &logfile_path, "also write log to file (SIGHUP reopens for logrotate)", NULL, 0, 0),
+ OPT_INTEGER(0, "resync-interval", &resync_interval, "seconds between full resyncs (0 disables timer)", NULL, 0, 0),
+ OPT_STRING(0, "ready-file", &ready_file, "readiness file to publish after initial resync (empty disables)", NULL, 0, 0),
OPT_END(),
};
@@ -107,10 +180,62 @@ int main(int argc, const char **argv) {
);
argc = argparse_parse(&argparse, argc, argv);
+ int level = LOG_INFO;
+ if (0) {
+ (void)0;
+ } else if (!strcasecmp(loglevel, "trace")) {
+ level = LOG_TRACE;
+ } else if (!strcasecmp(loglevel, "debug")) {
+ level = LOG_DEBUG;
+ } else if (!strcasecmp(loglevel, "info")) {
+ level = LOG_INFO;
+ } else if (!strcasecmp(loglevel, "warn")) {
+ level = LOG_WARN;
+ } else if (!strcasecmp(loglevel, "error")) {
+ level = LOG_ERROR;
+ } else if (!strcasecmp(loglevel, "fatal")) {
+ level = LOG_FATAL;
+ } else {
+ fprintf(stderr, "Unknown log level: %s\n", loglevel);
+ return 1;
+ }
+ log_set_level(level);
+ setvbuf(stderr, NULL, _IOLBF, 0);
+
+ log_file = NULL;
+ log_path = NULL;
+ sighup_received = 0;
+
+ if (logfile_path && logfile_path[0]) {
+ log_path = strdup(logfile_path);
+ log_file = fopen(log_path, "a");
+ if (log_file) {
+ log_add_callback(logfile_callback, log_path, level);
+ } else {
+ fprintf(stderr, "Could not open log file: %s\n", logfile_path);
+ free(log_path);
+ log_path = NULL;
+ }
+ }
+
+ if (resync_interval < 0) {
+ log_error("--resync-interval must be >= 0");
+ return 1;
+ }
+
+ signal(SIGHUP, sighup_handler);
+ signal(SIGINT, stop_handler);
+ signal(SIGTERM, stop_handler);
+ signal(SIGPIPE, SIG_IGN);
+
if (load_ports(config_dir) < 0) {
return 1;
}
+ // Startup order is load-bearing: ports -> backend select -> init ->
+ // port_apply -> nl_open -> initial resync -> ready -> event loop. frr must
+ // not start before the ready file exists or zebra misses the swpN netdevs.
+ //
// The built-in kernel backend is registered first so it acts as the
// fallback; plugins installed by hardware packages are probed ahead of it.
dp_register(dp_kernel_ops());
@@ -121,20 +246,49 @@ int main(int argc, const char **argv) {
}
if (dp_init() != DP_RET_OK) {
- fprintf(stderr, "dataplane: initialisation failed\n");
+ log_error("dataplane: initialisation failed");
return 1;
}
apply_ports();
- // TODO: netlink event loop -- subscribe to RTNLGRP_LINK / NEIGH / IPV4_ROUTE
- // / IPV6_ROUTE, mirror into the active dataplane, and periodically
- // call dp_active()->resync() to recover from dropped messages.
+ nl_fd = nl_open();
+ if (nl_fd < 0) {
+ dp_fini();
+ ports_free();
+ return 1;
+ }
+
+ // Fail loud: never publish readiness on partial state; runit retries.
+ if (nl_resync(nl_fd) != 0) {
+ log_fatal("netlink: initial resync failed");
+ close(nl_fd);
+ dp_fini();
+ ports_free();
+ return 1;
+ }
+
+ if (ready_file && *ready_file) {
+ if (ready_publish(ready_file) != 0) {
+ close(nl_fd);
+ dp_fini();
+ ports_free();
+ return 1;
+ }
+ log_info("ready: published %s", ready_file);
+ }
+
+ rc = nl_run(nl_fd, resync_interval);
+ close(nl_fd);
+ ready_remove(ready_file);
dp_fini();
ports_free();
- return rc;
+ if (log_file) fclose(log_file);
+ free(log_path);
+
+ return rc < 0 ? 1 : 0;
}
#ifdef __cplusplus
diff --git a/src/netlink/filter.c b/src/netlink/filter.c
@@ -0,0 +1,69 @@
+#include <string.h>
+#include <sys/socket.h>
+
+#include <linux/rtnetlink.h>
+
+#include "filter.h"
+
+#define FILTER_MAX_TRACKED 64
+#define FILTER_MAX_EXCLUDED 16
+
+static uint32_t tracked[FILTER_MAX_TRACKED];
+static int tracked_count = 0;
+static uint32_t excluded[FILTER_MAX_EXCLUDED];
+static int excluded_count = 0;
+
+static int find_u32(const uint32_t *arr, int n, uint32_t v) {
+ int i;
+ for (i = 0; i < n; i++) {
+ if (arr[i] == v) return i;
+ }
+ return -1;
+}
+
+void nl_filter_reset(void) {
+ tracked_count = 0;
+ excluded_count = 0;
+ tracked[tracked_count++] = RT_TABLE_MAIN;
+}
+
+void nl_filter_track(uint32_t table) {
+ if (table == RT_TABLE_LOCAL) return;
+ if (find_u32(tracked, tracked_count, table) >= 0) return;
+ if (tracked_count >= FILTER_MAX_TRACKED) return;
+ tracked[tracked_count++] = table;
+}
+
+void nl_filter_untrack(uint32_t table) {
+ int idx;
+ if (table == RT_TABLE_MAIN) return;
+ idx = find_u32(tracked, tracked_count, table);
+ if (idx < 0) return;
+ tracked[idx] = tracked[--tracked_count];
+}
+
+void nl_filter_exclude(uint32_t table) {
+ if (find_u32(excluded, excluded_count, table) >= 0) return;
+ if (excluded_count >= FILTER_MAX_EXCLUDED) return;
+ excluded[excluded_count++] = table;
+}
+
+bool nl_filter_mgmt_name(const char *ifname) {
+ if (!ifname) return false;
+ if (!strcmp(ifname, "mgmt")) return true;
+ if (!strcmp(ifname, "mgmt0")) return true;
+ return false;
+}
+
+bool nl_table_allowed(uint32_t table) {
+ if (table == RT_TABLE_LOCAL) return false;
+ if (find_u32(excluded, excluded_count, table) >= 0) return false;
+ return find_u32(tracked, tracked_count, table) >= 0;
+}
+
+bool nl_route_allowed(int family, uint8_t type, uint8_t scope, uint32_t table) {
+ if (family != AF_INET && family != AF_INET6) return false;
+ if (type != RTN_UNICAST) return false;
+ if (scope == RT_SCOPE_HOST) return false;
+ return nl_table_allowed(table);
+}
diff --git a/src/netlink/filter.h b/src/netlink/filter.h
@@ -0,0 +1,31 @@
+#ifndef __UNOS_NETLINK_FILTER_H__
+#define __UNOS_NETLINK_FILTER_H__
+
+#include <stdbool.h>
+#include <stdint.h>
+
+// Single policy point for what the mirror replicates into the dataplane.
+//
+// v1 policy: accept-all-except-LOCAL at the route-type level, intersected with
+// the tracked-table set so a management VRF never leaks into the ASIC. Both
+// the event path and the dump path call into here -- there is no second copy
+// of the policy. A future config (allow/deny lists) only extends this file's
+// backing store.
+
+void nl_filter_reset(void);
+
+// Called on VRF-master discovery / removal (RTM_NEWLINK / DELLINK).
+void nl_filter_track(uint32_t table);
+void nl_filter_untrack(uint32_t table);
+
+// Hard deny regardless of tracking. Used for the management VRF.
+void nl_filter_exclude(uint32_t table);
+
+// True when ifname looks like a management VRF (placeholder until the
+// exclusion is config-driven; verified name lands here).
+bool nl_filter_mgmt_name(const char *ifname);
+
+bool nl_table_allowed(uint32_t table);
+bool nl_route_allowed(int family, uint8_t type, uint8_t scope, uint32_t table);
+
+#endif // __UNOS_NETLINK_FILTER_H__
diff --git a/src/netlink/netlink.c b/src/netlink/netlink.c
@@ -0,0 +1,696 @@
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <net/if.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <linux/fib_rules.h>
+#include <linux/if_addr.h>
+#include <linux/if_link.h>
+#include <linux/neighbour.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+
+#include <arpa/inet.h>
+
+#include "rxi/log.h"
+
+#include "dataplane.h"
+#include "dataplane/registry.h"
+
+#include "filter.h"
+#include "netlink.h"
+
+#define NL_BUFSZ (64 * 1024)
+#define NL_MAX_NH 16
+
+// VRF masters: master ifindex -> table id + name. Single digits expected.
+struct vrf_master {
+ int ifindex;
+ uint32_t table;
+ char name[IF_NAMESIZE];
+};
+
+// Member enslavement: member ifindex -> master ifindex (0 = none).
+struct vrf_member {
+ int ifindex;
+ int master;
+};
+
+#define VRF_MAX_MASTERS 64
+#define VRF_MAX_MEMBERS 256
+
+static struct vrf_master vrf_masters[VRF_MAX_MASTERS];
+static int vrf_master_count = 0;
+static struct vrf_member vrf_members[VRF_MAX_MEMBERS];
+static int vrf_member_count = 0;
+
+static volatile sig_atomic_t nl_stop = 0;
+static uint32_t nl_seq = 0;
+
+void nl_request_stop(void) {
+ nl_stop = 1;
+}
+
+static void vrf_map_reset(void) {
+ vrf_master_count = 0;
+ vrf_member_count = 0;
+}
+
+static struct vrf_master *vrf_master_find(int ifindex) {
+ int i;
+ for (i = 0; i < vrf_master_count; i++) {
+ if (vrf_masters[i].ifindex == ifindex) return &vrf_masters[i];
+ }
+ return NULL;
+}
+
+static void vrf_master_set(int ifindex, uint32_t table, const char *name) {
+ struct vrf_master *m = vrf_master_find(ifindex);
+ if (m) {
+ m->table = table;
+ } else {
+ if (vrf_master_count >= VRF_MAX_MASTERS) {
+ log_warn("netlink: vrf master table full, ignoring %s", name ? name : "?");
+ return;
+ }
+ m = &vrf_masters[vrf_master_count++];
+ m->ifindex = ifindex;
+ m->table = table;
+ }
+ if (name) {
+ strncpy(m->name, name, sizeof(m->name) - 1);
+ m->name[sizeof(m->name) - 1] = '\0';
+ }
+}
+
+static void vrf_master_remove(int ifindex) {
+ int i;
+ for (i = 0; i < vrf_master_count; i++) {
+ if (vrf_masters[i].ifindex == ifindex) {
+ vrf_masters[i] = vrf_masters[--vrf_master_count];
+ return;
+ }
+ }
+}
+
+static void vrf_member_set(int ifindex, int master) {
+ int i;
+ for (i = 0; i < vrf_member_count; i++) {
+ if (vrf_members[i].ifindex == ifindex) {
+ vrf_members[i].master = master;
+ return;
+ }
+ }
+ if (vrf_member_count >= VRF_MAX_MEMBERS) return;
+ vrf_members[vrf_member_count].ifindex = ifindex;
+ vrf_members[vrf_member_count].master = master;
+ vrf_member_count++;
+}
+
+static void vrf_member_remove(int ifindex) {
+ int i;
+ for (i = 0; i < vrf_member_count; i++) {
+ if (vrf_members[i].ifindex == ifindex) {
+ vrf_members[i] = vrf_members[--vrf_member_count];
+ return;
+ }
+ }
+}
+
+// Table owning an interface: master's table, or MAIN when unenslaved/unknown.
+static uint32_t vrf_table_of_iface(int ifindex) {
+ int i;
+ int master = 0;
+ for (i = 0; i < vrf_member_count; i++) {
+ if (vrf_members[i].ifindex == ifindex) {
+ master = vrf_members[i].master;
+ break;
+ }
+ }
+ if (!master) return RT_TABLE_MAIN;
+ for (i = 0; i < vrf_master_count; i++) {
+ if (vrf_masters[i].ifindex == master) return vrf_masters[i].table;
+ }
+ return RT_TABLE_MAIN;
+}
+
+// rtm_table is 8-bit; ids above 255 arrive via RTA_TABLE with rtm_table set to
+// RT_TABLE_COMPAT. Getting this wrong silently aliases VRF tables onto MAIN.
+static uint32_t route_table_of(const struct rtmsg *rtm, struct rtattr **tb) {
+ uint32_t table = rtm->rtm_table;
+ if (table == RT_TABLE_COMPAT && tb[RTA_TABLE]) {
+ memcpy(&table, RTA_DATA(tb[RTA_TABLE]), sizeof(table));
+ }
+ return table;
+}
+
+static void parse_rtattr(struct rtattr **tb, int max, struct rtattr *rta, int len) {
+ memset(tb, 0, sizeof(struct rtattr *) * (max + 1));
+ while (RTA_OK(rta, len)) {
+ if (rta->rta_type <= max) tb[rta->rta_type] = rta;
+ rta = RTA_NEXT(rta, len);
+ }
+}
+
+// No NDMSG_RTA helper in the UAPI headers; attrs follow the fixed struct.
+static struct rtattr *ndmsg_rta(const struct ndmsg *ndm) {
+ return (struct rtattr *)(((char *)ndm) + NLMSG_ALIGN(sizeof(*ndm)));
+}
+
+static int handle_link(struct nlmsghdr *nh) {
+ struct ifinfomsg *ifi = NLMSG_DATA(nh);
+ struct rtattr *tb[IFLA_MAX + 1];
+ char ifname[IF_NAMESIZE] = "";
+ uint32_t mtu = 0;
+ 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)));
+ if (len < 0) return 0;
+ parse_rtattr(tb, IFLA_MAX, IFLA_RTA(ifi), len);
+
+ if (!tb[IFLA_IFNAME]) return 0;
+ strncpy(ifname, RTA_DATA(tb[IFLA_IFNAME]), sizeof(ifname) - 1);
+ if (tb[IFLA_MTU]) memcpy(&mtu, RTA_DATA(tb[IFLA_MTU]), sizeof(mtu));
+ if (tb[IFLA_MASTER]) memcpy(&master, RTA_DATA(tb[IFLA_MASTER]), sizeof(master));
+
+ // VRF masters carry linkinfo kind "vrf" with the table id nested inside
+ // IFLA_INFO_DATA as IFLA_VRF_TABLE.
+ if (tb[IFLA_LINKINFO]) {
+ struct rtattr *li[IFLA_INFO_MAX + 1];
+ struct rtattr *rta = RTA_DATA(tb[IFLA_LINKINFO]);
+ int lilen = (int)RTA_PAYLOAD(tb[IFLA_LINKINFO]);
+ parse_rtattr(li, IFLA_INFO_MAX, rta, lilen);
+ if (li[IFLA_INFO_KIND] && !strcmp(RTA_DATA(li[IFLA_INFO_KIND]), "vrf")) {
+ is_vrf = 1;
+ if (li[IFLA_INFO_DATA]) {
+ struct rtattr *vd[IFLA_VRF_MAX + 1];
+ struct rtattr *vrta = RTA_DATA(li[IFLA_INFO_DATA]);
+ int vlen = (int)RTA_PAYLOAD(li[IFLA_INFO_DATA]);
+ parse_rtattr(vd, IFLA_VRF_MAX, vrta, vlen);
+ if (vd[IFLA_VRF_TABLE]) {
+ memcpy(&vrf_table, RTA_DATA(vd[IFLA_VRF_TABLE]), sizeof(vrf_table));
+ }
+ }
+ }
+ }
+
+ if (nh->nlmsg_type == RTM_DELLINK) {
+ vrf_member_remove(ifi->ifi_index);
+ if (is_vrf || vrf_master_find(ifi->ifi_index)) {
+ // Removal drops tracking; a standing exclusion survives re-creation.
+ if (vrf_table) nl_filter_untrack(vrf_table);
+ 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);
+ }
+ }
+ return 0;
+ }
+
+ vrf_member_set(ifi->ifi_index, master);
+ if (is_vrf && vrf_table) {
+ vrf_master_set(ifi->ifi_index, vrf_table, ifname);
+ if (nl_filter_mgmt_name(ifname)) {
+ nl_filter_exclude(vrf_table);
+ log_info("netlink: vrf %s table %u excluded (management)", ifname, vrf_table);
+ } else {
+ nl_filter_track(vrf_table);
+ log_info("netlink: vrf %s table %u tracked", ifname, vrf_table);
+ }
+ }
+
+ 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 (mtu && dp->port_mtu) {
+ if (dp->port_mtu(ifname, mtu) != DP_RET_OK) {
+ log_error("netlink: port_mtu %s %u failed", ifname, mtu);
+ }
+ }
+ return 0;
+}
+
+static int handle_addr(struct nlmsghdr *nh) {
+ struct ifaddrmsg *ifa = NLMSG_DATA(nh);
+ struct rtattr *tb[IFA_MAX + 1];
+ 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;
+ int del;
+
+ len = (int)(nh->nlmsg_len - NLMSG_LENGTH(sizeof(*ifa)));
+ if (len < 0) return 0;
+ parse_rtattr(tb, IFA_MAX, IFA_RTA(ifa), len);
+
+ if (ifa->ifa_family != AF_INET && ifa->ifa_family != AF_INET6) return 0;
+ // IFA_LOCAL is the local address on point-to-point v4; IFA_ADDRESS would be
+ // the peer. Prefer LOCAL whenever present.
+ if (tb[IFA_LOCAL]) bytes = RTA_DATA(tb[IFA_LOCAL]);
+ else if (tb[IFA_ADDRESS]) bytes = RTA_DATA(tb[IFA_ADDRESS]);
+ else return 0;
+ want = (ifa->ifa_family == AF_INET) ? 4 : 16;
+
+ if (!if_indextoname(ifa->ifa_index, ifname)) return 0;
+
+ memset(&addr, 0, sizeof(addr));
+ addr.family = ifa->ifa_family;
+ addr.prefixlen = ifa->ifa_prefixlen;
+ memcpy(addr.addr, bytes, want);
+
+ memset(&rif, 0, sizeof(rif));
+ rif.ifname = ifname;
+ rif.addr = addr;
+ rif.table = vrf_table_of_iface(ifa->ifa_index);
+ if (!nl_table_allowed(rif.table)) {
+ log_debug("netlink: rif %s skipped (table %u not tracked)", ifname, rif.table);
+ 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);
+ } else {
+ if (dp->rif_add && dp->rif_add(&rif) != DP_RET_OK) log_error("netlink: rif_add %s failed", ifname);
+ }
+ return 0;
+}
+
+static int handle_neigh(struct nlmsghdr *nh) {
+ struct ndmsg *ndm = NLMSG_DATA(nh);
+ struct rtattr *tb[NDA_MAX + 1];
+ char ifname[IF_NAMESIZE] = "";
+ struct dp_neigh neigh;
+ const struct dp_ops *dp;
+ int len;
+ int del;
+
+ len = (int)(nh->nlmsg_len - NLMSG_LENGTH(sizeof(*ndm)));
+ if (len < 0) return 0;
+ parse_rtattr(tb, NDA_MAX, ndmsg_rta(ndm), len);
+
+ if (ndm->ndm_family != AF_INET && ndm->ndm_family != AF_INET6) return 0;
+ // Valid-only: incomplete/failed resolutions never reach the dataplane.
+ del = (nh->nlmsg_type == RTM_DELNEIGH);
+ if (!del) {
+ if (ndm->ndm_state & (NUD_FAILED | NUD_INCOMPLETE)) del = 1;
+ else if (!(ndm->ndm_state & (NUD_PERMANENT | NUD_REACHABLE | NUD_STALE | NUD_DELAY | NUD_PROBE))) {
+ return 0;
+ }
+ }
+ if (!tb[NDA_DST]) return 0;
+ if (!del && (!tb[NDA_LLADDR] || RTA_PAYLOAD(tb[NDA_LLADDR]) != 6)) {
+ log_debug("netlink: neigh skipped (no MAC)");
+ return 0;
+ }
+ if (!if_indextoname(ndm->ndm_ifindex, ifname)) return 0;
+
+ memset(&neigh, 0, sizeof(neigh));
+ neigh.ifname = ifname;
+ neigh.addr.family = ndm->ndm_family;
+ neigh.addr.prefixlen = (ndm->ndm_family == AF_INET) ? 32 : 128;
+ memcpy(neigh.addr.addr, RTA_DATA(tb[NDA_DST]), (ndm->ndm_family == AF_INET) ? 4 : 16);
+ if (!del) memcpy(neigh.mac, RTA_DATA(tb[NDA_LLADDR]), 6);
+ neigh.table = vrf_table_of_iface(ndm->ndm_ifindex);
+ if (!nl_table_allowed(neigh.table)) {
+ log_debug("netlink: neigh %s skipped (table %u not tracked)", ifname, neigh.table);
+ 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");
+ } else {
+ if (dp->neigh_add && dp->neigh_add(&neigh) != DP_RET_OK) log_error("netlink: neigh_add failed");
+ }
+ return 0;
+}
+
+static int handle_route(struct nlmsghdr *nh) {
+ struct rtmsg *rtm = NLMSG_DATA(nh);
+ struct rtattr *tb[RTA_MAX + 1];
+ 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;
+ int del;
+ size_t nh_count = 0;
+
+ len = (int)(nh->nlmsg_len - NLMSG_LENGTH(sizeof(*rtm)));
+ if (len < 0) return 0;
+ parse_rtattr(tb, RTA_MAX, RTM_RTA(rtm), len);
+
+ table = route_table_of(rtm, tb);
+ if (!nl_route_allowed(rtm->rtm_family, rtm->rtm_type, rtm->rtm_scope, table)) {
+ return 0;
+ }
+ if (tb[RTA_PRIORITY]) memcpy(&metric, RTA_DATA(tb[RTA_PRIORITY]), sizeof(metric));
+
+ memset(&route, 0, sizeof(route));
+ memset(nhops, 0, sizeof(nhops));
+ memset(nhnames, 0, sizeof(nhnames));
+ route.dst.family = rtm->rtm_family;
+ route.dst.prefixlen = rtm->rtm_dst_len;
+ if (tb[RTA_DST]) {
+ size_t want = (rtm->rtm_family == AF_INET) ? 4 : 16;
+ memcpy(route.dst.addr, RTA_DATA(tb[RTA_DST]), want);
+ }
+ route.metric = metric;
+ route.table = table;
+
+ if (tb[RTA_MULTIPATH]) {
+ // ECMP: nexthops nest gateways inside each rtnexthop, not at top level.
+ struct rtnexthop *rtnh = RTA_DATA(tb[RTA_MULTIPATH]);
+ int mplen = (int)RTA_PAYLOAD(tb[RTA_MULTIPATH]);
+ while (mplen >= (int)sizeof(*rtnh) && RTNH_OK(rtnh, mplen) && nh_count < NL_MAX_NH) {
+ struct rtattr *hops[RTA_MAX + 1];
+ int cur = RTNH_ALIGN(rtnh->rtnh_len);
+ int hlen = (int)rtnh->rtnh_len - (int)sizeof(*rtnh);
+ memset(hops, 0, sizeof(hops));
+ if (hlen > 0) parse_rtattr(hops, RTA_MAX, RTNH_DATA(rtnh), hlen);
+ if (if_indextoname(rtnh->rtnh_ifindex, nhnames[nh_count])) {
+ nhops[nh_count].ifname = nhnames[nh_count];
+ nhops[nh_count].gw.family = rtm->rtm_family;
+ if (hops[RTA_GATEWAY]) {
+ size_t want = (rtm->rtm_family == AF_INET) ? 4 : 16;
+ memcpy(nhops[nh_count].gw.addr, RTA_DATA(hops[RTA_GATEWAY]), want);
+ } else {
+ nhops[nh_count].gw.family = AF_UNSPEC;
+ }
+ nh_count++;
+ }
+ rtnh = RTNH_NEXT(rtnh);
+ mplen -= cur;
+ }
+ if (mplen > 0 && nh_count >= NL_MAX_NH) {
+ log_warn("netlink: ECMP group truncated to %d nexthops", NL_MAX_NH);
+ }
+ } else {
+ uint32_t oif = 0;
+ if (tb[RTA_OIF]) memcpy(&oif, RTA_DATA(tb[RTA_OIF]), sizeof(oif));
+ // Directly-connected routes carry OIF without a gateway; represent with
+ // an empty (AF_UNSPEC) gw rather than inventing one.
+ if (oif || tb[RTA_GATEWAY]) {
+ if (oif && !if_indextoname(oif, nhnames[0])) return 0;
+ nhops[0].ifname = oif ? nhnames[0] : NULL;
+ nhops[0].gw.family = rtm->rtm_family;
+ if (tb[RTA_GATEWAY]) {
+ size_t want = (rtm->rtm_family == AF_INET) ? 4 : 16;
+ memcpy(nhops[0].gw.addr, RTA_DATA(tb[RTA_GATEWAY]), want);
+ } else {
+ nhops[0].gw.family = AF_UNSPEC;
+ }
+ nh_count = 1;
+ }
+ }
+
+ 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);
+ } else {
+ if (dp->route_add && dp->route_add(&route) != DP_RET_OK) log_error("netlink: route_add table %u failed", table);
+ }
+ return 0;
+}
+
+static int handle_nlmsg(struct nlmsghdr *nh) {
+ switch (nh->nlmsg_type) {
+ case RTM_NEWLINK:
+ case RTM_DELLINK:
+ return handle_link(nh);
+ case RTM_NEWADDR:
+ case RTM_DELADDR:
+ return handle_addr(nh);
+ case RTM_NEWNEIGH:
+ case RTM_DELNEIGH:
+ return handle_neigh(nh);
+ case RTM_NEWROUTE:
+ case RTM_DELROUTE:
+ return handle_route(nh);
+ default:
+ return 0;
+ }
+}
+
+int nl_open(void) {
+ struct sockaddr_nl addr;
+ int fd;
+ int sndbuf = 256 * 1024;
+ int rcvbuf = 2 * 1024 * 1024;
+
+ fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_ROUTE);
+ if (fd < 0) {
+ log_error("netlink: socket failed: %s", strerror(errno));
+ return -1;
+ }
+ // Best effort: large receive buffer so bursts become resyncs, not losses.
+ // May fail without privilege in dev environments; event flow still works.
+ if (setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &rcvbuf, sizeof(rcvbuf)) != 0) {
+ log_warn("netlink: SO_RCVBUFFORCE failed: %s", strerror(errno));
+ }
+ setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
+
+ memset(&addr, 0, sizeof(addr));
+ addr.nl_family = AF_NETLINK;
+ // RTMGRP_* are group bitmasks; RTNLGRP_* are group numbers. Do not mix up.
+ addr.nl_groups = RTMGRP_LINK | RTMGRP_NEIGH
+ | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR
+ | RTMGRP_IPV4_ROUTE | RTMGRP_IPV6_ROUTE;
+ if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
+ log_error("netlink: bind failed: %s", strerror(errno));
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+// Send one GET*+DUMP and dispatch matching replies through handle_nlmsg.
+int nl_dump(int fd, uint16_t type, int family) {
+ struct {
+ struct nlmsghdr nh;
+ struct rtgenmsg gen;
+ } req;
+ char buf[NL_BUFSZ];
+ int done = 0;
+ int rc = 0;
+
+ memset(&req, 0, sizeof(req));
+ req.nh.nlmsg_len = NLMSG_LENGTH(sizeof(req.gen));
+ req.nh.nlmsg_type = type;
+ req.nh.nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST;
+ req.nh.nlmsg_seq = ++nl_seq;
+ req.nh.nlmsg_pid = 0;
+ req.gen.rtgen_family = (uint8_t)family;
+
+ if (send(fd, &req, req.nh.nlmsg_len, 0) < 0) {
+ log_error("netlink: dump send %u failed: %s", type, strerror(errno));
+ return -1;
+ }
+
+ while (!done) {
+ struct pollfd pfd = { .fd = fd, .events = POLLIN };
+ struct nlmsghdr *h;
+ int len;
+ int pr = poll(&pfd, 1, 5000);
+ if (pr <= 0) {
+ log_error("netlink: dump recv %u timed out", type);
+ return -1;
+ }
+ len = recv(fd, buf, sizeof(buf), 0);
+ if (len < 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK) continue;
+ log_error("netlink: dump recv %u failed: %s", type, strerror(errno));
+ return -1;
+ }
+ h = (struct nlmsghdr *)buf;
+ while (NLMSG_OK(h, len)) {
+ if (h->nlmsg_seq && h->nlmsg_seq != nl_seq) goto next;
+ if (h->nlmsg_type == NLMSG_DONE) {
+ done = 1;
+ break;
+ }
+ if (h->nlmsg_type == NLMSG_ERROR) {
+ struct nlmsgerr *e = NLMSG_DATA(h);
+ if (e->error) {
+ log_error("netlink: dump %u error: %s", type, strerror(-e->error));
+ rc = -1;
+ done = 1;
+ break;
+ }
+ goto next;
+ }
+ handle_nlmsg(h);
+ next:
+ h = NLMSG_NEXT(h, len);
+ }
+ }
+ return rc;
+}
+
+// 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();
+ vrf_map_reset();
+
+ if (nl_dump(fd, RTM_GETLINK, AF_UNSPEC) != 0) fails++;
+ if (nl_dump(fd, RTM_GETADDR, AF_INET) != 0) fails++;
+ if (nl_dump(fd, RTM_GETADDR, AF_INET6) != 0) fails++;
+ if (nl_dump(fd, RTM_GETNEIGH, AF_INET) != 0) fails++;
+ if (nl_dump(fd, RTM_GETNEIGH, AF_INET6) != 0) fails++;
+ 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");
+ fails++;
+ }
+ if (fails) {
+ log_warn("netlink: resync finished with %d failures", fails);
+ return -1;
+ }
+ log_info("netlink: resync complete");
+ return 0;
+}
+
+static long elapsed_ms(const struct timespec *a, const struct timespec *b) {
+ return (b->tv_sec - a->tv_sec) * 1000L + (b->tv_nsec - a->tv_nsec) / 1000000L;
+}
+
+int nl_run(int fd, int resync_interval_s) {
+ static char buf[NL_BUFSZ];
+ struct timespec last_resync;
+ clock_gettime(CLOCK_MONOTONIC, &last_resync);
+
+ while (!nl_stop) {
+ struct pollfd pfd = { .fd = fd, .events = POLLIN };
+ struct timespec now;
+ long wait_ms = 30000;
+ int pr;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if (resync_interval_s > 0) {
+ long remain = resync_interval_s * 1000L - elapsed_ms(&last_resync, &now);
+ if (remain <= 0) {
+ if (nl_resync(fd) != 0) {
+ // Log-and-continue with a single immediate retry; the next timer
+ // converges anything still missing.
+ log_warn("netlink: periodic resync failed, retrying once");
+ if (nl_resync(fd) != 0) log_error("netlink: periodic resync retry failed");
+ }
+ clock_gettime(CLOCK_MONOTONIC, &last_resync);
+ continue;
+ }
+ if (remain < wait_ms) wait_ms = remain;
+ }
+
+ pr = poll(&pfd, 1, (int)wait_ms);
+ if (pr < 0) {
+ if (errno == EINTR) continue;
+ log_error("netlink: poll failed: %s", strerror(errno));
+ return -1;
+ }
+ if (!pr) continue;
+
+ for (;;) {
+ struct nlmsghdr *h;
+ struct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+ struct msghdr msg;
+ struct sockaddr_nl nladdr;
+ ssize_t len;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.msg_name = &nladdr;
+ msg.msg_namelen = sizeof(nladdr);
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+
+ len = recvmsg(fd, &msg, 0);
+ if (len < 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) break;
+ if (errno == ENOBUFS) {
+ // Expected under load, not exceptional: re-converge from dumps.
+ log_warn("netlink: ENOBUFS, resyncing");
+ if (nl_resync(fd) != 0) {
+ log_warn("netlink: ENOBUFS resync failed, retrying once");
+ nl_resync(fd);
+ }
+ clock_gettime(CLOCK_MONOTONIC, &last_resync);
+ break;
+ }
+ log_error("netlink: recvmsg failed: %s", strerror(errno));
+ return -1;
+ }
+ if (len == 0) break;
+ if (msg.msg_flags & MSG_TRUNC) {
+ log_warn("netlink: message truncated, resyncing");
+ if (nl_resync(fd) != 0) nl_resync(fd);
+ clock_gettime(CLOCK_MONOTONIC, &last_resync);
+ break;
+ }
+
+ h = (struct nlmsghdr *)buf;
+ while (NLMSG_OK(h, (int)len)) {
+ if (h->nlmsg_type == NLMSG_ERROR) {
+ struct nlmsgerr *e = NLMSG_DATA(h);
+ if (e->error == -ENOBUFS) {
+ log_warn("netlink: ENOBUFS via NLMSG_ERROR, resyncing");
+ if (nl_resync(fd) != 0) nl_resync(fd);
+ clock_gettime(CLOCK_MONOTONIC, &last_resync);
+ } else if (e->error) {
+ log_error("netlink: NLMSG_ERROR: %s", strerror(-e->error));
+ }
+ } else if (h->nlmsg_type != NLMSG_DONE) {
+ handle_nlmsg(h);
+ }
+ h = NLMSG_NEXT(h, len);
+ }
+ // Non-blocking drain: keep reading until EAGAIN.
+ if ((size_t)len < sizeof(buf)) break;
+ }
+ }
+ return 0;
+}
diff --git a/src/netlink/netlink.h b/src/netlink/netlink.h
@@ -0,0 +1,18 @@
+#ifndef __UNOS_NETLINK_H__
+#define __UNOS_NETLINK_H__
+
+#include <stdint.h>
+
+// Raw NETLINK_ROUTE mirror. No libnl/libmnl dependency by design.
+//
+// Ownership: nl_run() resolves dp_active() on every dispatch, never caches it.
+// The VRF map (master table ids, member enslavement) stays inside netlink.c;
+// only the numeric table id crosses struct dp_ops.
+
+int nl_open(void); // bound NETLINK_ROUTE fd, or -1
+int nl_run(int fd, int resync_interval_s); // poll loop; 0 on stop, -1 on fatal
+void nl_request_stop(void); // async-signal-safe stop flag
+int nl_dump(int fd, uint16_t type, int family); // one GET*+DUMP, replies via handle_nlmsg
+int nl_resync(int fd); // filter_reset + dumps + dp resync; 0 ok, -1 fail
+
+#endif // __UNOS_NETLINK_H__
diff --git a/target/common/.dep b/target/common/.dep
@@ -1,2 +1,3 @@
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
+rxi/log https://git.finwo.net/misc/dep-repository/archives/heads/pkg/rxi/log.tar.gz