From be3891c2f0d296561462707df4adce1db33bdbc0 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 12 Mar 2026 10:13:36 +0100 Subject: [PATCH 1/2] netd: watch conf.d with inotify, retry on backend failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two races could prevent DHCP-learned default routes from being installed at boot: 1. The signal from the DHCP client script could be lost leaving conf.d updated but frr.conf stale. 2. Even when the signal was received, 'vtysh -b' could fail because the FRR daemon chain (mgmtd→zebra→staticd) writes PID files before being fully operational, causing netd to give up with no retry. Fix both by refactoring netd to use libev: - Use an inotify watch of CONF_DIR, so netd reacts directly to file changes without depending on signal delivery. - On backend_apply() failure, schedule a retry in 5s and call pidfile() unconditionally so dependent services are not blocked waiting for the Finit condition 'pid/netd' to be satisfied. We also take this opportunity to rename /etc/netd/conf.d/ to /etc/net.d/ The extra /etc/netd/ directory level served no purpose — nothing else lives there. Flatten to /etc/net.d/ which reads more naturally as the drop-in directory for network configuration snippets. Also, reduce logging a bit since each netd backend already logs success or fail which is sufficient to know that a configuration change has been applied or not. Finally, with the new inotify processing in netd it's redundant to call 'initctl reload netd' from the udhcpc script, in fact it will only cause unnecessary overhead, so we drop it. Fixes #1438 Signed-off-by: Joachim Wiberg --- board/common/rootfs/etc/netd/conf.d/.empty | 0 .../rootfs/usr/share/udhcpc/default.script | 6 +- package/netd/Config.in | 3 +- package/netd/netd.mk | 4 +- src/confd/src/routing.c | 2 +- src/netd/Makefile.am | 2 +- src/netd/README.md | 8 +- src/netd/configure.ac | 12 +- src/netd/src/netd.c | 127 ++++++++++++------ src/netd/src/netd.h | 2 +- 10 files changed, 109 insertions(+), 57 deletions(-) delete mode 100644 board/common/rootfs/etc/netd/conf.d/.empty diff --git a/board/common/rootfs/etc/netd/conf.d/.empty b/board/common/rootfs/etc/netd/conf.d/.empty deleted file mode 100644 index e69de29b..00000000 diff --git a/board/common/rootfs/usr/share/udhcpc/default.script b/board/common/rootfs/usr/share/udhcpc/default.script index c2a8c24a..a28eb28f 100755 --- a/board/common/rootfs/usr/share/udhcpc/default.script +++ b/board/common/rootfs/usr/share/udhcpc/default.script @@ -7,7 +7,7 @@ ACTION="$1" IP_CACHE="/var/lib/misc/${interface}.cache" RESOLV_CONF="/run/resolvconf/interfaces/${interface}.conf" NTPFILE="/run/chrony/dhcp-sources.d/${interface}.sources" -NAME="/etc/netd/conf.d/${interface}-dhcp.conf" +NAME="/etc/net.d/${interface}-dhcp.conf" NEXT="${NAME}+" [ -n "$broadcast" ] && BROADCAST="broadcast $broadcast" @@ -118,8 +118,6 @@ set_dhcp_routes() # Reduce changes needed by comparing with previous route(s) cmp -s "$NAME" "$NEXT" && return mv "$NEXT" "$NAME" - - initctl reload netd } clr_dhcp_routes() @@ -127,8 +125,6 @@ clr_dhcp_routes() log "deleting DHCP routes" [ -f "$NAME" ] || return rm "$NAME" - - initctl reload netd } clr_dhcp_addresses() diff --git a/package/netd/Config.in b/package/netd/Config.in index 94bd4a92..96be1fc3 100644 --- a/package/netd/Config.in +++ b/package/netd/Config.in @@ -2,9 +2,10 @@ config BR2_PACKAGE_NETD bool "netd" select BR2_PACKAGE_LIBITE select BR2_PACKAGE_LIBCONFUSE + select BR2_PACKAGE_LIBEV help Network route daemon. Manages static routes and RIP routing. - Reads configuration from /etc/netd/conf.d/*.conf. + Reads configuration from /etc/net.d/*.conf. With FRR: Full routing via gRPC, vtysh, or frr.conf. Without FRR: Standalone Linux backend via rtnetlink. diff --git a/package/netd/netd.mk b/package/netd/netd.mk index 60ab4535..8a658af0 100644 --- a/package/netd/netd.mk +++ b/package/netd/netd.mk @@ -4,13 +4,13 @@ # ################################################################################ -NETD_VERSION = 1.0 +NETD_VERSION = 1.1.0 NETD_SITE_METHOD = local NETD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/netd NETD_LICENSE = BSD-3-Clause NETD_LICENSE_FILES = LICENSE NETD_REDISTRIBUTE = NO -NETD_DEPENDENCIES = libite libconfuse jansson +NETD_DEPENDENCIES = libite libconfuse jansson libev NETD_AUTORECONF = YES NETD_CONF_ENV = CFLAGS="$(INFIX_CFLAGS)" diff --git a/src/confd/src/routing.c b/src/confd/src/routing.c index 5bb8b45a..1ef2a5f9 100644 --- a/src/confd/src/routing.c +++ b/src/confd/src/routing.c @@ -8,7 +8,7 @@ #define XPATH_BASE_ "/ietf-routing:routing/control-plane-protocols/control-plane-protocol" #define XPATH_OSPF_ XPATH_BASE_ "/ietf-ospf:ospf" -#define NETD_CONF "/etc/netd/conf.d/confd.conf" +#define NETD_CONF "/etc/net.d/confd.conf" #define NETD_CONF_NEXT NETD_CONF "+" #define NETD_CONF_PREV NETD_CONF "-" #define OSPFD_CONF "/etc/frr/ospfd.conf" diff --git a/src/netd/Makefile.am b/src/netd/Makefile.am index 16803eac..0de3ad9e 100644 --- a/src/netd/Makefile.am +++ b/src/netd/Makefile.am @@ -6,7 +6,7 @@ netd_SOURCES = src/netd.c src/netd.h src/config.c src/config.h netd_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -I$(srcdir)/src netd_CFLAGS = -W -Wall -Wextra netd_CFLAGS += $(libite_CFLAGS) $(libconfuse_CFLAGS) $(jansson_CFLAGS) -netd_LDADD = $(libite_LIBS) $(libconfuse_LIBS) $(jansson_LIBS) +netd_LDADD = $(libite_LIBS) $(libconfuse_LIBS) $(jansson_LIBS) $(EV_LIBS) # Backend selection: FRR gRPC, FRR frr.conf, or Linux kernel if HAVE_FRR_GRPC diff --git a/src/netd/README.md b/src/netd/README.md index 5ebcfc80..aa0aca35 100644 --- a/src/netd/README.md +++ b/src/netd/README.md @@ -60,7 +60,7 @@ make install netd uses [libconfuse](https://github.com/martinh/libconfuse) for configuration parsing, providing a clean and structured format. -Configuration files are placed in `/etc/netd/conf.d/` with the `.conf` extension. Files are processed in alphabetical order. +Configuration files are placed in `/etc/net.d/` with the `.conf` extension. Files are processed in alphabetical order. ### Configuration Format @@ -219,10 +219,10 @@ rip { ### Configuration Files -Configuration files must be placed in `/etc/netd/conf.d/` with the `.conf` extension: +Configuration files must be placed in `/etc/net.d/` with the `.conf` extension: ```bash -/etc/netd/conf.d/ +/etc/net.d/ ├── 10-static.conf # Static routes ├── 20-rip.conf # RIP configuration └── 99-local.conf # Local overrides @@ -256,7 +256,7 @@ netd validates configuration on reload. Check syslog for errors. ``` ┌─────────┐ -│ confd │ Writes /etc/netd/conf.d/confd.conf +│ confd │ Writes /etc/net.d/confd.conf └────┬────┘ │ SIGHUP ▼ diff --git a/src/netd/configure.ac b/src/netd/configure.ac index ca2fd2b1..04b7fa7a 100644 --- a/src/netd/configure.ac +++ b/src/netd/configure.ac @@ -1,5 +1,5 @@ AC_PREREQ(2.61) -AC_INIT([netd], [1.0.0], [https://github.com/kernelkit/infix/issues]) +AC_INIT([netd], [1.1.0], [https://github.com/kernelkit/infix/issues]) AM_INIT_AUTOMAKE(1.11 foreign subdir-objects) AM_SILENT_RULES(yes) @@ -81,6 +81,16 @@ PKG_CHECK_MODULES([libite], [libite >= 2.6.1]) PKG_CHECK_MODULES([libconfuse], [libconfuse >= 3.0]) PKG_CHECK_MODULES([jansson], [jansson >= 2.0]) +AC_CHECK_HEADER([ev.h], + [saved_LIBS="$LIBS" + AC_CHECK_LIB([ev], [ev_loop_new], + [EV_LIBS="-lev"], + [AC_MSG_ERROR("libev not found")]) + LIBS="$saved_LIBS"], + [AC_MSG_ERROR("ev.h not found")] +) +AC_SUBST([EV_LIBS]) + test "x$prefix" = xNONE && prefix=$ac_default_prefix test "x$exec_prefix" = xNONE && exec_prefix='${prefix}' diff --git a/src/netd/src/netd.c b/src/netd/src/netd.c index 3d120f1f..c8f183c1 100644 --- a/src/netd/src/netd.c +++ b/src/netd/src/netd.c @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include "netd.h" @@ -9,9 +11,6 @@ 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; @@ -46,19 +45,7 @@ static int backend_apply(struct route_head *routes, struct rip_config *rip) { } #endif -static void sighup_handler(int sig) -{ - (void)sig; - - INFO("Got SIGHUP, reloading ..."); - do_reload = 1; -} - -static void sigterm_handler(int sig) -{ - (void)sig; - do_shutdown = 1; -} +static ev_timer retry_w; static void route_list_free(struct route_head *head) { @@ -118,7 +105,7 @@ static void rip_config_free(struct rip_config *cfg) } } -static void reload(void) +static void reload(struct ev_loop *loop) { struct route_head new_routes = TAILQ_HEAD_INITIALIZER(new_routes); struct rip_redistribute *redist; @@ -129,7 +116,7 @@ static void reload(void) struct route *r; int count = 0; - INFO("Reloading configuration"); + DEBUG("Reloading configuration"); rip_config_init(&new_rip); @@ -148,11 +135,16 @@ static void reload(void) /* Apply config via backend */ if (backend_apply(&new_routes, &new_rip)) { - ERROR("Failed applying config via backend"); + ERROR("Failed applying config via backend, retry in 5s"); route_list_free(&new_routes); rip_config_free(&new_rip); + ev_timer_stop(loop, &retry_w); + ev_timer_set(&retry_w, 5., 0.); + ev_timer_start(loop, &retry_w); + pidfile(NULL); return; } + ev_timer_stop(loop, &retry_w); route_list_free(&active_routes); TAILQ_INIT(&active_routes); @@ -214,26 +206,59 @@ static void reload(void) } } - INFO("Configuration reloaded"); pidfile(NULL); } +static void inotify_cb(struct ev_loop *loop, ev_io *w, int revents) +{ + char buf[sizeof(struct inotify_event) + NAME_MAX + 1]; + + (void)revents; + while (read(w->fd, buf, sizeof(buf)) > 0) + ; + DEBUG("conf.d changed, triggering reload"); + reload(loop); +} + +static void sighup_cb(struct ev_loop *loop, ev_signal *w, int revents) +{ + (void)w; (void)revents; + INFO("Got SIGHUP, reloading ..."); + reload(loop); +} + +static void sigterm_cb(struct ev_loop *loop, ev_signal *w, int revents) +{ + (void)w; (void)revents; + ev_break(loop, EVBREAK_ALL); +} + +static void retry_cb(struct ev_loop *loop, ev_timer *w, int revents) +{ + (void)w; (void)revents; + reload(loop); +} + static int usage(int rc) { fprintf(stderr, - "Usage: netd [-dh]\n" + "Usage: netd [-dhv]\n" " -d Enable debug (log to stderr)\n" - " -h Show this help text\n"); + " -h Show this help text\n" + " -v Show version and exit\n"); return rc; } int main(int argc, char *argv[]) { + struct ev_loop *loop = EV_DEFAULT; + ev_signal sighup_w, sigterm_w, sigint_w; + ev_io inotify_w; int log_opts = LOG_PID | LOG_NDELAY; - struct sigaction sa = { 0 }; + int ifd = -1; int c; - while ((c = getopt(argc, argv, "dhp:")) != -1) { + while ((c = getopt(argc, argv, "dhv")) != -1) { switch (c) { case 'd': log_opts |= LOG_PERROR; @@ -241,6 +266,9 @@ int main(int argc, char *argv[]) break; case 'h': return usage(0); + case 'v': + puts("v" PACKAGE_VERSION); + return 0; default: return usage(1); } @@ -248,15 +276,7 @@ int main(int argc, char *argv[]) openlog("netd", log_opts, LOG_DAEMON); setlogmask(LOG_UPTO(LOG_INFO)); - INFO("starting"); - - /* Set up signal handlers */ - sa.sa_handler = sighup_handler; - sigaction(SIGHUP, &sa, NULL); - - sa.sa_handler = sigterm_handler; - sigaction(SIGTERM, &sa, NULL); - sigaction(SIGINT, &sa, NULL); + INFO("v%s starting", PACKAGE_VERSION); if (backend_init()) { ERROR("Failed to initialize backend"); @@ -267,19 +287,44 @@ int main(int argc, char *argv[]) TAILQ_INIT(&active_routes); rip_config_init(&active_rip); - /* Initial load */ - do_reload = 1; + /* Signal watchers */ + ev_signal_init(&sighup_w, sighup_cb, SIGHUP); + ev_signal_start(loop, &sighup_w); - while (!do_shutdown) { - if (do_reload) { - do_reload = 0; - reload(); - } - pause(); + ev_signal_init(&sigterm_w, sigterm_cb, SIGTERM); + ev_signal_start(loop, &sigterm_w); + + ev_signal_init(&sigint_w, sigterm_cb, SIGINT); + ev_signal_start(loop, &sigint_w); + + /* Retry timer — one-shot, started only on backend failure */ + ev_timer_init(&retry_w, retry_cb, 0., 0.); + + /* Watch conf.d for changes so we don't rely solely on signals */ + mkdir(CONF_DIR, 0755); + ifd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); + if (ifd < 0) { + ERROR("inotify_init1: %s, falling back to signals only", strerror(errno)); + } else if (inotify_add_watch(ifd, CONF_DIR, + IN_CLOSE_WRITE | IN_DELETE | + IN_MOVED_TO | IN_MOVED_FROM) < 0) { + ERROR("inotify_add_watch %s: %s", CONF_DIR, strerror(errno)); + close(ifd); + ifd = -1; + } else { + ev_io_init(&inotify_w, inotify_cb, ifd, EV_READ); + ev_io_start(loop, &inotify_w); } + /* Initial load */ + reload(loop); + + ev_run(loop, 0); + INFO("shutting down"); + if (ifd >= 0) + close(ifd); route_list_free(&active_routes); rip_config_free(&active_rip); backend_cleanup(); diff --git a/src/netd/src/netd.h b/src/netd/src/netd.h index 99fb4ab2..80df72b4 100644 --- a/src/netd/src/netd.h +++ b/src/netd/src/netd.h @@ -21,7 +21,7 @@ extern int debug; #define INFO(fmt, args...) LOG(LOG_INFO, fmt, ##args) #define DEBUG(fmt, args...) do { if (debug) LOG(LOG_DEBUG, fmt, ##args); } while (0) -#define CONF_DIR "/etc/netd/conf.d" +#define CONF_DIR "/etc/net.d" /* Nexthop types */ enum nh_type { From b4ea26961f9e85bfc061e3d742f97c49537fc617 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 12 Mar 2026 13:53:38 +0100 Subject: [PATCH 2/2] Update ChangeLog for v26.02.1 GA Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 8fd5df4e..b0c536a5 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -3,7 +3,7 @@ Change Log All notable changes to the project are documented in this file. -[v26.02.1][] - 2026-03-11 +[v26.02.1][] - 2026-03-12 ------------------------- ### Changes @@ -17,6 +17,7 @@ All notable changes to the project are documented in this file. - Fix #1389: legacy name limit in firewalld triggered problems with policy names - Fix #1416: `show firewall` command show an error when the firewall is disabled +- Fix #1438: default route from DHCP client not set at boot, regression in v26.02.0 - Fix instabilities in Zebra route manager after Frr upgrade in v26.02.0 - Fix regression in MVEBU SafeXcel Crypto Engine for Marvell Armada SOCs (37xx, 7k, 8k, and CN913x series). Firmware package lost in v26.01.0