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 <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-02-24 06:59:32 +01:00
parent 965906e2ff
commit 4a31df1059
12 changed files with 393 additions and 23 deletions
@@ -1,2 +1,2 @@
#set DEBUG=1
service [2345] <pid/confd> name:netd netd -- Network route daemon
service [2345] <pid/confd,pid/staticd> name:netd netd -- Network route daemon
+27 -8
View File
@@ -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
+3
View File
@@ -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 += \
@@ -1,2 +1,3 @@
service <pid/zebra> env:-/etc/default/ripd \
[2345] ripd $RIPD_ARGS -- RIP daemon
[2345] ripd $RIPD_ARGS
-- RIP daemon
@@ -1,3 +1,3 @@
service log:null <!pid/zebra> \
service <!pid/zebra> log:null \
[2345] staticd -A 127.0.0.1 -u frr -g frr \
-- Static routing daemon
@@ -1,2 +1,3 @@
service <!pid/netd> pid:!/run/frr/zebra.pid env:-/etc/default/zebra \
[2345] zebra $ZEBRA_ARGS -- Zebra routing daemon
service <!pid/mgmtd> pid:!/run/frr/zebra.pid env:-/etc/default/zebra \
[2345] zebra $ZEBRA_ARGS
-- Zebra routing daemon
+9 -1
View File
@@ -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
+5
View File
@@ -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
+17 -1
View File
@@ -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 <<EOF
Prefix................: $prefix
Exec prefix...........: $eprefix
Sysconfdir............: `eval echo $sysconfdir`
Backend...............: $(test "x$with_frr" = "xyes" && echo "FRR gRPC" || (test "x$with_frr_conf" = "xyes" && echo "FRR frr.conf" || echo "Linux kernel"))
Backend...............: $(test "x$with_frr" = "xyes" && echo "FRR gRPC" || (test "x$with_frr_conf" = "xyes" && echo "FRR frr.conf" || (test "x$with_frr_vtysh" = "xyes" && echo "FRR vtysh" || echo "Linux kernel")))
C Compiler............: $CC $CFLAGS $CPPFLAGS $LDFLAGS $LIBS
------------- Compiler version --------------
+15 -8
View File
@@ -7,6 +7,14 @@
#include "netd.h"
#include "config.h"
int debug;
static sig_atomic_t do_reload;
static sig_atomic_t do_shutdown;
static struct route_head active_routes = TAILQ_HEAD_INITIALIZER(active_routes);
static struct rip_config active_rip;
/* Backend selection at compile time */
#ifdef HAVE_FRR_GRPC
#include "grpc_backend.h"
@@ -22,6 +30,13 @@ static void backend_cleanup(void) { frrconf_backend_cleanup(); }
static int backend_apply(struct route_head *routes, struct rip_config *rip) {
return frrconf_backend_apply(routes, rip);
}
#elif defined(HAVE_FRR_VTYSH)
#include "vtysh_backend.h"
static int backend_init(void) { return vtysh_backend_init(); }
static void backend_cleanup(void) { vtysh_backend_cleanup(); }
static int backend_apply(struct route_head *routes, struct rip_config *rip) {
return vtysh_backend_apply(routes, rip);
}
#else
#include "linux_backend.h"
static int backend_init(void) { return linux_backend_init(); }
@@ -31,14 +46,6 @@ static int backend_apply(struct route_head *routes, struct rip_config *rip) {
}
#endif
int debug;
static volatile sig_atomic_t do_reload;
static volatile sig_atomic_t do_shutdown;
static struct route_head active_routes = TAILQ_HEAD_INITIALIZER(active_routes);
static struct rip_config active_rip;
static void sighup_handler(int sig)
{
(void)sig;
+298
View File
@@ -0,0 +1,298 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* FRR vtysh backend for netd - applies incremental route changes
* by generating a diff (negations of old state + new state) and
* feeding it to running FRR daemons via `vtysh -b`.
*
* Old state is persisted in NETD_CONF so we can recover after a
* crash — on startup, any routes/config in that file are negated
* before new config is applied.
*/
#include <errno.h>
#include <stdio.h>
#include <string.h>
#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;
}
+12
View File
@@ -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_ */