From 4a31df1059b739edffd305d65abd21a66eaf5f9f Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 22 Feb 2026 18:51:17 +0100 Subject: [PATCH] netd: add `vtysh -b` backend This is a simpler approach than both grpc and watchfrr. Since we only really need dynamic handling of static routes (for DHCP), we can track changes made and maintain a "diff engine" for the resulting frr.conf. Signed-off-by: Joachim Wiberg --- .../rootfs/etc/finit.d/available/netd.conf | 2 +- package/netd/Config.in | 35 +- package/netd/netd.mk | 3 + .../etc/finit.d/available/frr/ripd.conf | 3 +- .../etc/finit.d/available/frr/staticd.conf | 2 +- .../etc/finit.d/available/frr/zebra.conf | 5 +- src/confd/src/routing.c | 10 +- src/netd/Makefile.am | 5 + src/netd/configure.ac | 18 +- src/netd/src/netd.c | 23 +- src/netd/src/vtysh_backend.c | 298 ++++++++++++++++++ src/netd/src/vtysh_backend.h | 12 + 12 files changed, 393 insertions(+), 23 deletions(-) create mode 100644 src/netd/src/vtysh_backend.c create mode 100644 src/netd/src/vtysh_backend.h diff --git a/board/common/rootfs/etc/finit.d/available/netd.conf b/board/common/rootfs/etc/finit.d/available/netd.conf index 34d7e6e6..e6e0b79b 100644 --- a/board/common/rootfs/etc/finit.d/available/netd.conf +++ b/board/common/rootfs/etc/finit.d/available/netd.conf @@ -1,2 +1,2 @@ #set DEBUG=1 -service [2345] name:netd netd -- Network route daemon +service [2345] name:netd netd -- Network route daemon diff --git a/package/netd/Config.in b/package/netd/Config.in index 97e7ddaa..b7d05419 100644 --- a/package/netd/Config.in +++ b/package/netd/Config.in @@ -6,28 +6,42 @@ config BR2_PACKAGE_NETD Network route daemon. Manages static routes and RIP routing. Reads configuration from /etc/netd/conf.d/*.conf. - With FRR: Full routing via gRPC (static routes, RIP, OSPF). + With FRR: Full routing via gRPC, vtysh, or frr.conf. Without FRR: Standalone Linux backend via rtnetlink. https://github.com/kernelkit/infix if BR2_PACKAGE_NETD +choice + prompt "FRR backend" + default BR2_PACKAGE_NETD_FRR_VTYSH if BR2_PACKAGE_FRR + default BR2_PACKAGE_NETD_LINUX + +config BR2_PACKAGE_NETD_FRR_VTYSH + bool "FRR vtysh" + depends on BR2_PACKAGE_FRR + help + Enable FRR integration via vtysh -b. + netd generates incremental config diffs and applies + them to running FRR daemons via vtysh -b. + + FRR daemons run as standalone finit services (no watchfrr). + Provides static routes and RIP support. + config BR2_PACKAGE_NETD_FRR_CONF - bool "FRR frr.conf backend" - default y if BR2_PACKAGE_FRR + bool "FRR frr.conf" depends on BR2_PACKAGE_FRR help Enable FRR integration via frr.conf file generation. netd assembles a single /etc/frr/frr.conf from routing - config and signals FRR to reload via frrinit.sh. + config and signals FRR to reload via frrinit.sh/watchfrr. Provides full routing support (static routes, RIP, OSPF). config BR2_PACKAGE_NETD_FRR - bool "FRR gRPC backend" + bool "FRR gRPC" depends on BR2_PACKAGE_FRR - depends on !BR2_PACKAGE_NETD_FRR_CONF select BR2_PACKAGE_PROTOBUF select BR2_PACKAGE_GRPC select BR2_PACKAGE_HOST_PROTOBUF @@ -36,7 +50,12 @@ config BR2_PACKAGE_NETD_FRR Enable FRR integration via gRPC northbound API. Provides full routing support (static routes, RIP, OSPF). - If disabled, netd uses Linux kernel backend (rtnetlink) - with static routes only. +config BR2_PACKAGE_NETD_LINUX + bool "Linux kernel" + help + Use Linux kernel backend (rtnetlink) with static routes + only. No FRR dependency. + +endchoice endif diff --git a/package/netd/netd.mk b/package/netd/netd.mk index a4d3ce0c..094decb9 100644 --- a/package/netd/netd.mk +++ b/package/netd/netd.mk @@ -21,6 +21,9 @@ NETD_CONF_OPTS = --prefix= --disable-silent-rules ifeq ($(BR2_PACKAGE_NETD_FRR_CONF),y) NETD_DEPENDENCIES += frr NETD_CONF_OPTS += --with-frr-conf +else ifeq ($(BR2_PACKAGE_NETD_FRR_VTYSH),y) +NETD_DEPENDENCIES += frr +NETD_CONF_OPTS += --with-frr-vtysh else ifeq ($(BR2_PACKAGE_NETD_FRR),y) NETD_DEPENDENCIES += frr grpc host-grpc protobuf NETD_CONF_ENV += \ diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf index 7865b0b7..bb311b58 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf @@ -1,2 +1,3 @@ service env:-/etc/default/ripd \ - [2345] ripd $RIPD_ARGS -- RIP daemon + [2345] ripd $RIPD_ARGS + -- RIP daemon diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/staticd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/staticd.conf index 0016ef90..449cd95b 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/staticd.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/staticd.conf @@ -1,3 +1,3 @@ -service log:null \ +service log:null \ [2345] staticd -A 127.0.0.1 -u frr -g frr \ -- Static routing daemon diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf index 8699e09a..dce1abca 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf @@ -1,2 +1,3 @@ -service pid:!/run/frr/zebra.pid env:-/etc/default/zebra \ - [2345] zebra $ZEBRA_ARGS -- Zebra routing daemon +service pid:!/run/frr/zebra.pid env:-/etc/default/zebra \ + [2345] zebra $ZEBRA_ARGS + -- Zebra routing daemon diff --git a/src/confd/src/routing.c b/src/confd/src/routing.c index 6906b5bf..50d8a7bc 100644 --- a/src/confd/src/routing.c +++ b/src/confd/src/routing.c @@ -539,7 +539,7 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly ripd_enabled = fexist(RIPD_SIGNAL_NEXT); activate: - /* Generate complete /etc/frr/daemons */ + /* Generate complete /etc/frr/daemons (for watchfrr/frrinit.sh) */ frr_daemons_write(ospfd_enabled, ripd_enabled, bfdd_enabled); if (bfdd_enabled) @@ -569,6 +569,14 @@ activate: (void)remove(NETD_CONF); } + /* Enable/disable FRR daemons as standalone finit services. + * Harmless no-op when using watchfrr (services not installed). */ + systemf("initctl -nbq %s ospfd", ospfd_enabled ? "enable" : "disable"); + if (ospfd_enabled) + systemf("initctl -nbq touch ospfd"); + systemf("initctl -nbq %s ripd", ripd_enabled ? "enable" : "disable"); + systemf("initctl -nbq %s bfdd", bfdd_enabled ? "enable" : "disable"); + /* * Signal netd to reload - it assembles /etc/frr/frr.conf and * Finit propagates the restart to the frr sysv service diff --git a/src/netd/Makefile.am b/src/netd/Makefile.am index 03be8ab7..16803eac 100644 --- a/src/netd/Makefile.am +++ b/src/netd/Makefile.am @@ -33,7 +33,12 @@ if HAVE_FRR_CONF # FRR frr.conf file backend netd_SOURCES += src/frrconf_backend.c src/frrconf_backend.h else +if HAVE_FRR_VTYSH +# FRR vtysh -b backend +netd_SOURCES += src/vtysh_backend.c src/vtysh_backend.h +else # Linux kernel backend (no FRR) netd_SOURCES += src/linux_backend.c src/linux_backend.h endif endif +endif diff --git a/src/netd/configure.ac b/src/netd/configure.ac index 281d01d1..ca2fd2b1 100644 --- a/src/netd/configure.ac +++ b/src/netd/configure.ac @@ -24,10 +24,21 @@ AC_ARG_WITH(frr-conf, [with_frr_conf=$withval], [with_frr_conf=no]) +AC_ARG_WITH(frr-vtysh, + AS_HELP_STRING([--with-frr-vtysh], [Build with FRR vtysh -b backend (incremental config)]), + [with_frr_vtysh=$withval], + [with_frr_vtysh=no]) + # Mutual exclusion check AS_IF([test "x$with_frr" = "xyes" -a "x$with_frr_conf" = "xyes"], [ AC_MSG_ERROR([--with-frr and --with-frr-conf are mutually exclusive]) ]) +AS_IF([test "x$with_frr" = "xyes" -a "x$with_frr_vtysh" = "xyes"], [ + AC_MSG_ERROR([--with-frr and --with-frr-vtysh are mutually exclusive]) +]) +AS_IF([test "x$with_frr_conf" = "xyes" -a "x$with_frr_vtysh" = "xyes"], [ + AC_MSG_ERROR([--with-frr-conf and --with-frr-vtysh are mutually exclusive]) +]) AS_IF([test "x$with_frr" = "xyes"], [ # Check for protoc and grpc_cpp_plugin @@ -55,8 +66,13 @@ AS_IF([test "x$with_frr_conf" = "xyes"], [ AC_DEFINE(HAVE_FRR_CONF, 1, [Built with FRR frr.conf file backend]) ]) +AS_IF([test "x$with_frr_vtysh" = "xyes"], [ + AC_DEFINE(HAVE_FRR_VTYSH, 1, [Built with FRR vtysh -b backend]) +]) + AM_CONDITIONAL(HAVE_FRR_GRPC, [test "x$with_frr" = "xyes"]) AM_CONDITIONAL(HAVE_FRR_CONF, [test "x$with_frr_conf" = "xyes"]) +AM_CONDITIONAL(HAVE_FRR_VTYSH, [test "x$with_frr_vtysh" = "xyes"]) # Check for pkg-config first PKG_PROG_PKG_CONFIG @@ -89,7 +105,7 @@ cat < +#include +#include + +#include "netd.h" +#include "vtysh_backend.h" + +#define FRR_CONF "/etc/frr/frr.conf" +#define FRR_CONF_NEXT FRR_CONF "+" +#define NETD_CONF "/etc/frr/netd.conf" +#define NETD_CONF_NEXT NETD_CONF "+" +#define OSPFD_CONF "/etc/frr/ospfd.conf" + +static const char *frr_header = + "! Generated by netd\n" + "frr defaults traditional\n" + "hostname Router\n" + "password zebra\n" + "enable password zebra\n" + "no log unique-id\n" + "log syslog warnings\n" + "log facility local2\n" + "!\n"; + +int vtysh_backend_init(void) +{ + INFO("Using FRR vtysh backend"); + return 0; +} + +void vtysh_backend_cleanup(void) +{ + /* Nothing to clean up */ +} + +static void write_route(FILE *fp, struct route *r) +{ + char prefix_str[INET6_ADDRSTRLEN]; + char gw_str[INET6_ADDRSTRLEN]; + const char *cmd; + + if (r->family == AF_INET) { + cmd = "ip"; + inet_ntop(AF_INET, &r->prefix.ip4, prefix_str, sizeof(prefix_str)); + } else { + cmd = "ipv6"; + inet_ntop(AF_INET6, &r->prefix.ip6, prefix_str, sizeof(prefix_str)); + } + + fprintf(fp, "%s route %s/%u ", cmd, prefix_str, r->prefixlen); + + switch (r->nh_type) { + case NH_ADDR: + if (r->family == AF_INET) + inet_ntop(AF_INET, &r->gateway.gw4, gw_str, sizeof(gw_str)); + else + inet_ntop(AF_INET6, &r->gateway.gw6, gw_str, sizeof(gw_str)); + fputs(gw_str, fp); + break; + case NH_IFNAME: + fputs(r->ifname, fp); + break; + case NH_BLACKHOLE: + switch (r->bh_type) { + case BH_NULL: + fputs("Null0", fp); + break; + case BH_REJECT: + fputs("reject", fp); + break; + case BH_DROP: + fputs("blackhole", fp); + break; + } + break; + } + + if (r->distance) + fprintf(fp, " %u", r->distance); + + fputc('\n', fp); +} + +static const char *redist_name(enum rip_redist_type type) +{ + switch (type) { + case RIP_REDIST_CONNECTED: return "connected"; + case RIP_REDIST_STATIC: return "static"; + case RIP_REDIST_KERNEL: return "kernel"; + case RIP_REDIST_OSPF: return "ospf"; + } + return "unknown"; +} + +static void write_rip_config(FILE *fp, struct rip_config *rip) +{ + struct rip_redistribute *redist; + struct rip_neighbor *nbr; + struct rip_network *net; + + if (!rip->enabled) + return; + + fputs("router rip\n", fp); + fprintf(fp, " default-metric %u\n", rip->default_metric); + fprintf(fp, " distance %u\n", rip->distance); + fprintf(fp, " timers basic %u %u %u\n", + rip->timers.update, rip->timers.invalid, rip->timers.flush); + + if (rip->default_route) + fputs(" default-information originate\n", fp); + + TAILQ_FOREACH(net, &rip->networks, entries) { + fprintf(fp, " network %s\n", net->ifname); + if (net->passive) + fprintf(fp, " passive-interface %s\n", net->ifname); + } + + TAILQ_FOREACH(nbr, &rip->neighbors, entries) { + char addr[INET_ADDRSTRLEN]; + + inet_ntop(AF_INET, &nbr->addr, addr, sizeof(addr)); + fprintf(fp, " neighbor %s\n", addr); + } + + TAILQ_FOREACH(redist, &rip->redistributes, entries) + fprintf(fp, " redistribute %s\n", redist_name(redist->type)); + + fputs("!\n", fp); +} + +static void append_file(FILE *fp, const char *path) +{ + char buf[1024]; + FILE *src; + size_t n; + + src = fopen(path, "r"); + if (!src) + return; + + while ((n = fread(buf, 1, sizeof(buf), src)) > 0) + fwrite(buf, 1, n, fp); + + fclose(src); + DEBUG("vtysh: appended %s", path); +} + +/* + * Negate old state by reading previous netd.conf and prefixing + * each config line with "no ". Comment and blank lines are skipped. + * + * For "router rip" blocks we emit a single "no router rip" instead + * of negating each sub-command individually. + */ +static void negate_old_conf(FILE *fp) +{ + char line[256]; + int in_rip = 0; + int count = 0; + FILE *old; + + old = fopen(NETD_CONF, "r"); + if (!old) { + DEBUG("vtysh: no previous %s, first boot", NETD_CONF); + return; + } + + while (fgets(line, sizeof(line), old)) { + size_t len = strlen(line); + + /* Strip trailing newline */ + if (len > 0 && line[len - 1] == '\n') + line[--len] = '\0'; + + /* Skip empty lines and comments */ + if (len == 0 || line[0] == '!' || line[0] == '#') + continue; + + /* Track router rip block */ + if (strcmp(line, "router rip") == 0) { + in_rip = 1; + fputs("no router rip\n", fp); + count++; + continue; + } + + /* Skip sub-commands inside router rip block */ + if (in_rip) { + /* Sub-commands are indented with space */ + if (line[0] == ' ') + continue; + in_rip = 0; + } + + /* Negate route lines (ip route ..., ipv6 route ...) */ + fprintf(fp, "no %s\n", line); + count++; + } + + fclose(old); + DEBUG("vtysh: negated %d old config lines", count); +} + +/* + * Save current config to netd.conf for next reload or crash recovery. + * Written atomically via rename. + */ +static int save_conf(struct route_head *routes, struct rip_config *rip) +{ + struct route *r; + FILE *fp; + + fp = fopen(NETD_CONF_NEXT, "w"); + if (!fp) { + ERROR("vtysh: failed to open %s: %s", NETD_CONF_NEXT, strerror(errno)); + return -1; + } + + TAILQ_FOREACH(r, routes, entries) + write_route(fp, r); + + write_rip_config(fp, rip); + + fclose(fp); + + if (rename(NETD_CONF_NEXT, NETD_CONF)) { + ERROR("vtysh: failed to rename %s to %s: %s", + NETD_CONF_NEXT, NETD_CONF, strerror(errno)); + return -1; + } + + return 0; +} + +int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip) +{ + struct route *r; + int rc, count = 0; + FILE *fp; + + fp = fopen(FRR_CONF_NEXT, "w"); + if (!fp) { + ERROR("vtysh: failed to open %s: %s", FRR_CONF_NEXT, strerror(errno)); + return -1; + } + + fputs(frr_header, fp); + + /* Negate old state from previous netd.conf */ + negate_old_conf(fp); + + /* Write new routes */ + TAILQ_FOREACH(r, routes, entries) { + write_route(fp, r); + count++; + } + DEBUG("vtysh: wrote %d new routes", count); + + /* Write new RIP config */ + write_rip_config(fp, rip); + + /* Append OSPF config if present (written by confd) */ + append_file(fp, OSPFD_CONF); + + fclose(fp); + + if (rename(FRR_CONF_NEXT, FRR_CONF)) { + ERROR("vtysh: failed to rename %s to %s: %s", + FRR_CONF_NEXT, FRR_CONF, strerror(errno)); + return -1; + } + + rc = system("vtysh -b"); + if (rc) { + ERROR("vtysh: vtysh -b failed with status %d", rc); + return -1; + } + + /* Persist new state for next reload / crash recovery */ + if (save_conf(routes, rip)) + ERROR("vtysh: failed to save %s, next reload may be inconsistent", NETD_CONF); + + INFO("vtysh: applied config via vtysh -b"); + return 0; +} diff --git a/src/netd/src/vtysh_backend.h b/src/netd/src/vtysh_backend.h new file mode 100644 index 00000000..dc60e4fa --- /dev/null +++ b/src/netd/src/vtysh_backend.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef NETD_VTYSH_BACKEND_H_ +#define NETD_VTYSH_BACKEND_H_ + +#include "netd.h" + +int vtysh_backend_init(void); +void vtysh_backend_cleanup(void); +int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip); + +#endif /* NETD_VTYSH_BACKEND_H_ */