From f9ea5567c4481de2a745505332773ff0fc2a0111 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Apr 2026 09:28:14 +0200 Subject: [PATCH 1/8] package/finit: bump to v4.17 https://github.com/finit-project/finit/releases/tag/4.17 Signed-off-by: Joachim Wiberg --- ...-global-path-var-and-fix-memory-corr.patch | 54 --- ...gin-names-to-prevent-subtle-macro-pr.patch | 318 ------------------ ...ndition-before-stopping-rdeps-on-rel.patch | 66 ---- package/finit/finit.hash | 2 +- package/finit/finit.mk | 2 +- 5 files changed, 2 insertions(+), 440 deletions(-) delete mode 100644 package/finit/0001-Remove-redundant-global-path-var-and-fix-memory-corr.patch delete mode 100644 package/finit/0002-Use-explicit-plugin-names-to-prevent-subtle-macro-pr.patch delete mode 100644 package/finit/0003-service-clear-condition-before-stopping-rdeps-on-rel.patch diff --git a/package/finit/0001-Remove-redundant-global-path-var-and-fix-memory-corr.patch b/package/finit/0001-Remove-redundant-global-path-var-and-fix-memory-corr.patch deleted file mode 100644 index e10e6655..00000000 --- a/package/finit/0001-Remove-redundant-global-path-var-and-fix-memory-corr.patch +++ /dev/null @@ -1,54 +0,0 @@ -From 184c079c08387d1b0f74de25b63c56304d2156d0 Mon Sep 17 00:00:00 2001 -From: bazub -Date: Tue, 3 Mar 2026 20:31:33 +0000 -Subject: [PATCH 1/3] Remove redundant global path var and fix memory - corruption -Organization: Wires - -Signed-off-by: Joachim Wiberg ---- - src/conf.c | 13 ++----------- - 1 file changed, 2 insertions(+), 11 deletions(-) - -diff --git a/src/conf.c b/src/conf.c -index d0abb61d..6e8ec834 100644 ---- a/src/conf.c -+++ b/src/conf.c -@@ -121,9 +121,6 @@ static uev_t etcw; - - static TAILQ_HEAD(, conf_change) conf_change_list = TAILQ_HEAD_INITIALIZER(conf_change_list); - --static char *path; --static char *shell; -- - static int parse_conf(char *file, int is_rcsd); - static void drop_changes(void); - -@@ -377,8 +374,6 @@ void conf_parse_cmdline(int argc, char *argv[]) - fstab = strdup(ptr); - finit_conf = strdup(FINIT_CONF); - finit_rcsd = strdup(FINIT_RCSD); -- path = getenv("PATH"); -- shell = getenv("SHELL"); - - for (int i = 1; i < argc; i++) - parse_arg(argv[i]); -@@ -404,13 +399,9 @@ void conf_reset_env(void) - free(node); - } - -- if (path) -- setenv("PATH", path, 1); -- else -+ if (!getenv("PATH")) - setenv("PATH", _PATH_STDPATH, 1); -- if (shell) -- setenv("SHELL", shell, 1); -- else -+ if (!getenv("SHELL")) - setenv("SHELL", _PATH_BSHELL, 1); - setenv("LOGNAME", "root", 1); - setenv("USER", "root", 1); --- -2.43.0 - diff --git a/package/finit/0002-Use-explicit-plugin-names-to-prevent-subtle-macro-pr.patch b/package/finit/0002-Use-explicit-plugin-names-to-prevent-subtle-macro-pr.patch deleted file mode 100644 index 1c9bdb72..00000000 --- a/package/finit/0002-Use-explicit-plugin-names-to-prevent-subtle-macro-pr.patch +++ /dev/null @@ -1,318 +0,0 @@ -From 662293e194811213b9b387163dfe8499e3300ccf Mon Sep 17 00:00:00 2001 -From: bazub -Date: Fri, 6 Mar 2026 20:36:23 +0000 -Subject: [PATCH 2/3] Use explicit plugin names to prevent subtle macro - processing bugs -Organization: Wires - -Signed-off-by: Joachim Wiberg ---- - plugins/alsa-utils.c | 6 +++--- - plugins/bootmisc.c | 2 +- - plugins/dbus.c | 4 ++-- - plugins/hook-scripts.c | 2 +- - plugins/modprobe.c | 2 +- - plugins/modules-load.c | 2 +- - plugins/netlink.c | 2 +- - plugins/pidfile.c | 2 +- - plugins/procps.c | 4 ++-- - plugins/resolvconf.c | 2 +- - plugins/rtc.c | 6 +++--- - plugins/sys.c | 2 +- - plugins/tty.c | 1 + - plugins/urandom.c | 6 +++--- - plugins/usr.c | 2 +- - plugins/x11-common.c | 4 ++-- - 16 files changed, 25 insertions(+), 24 deletions(-) - -diff --git a/plugins/alsa-utils.c b/plugins/alsa-utils.c -index 6b2c3603..a8a967b0 100644 ---- a/plugins/alsa-utils.c -+++ b/plugins/alsa-utils.c -@@ -38,7 +38,7 @@ - static void save(void *arg) - { - if (rescue) { -- dbg("Skipping %s plugin in rescue mode.", __FILE__); -+ dbg("Skipping %s plugin in rescue mode.", "alsa-utils"); - return; - } - -@@ -51,7 +51,7 @@ static void save(void *arg) - static void restore(void *arg) - { - if (rescue) { -- dbg("Skipping %s plugin in rescue mode.", __FILE__); -+ dbg("Skipping %s plugin in rescue mode.", "alsa-utils"); - return; - } - -@@ -62,7 +62,7 @@ static void restore(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "alsa-utils", - .hook[HOOK_BASEFS_UP] = { .cb = restore }, - .hook[HOOK_SHUTDOWN] = { .cb = save } - }; -diff --git a/plugins/bootmisc.c b/plugins/bootmisc.c -index 701f73f0..a8ba3808 100644 ---- a/plugins/bootmisc.c -+++ b/plugins/bootmisc.c -@@ -172,7 +172,7 @@ static void setup(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "bootmisc", - .hook[HOOK_MOUNT_POST] = { .cb = clean }, - .hook[HOOK_BASEFS_UP] = { .cb = setup }, - .depends = { "pidfile" }, -diff --git a/plugins/dbus.c b/plugins/dbus.c -index bbf55bc2..a8a155a1 100644 ---- a/plugins/dbus.c -+++ b/plugins/dbus.c -@@ -106,7 +106,7 @@ static void setup(void *arg) - char *cmd; - - if (rescue) { -- dbg("Skipping %s plugin in rescue mode.", __FILE__); -+ dbg("Skipping %s plugin in rescue mode.", "dbus"); - return; - } - -@@ -164,7 +164,7 @@ static void setup(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "dbus", - .hook[HOOK_SVC_PLUGIN] = { .cb = setup }, - }; - -diff --git a/plugins/hook-scripts.c b/plugins/hook-scripts.c -index 9aa78173..e75808c5 100644 ---- a/plugins/hook-scripts.c -+++ b/plugins/hook-scripts.c -@@ -79,7 +79,7 @@ static void hscript_shutdown(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "hook-scripts", - .hook[HOOK_BANNER] = { .cb = hscript_banner }, - .hook[HOOK_ROOTFS_UP] = { .cb = hscript_rootfs_up }, - .hook[HOOK_MOUNT_ERROR] = { .cb = hscript_mount_error }, -diff --git a/plugins/modprobe.c b/plugins/modprobe.c -index b6b7e7bb..e52bf228 100644 ---- a/plugins/modprobe.c -+++ b/plugins/modprobe.c -@@ -228,7 +228,7 @@ static void coldplug(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "modprobe", - .hook[HOOK_BASEFS_UP] = { .cb = coldplug }, - .depends = { "bootmisc", } - }; -diff --git a/plugins/modules-load.c b/plugins/modules-load.c -index 53aef82f..e2e45c72 100644 ---- a/plugins/modules-load.c -+++ b/plugins/modules-load.c -@@ -213,7 +213,7 @@ static void load(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "modules-load", - .hook[HOOK_SVC_PLUGIN] = { .cb = load }, - }; - -diff --git a/plugins/netlink.c b/plugins/netlink.c -index a70b01e8..625cbd1b 100644 ---- a/plugins/netlink.c -+++ b/plugins/netlink.c -@@ -426,7 +426,7 @@ static void nl_enumerate(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "netlink", - .hook[HOOK_SVC_RECONF] = { .cb = nl_reconf }, - .hook[HOOK_SVC_PLUGIN] = { .cb = nl_enumerate }, - .io = { -diff --git a/plugins/pidfile.c b/plugins/pidfile.c -index ae0fdeea..f1232ab6 100644 ---- a/plugins/pidfile.c -+++ b/plugins/pidfile.c -@@ -335,7 +335,7 @@ static void pidfile_init(void *arg) - * SIGSTP:ed (in state PAUSED) waiting for . - */ - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "pidfile", - .hook[HOOK_BASEFS_UP] = { .cb = pidfile_init }, - .hook[HOOK_SVC_RECONF] = { .cb = pidfile_reconf }, - .depends = { "netlink" }, /* bootmisc depends on us */ -diff --git a/plugins/procps.c b/plugins/procps.c -index 826e38ad..5a178d75 100644 ---- a/plugins/procps.c -+++ b/plugins/procps.c -@@ -43,7 +43,7 @@ static void setup(void *arg) - glob_t gl; - - if (rescue) { -- dbg("Skipping %s plugin in rescue mode.", __FILE__); -+ dbg("Skipping %s plugin in rescue mode.", "procps"); - return; - } - -@@ -69,7 +69,7 @@ static void setup(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "procps", - .hook[HOOK_BASEFS_UP] = { - .cb = setup - }, -diff --git a/plugins/resolvconf.c b/plugins/resolvconf.c -index 1ac29dae..b98e9214 100644 ---- a/plugins/resolvconf.c -+++ b/plugins/resolvconf.c -@@ -51,7 +51,7 @@ static void setup(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "resolvconf", - .hook[HOOK_BASEFS_UP] = { - .cb = setup - }, -diff --git a/plugins/rtc.c b/plugins/rtc.c -index faf69415..cf734763 100644 ---- a/plugins/rtc.c -+++ b/plugins/rtc.c -@@ -236,7 +236,7 @@ static void rtc_save(void *arg) - int fd, rc = 0; - - if (rescue) { -- dbg("Skipping %s plugin in rescue mode.", __FILE__); -+ dbg("Skipping %s plugin in rescue mode.", "rtc"); - return; - } - -@@ -266,7 +266,7 @@ static void rtc_restore(void *arg) - int fd, rc = 0; - - if (rescue) { -- dbg("Skipping %s plugin in rescue mode.", __FILE__); -+ dbg("Skipping %s plugin in rescue mode.", "rtc"); - return; - } - -@@ -321,7 +321,7 @@ static void update(uev_t *w, void *arg, int events) - - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "rtc", - .hook[HOOK_BASEFS_UP] = { - .cb = rtc_restore - }, -diff --git a/plugins/sys.c b/plugins/sys.c -index 438fdfd5..484adc05 100644 ---- a/plugins/sys.c -+++ b/plugins/sys.c -@@ -180,7 +180,7 @@ static void sys_init(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "sys", - .hook[HOOK_BASEFS_UP] = { .cb = sys_init }, - .depends = { "bootmisc", }, - }; -diff --git a/plugins/tty.c b/plugins/tty.c -index b3255f9f..c6a750b0 100644 ---- a/plugins/tty.c -+++ b/plugins/tty.c -@@ -43,6 +43,7 @@ - static void tty_watcher(void *arg, int fd, int events); - - static plugin_t plugin = { -+ .name = "tty", - .io = { - .cb = tty_watcher, - .flags = PLUGIN_IO_READ, -diff --git a/plugins/urandom.c b/plugins/urandom.c -index 410d0660..9bb6252b 100644 ---- a/plugins/urandom.c -+++ b/plugins/urandom.c -@@ -87,7 +87,7 @@ static void setup(void *arg) - int fd, err; - - if (rescue) { -- dbg("Skipping %s plugin in rescue mode.", __FILE__); -+ dbg("Skipping %s plugin in rescue mode.", "urandom"); - return; - } - -@@ -188,7 +188,7 @@ static void save(void *arg) - mode_t prev; - - if (rescue) { -- dbg("Skipping %s plugin in rescue mode.", __FILE__); -+ dbg("Skipping %s plugin in rescue mode.", "urandom"); - return; - } - -@@ -202,7 +202,7 @@ static void save(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "urandom", - .hook[HOOK_BASEFS_UP] = { .cb = setup }, - .hook[HOOK_SHUTDOWN] = { .cb = save }, - .depends = { "bootmisc", } -diff --git a/plugins/usr.c b/plugins/usr.c -index d30d9440..496facc2 100644 ---- a/plugins/usr.c -+++ b/plugins/usr.c -@@ -104,7 +104,7 @@ static void usr_init(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "usr", - .hook[HOOK_BASEFS_UP] = { .cb = usr_init }, - .depends = { "bootmisc", }, - }; -diff --git a/plugins/x11-common.c b/plugins/x11-common.c -index f65331a0..75825a36 100644 ---- a/plugins/x11-common.c -+++ b/plugins/x11-common.c -@@ -39,7 +39,7 @@ - static void setup(void *arg) - { - if (rescue) { -- dbg("Skipping %s plugin in rescue mode.", __FILE__); -+ dbg("Skipping %s plugin in rescue mode.", "x11-common"); - return; - } - -@@ -48,7 +48,7 @@ static void setup(void *arg) - } - - static plugin_t plugin = { -- .name = __FILE__, -+ .name = "x11-common", - .hook[HOOK_SVC_PLUGIN] = { .cb = setup }, - }; - --- -2.43.0 - diff --git a/package/finit/0003-service-clear-condition-before-stopping-rdeps-on-rel.patch b/package/finit/0003-service-clear-condition-before-stopping-rdeps-on-rel.patch deleted file mode 100644 index 0925b673..00000000 --- a/package/finit/0003-service-clear-condition-before-stopping-rdeps-on-rel.patch +++ /dev/null @@ -1,66 +0,0 @@ -From c01faef99b7e4ff9c39f29ad5648db61a4742539 Mon Sep 17 00:00:00 2001 -From: Joachim Wiberg -Date: Thu, 19 Mar 2026 06:37:50 +0100 -Subject: [PATCH 3/3] service: clear condition before stopping rdeps on reload -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -Organization: Wires - -When a service without SIGHUP reload support (noreload) is touched and -'initctl reload' is called, service_update_rdeps() correctly identifies -its reverse dependencies but only marks them dirty. It does not clear -the service's condition, so when service_step_all() runs: - - - rdeps supporting SIGHUP hit the sm_in_reload() guard and break early, - left running while their dependency is being killed. - - rdeps without SIGHUP support may receive SIGTERM too late, after the - dependency has already died and broken their connection, causing them - to exit from RUNNING state and have their restart counter incremented. - -Fix by calling cond_clear() on the service's condition immediately in -service_update_rdeps(), before service_step_all() runs. cond_clear() -calls cond_update() which calls service_step() inline on all affected -services, which see COND_OFF and transition to STOPPING_STATE — all -before SIGTERM is ever sent to the dependency itself. - -This mirrors the pattern already used in api.c:do_reload() for direct -'initctl reload ' calls. - -Fixes: avahi/mdns stop causing mdns-alias restart counter increment - -Signed-off-by: Joachim Wiberg ---- - src/service.c | 16 +++++++++++++++- - 1 file changed, 15 insertions(+), 1 deletion(-) - -diff --git a/src/service.c b/src/service.c -index b2c6c15b..016b5d2f 100644 ---- a/src/service.c -+++ b/src/service.c -@@ -2418,7 +2418,21 @@ void service_update_rdeps(void) - if (!svc_is_noreload(svc)) - continue; /* Yup, no need to stop start rdeps */ - -- svc_mark_affected(mkcond(svc, cond, sizeof(cond))); -+ /* -+ * Clear the condition immediately, before service_step_all() -+ * runs. cond_clear() calls cond_update() which calls -+ * service_step() on all affected services right now. Those -+ * services see COND_OFF and get service_stop() called, -+ * transitioning to STOPPING_STATE before we ever send SIGTERM -+ * to this service. Without this, the condition is only cleared -+ * after the service dies, by which time reverse-dependencies -+ * may have already crashed due to the lost connection. -+ * See also: api.c do_reload() which does the same for direct -+ * 'initctl reload ' calls. -+ */ -+ mkcond(svc, cond, sizeof(cond)); -+ cond_clear(cond); -+ svc_mark_affected(cond); - } - } - --- -2.43.0 - diff --git a/package/finit/finit.hash b/package/finit/finit.hash index fa95a00e..d761d2d6 100644 --- a/package/finit/finit.hash +++ b/package/finit/finit.hash @@ -1,5 +1,5 @@ # From https://github.com/troglobit/finit/releases/ -sha256 f0894cd8b79ce030fdc656600208ddb0b994364f86ebad066de2e005e5e18a35 finit-4.16.tar.gz +sha256 ca8b489a0ed99c17c0288e9e65e325a8f14af3734dfecf58f516c0a6f884e903 finit-4.17.tar.gz # Locally calculated sha256 868cb6c5414933a48db11186042cfe65c87480d326734bc6cf0e4b19b4a2e52a LICENSE diff --git a/package/finit/finit.mk b/package/finit/finit.mk index 27326b74..09bea6a2 100644 --- a/package/finit/finit.mk +++ b/package/finit/finit.mk @@ -4,7 +4,7 @@ # ################################################################################ -FINIT_VERSION = 4.16 +FINIT_VERSION = 4.17 FINIT_SITE = https://github.com/troglobit/finit/releases/download/$(FINIT_VERSION) FINIT_LICENSE = MIT FINIT_LICENSE_FILES = LICENSE From b1ad0f16565d2d52a4fd11dbdcb0e83dbb111846 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 29 Apr 2026 16:22:47 +0200 Subject: [PATCH 2/8] package/mcd: bump and add support for per-bridge mcast router ports Fixes #395 Signed-off-by: Joachim Wiberg --- package/mcd/mcd.hash | 2 +- package/mcd/mcd.mk | 2 +- src/confd/yang/confd.inc | 2 +- src/confd/yang/confd/infix-if-bridge.yang | 11 +++++++++++ ...025-10-28.yang => infix-if-bridge@2026-04-29.yang} | 0 src/confd/yang/confd/infix-interfaces.yang | 5 +++++ ...26-04-09.yang => infix-interfaces@2026-04-29.yang} | 0 src/statd/python/yanger/ietf_interfaces/bridge.py | 10 ++++++++++ 8 files changed, 29 insertions(+), 3 deletions(-) rename src/confd/yang/confd/{infix-if-bridge@2025-10-28.yang => infix-if-bridge@2026-04-29.yang} (100%) rename src/confd/yang/confd/{infix-interfaces@2026-04-09.yang => infix-interfaces@2026-04-29.yang} (100%) diff --git a/package/mcd/mcd.hash b/package/mcd/mcd.hash index d8575d93..2c504841 100644 --- a/package/mcd/mcd.hash +++ b/package/mcd/mcd.hash @@ -1,2 +1,2 @@ -sha256 dcd639c77689c432e84e63267202c00958fb1032e066ae355e3cbc68d9fcd063 mcd-2.3.tar.gz +sha256 896c455c1a013a153cbf3186b5f87b7a3344b8939603c0f09e534de714a32e65 mcd-2.4.tar.gz sha256 99a0480db163445c5a1a5bf7f85bc37cbb5be8c40e9e441291ad78e8d4252d0b LICENSE diff --git a/package/mcd/mcd.mk b/package/mcd/mcd.mk index 77fa3693..b8478d0b 100644 --- a/package/mcd/mcd.mk +++ b/package/mcd/mcd.mk @@ -4,7 +4,7 @@ # ################################################################################ -MCD_VERSION = 2.3 +MCD_VERSION = 2.4 MCD_SITE = https://github.com/kernelkit/mcd/releases/download/v$(MCD_VERSION) MCD_LICENSE = BSD-3-Clause MCD_LICENSE_FILES = LICENSE diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 83bea27e..a3c64651 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -47,7 +47,7 @@ MODULES=( "ieee802-ethernet-interface@2019-06-21.yang" "infix-ethernet-interface@2024-02-27.yang" "infix-factory-default@2023-06-28.yang" - "infix-interfaces@2026-04-09.yang -e vlan-filtering" + "infix-interfaces@2026-04-29.yang -e vlan-filtering" "ietf-crypto-types -e cleartext-symmetric-keys" "infix-crypto-types@2026-02-14.yang" "ietf-keystore -e symmetric-keys" diff --git a/src/confd/yang/confd/infix-if-bridge.yang b/src/confd/yang/confd/infix-if-bridge.yang index c3af782c..3e4d32a2 100644 --- a/src/confd/yang/confd/infix-if-bridge.yang +++ b/src/confd/yang/confd/infix-if-bridge.yang @@ -29,6 +29,11 @@ submodule infix-if-bridge { contact "kernelkit@googlegroups.com"; description "Linux bridge extension for ietf-interfaces."; + revision 2026-04-29 { + description "Add operational state for multicast router ports per bridge."; + reference "internal"; + } + revision 2025-10-28 { description "Prevent IP addresses on bridge ports."; reference "internal"; @@ -389,6 +394,12 @@ submodule infix-if-bridge { } default 125; } + + leaf-list router-ports { + config false; + description "Ports with detected multicast routers, learned by IGMP/MLD snooping."; + type if:interface-ref; + } } } diff --git a/src/confd/yang/confd/infix-if-bridge@2025-10-28.yang b/src/confd/yang/confd/infix-if-bridge@2026-04-29.yang similarity index 100% rename from src/confd/yang/confd/infix-if-bridge@2025-10-28.yang rename to src/confd/yang/confd/infix-if-bridge@2026-04-29.yang diff --git a/src/confd/yang/confd/infix-interfaces.yang b/src/confd/yang/confd/infix-interfaces.yang index 0ace0fe2..03c9c8a6 100644 --- a/src/confd/yang/confd/infix-interfaces.yang +++ b/src/confd/yang/confd/infix-interfaces.yang @@ -41,6 +41,11 @@ module infix-interfaces { contact "kernelkit@googlegroups.com"; description "Linux bridge and lag extensions for ietf-interfaces."; + revision 2026-04-29 { + description "Add operational state for multicast router ports per bridge."; + reference "internal"; + } + revision 2026-04-09 { description "Add ptp-capabilities submodule for per-interface PTP timestamping info."; reference "internal"; diff --git a/src/confd/yang/confd/infix-interfaces@2026-04-09.yang b/src/confd/yang/confd/infix-interfaces@2026-04-29.yang similarity index 100% rename from src/confd/yang/confd/infix-interfaces@2026-04-09.yang rename to src/confd/yang/confd/infix-interfaces@2026-04-29.yang diff --git a/src/statd/python/yanger/ietf_interfaces/bridge.py b/src/statd/python/yanger/ietf_interfaces/bridge.py index 3c3bbd8c..218962ba 100644 --- a/src/statd/python/yanger/ietf_interfaces/bridge.py +++ b/src/statd/python/yanger/ietf_interfaces/bridge.py @@ -218,6 +218,13 @@ def mctl(ifname, vid, mctldata): return {} +def mctl_router_ports(brname, mctldata): + for entry in mctldata.get("multicast-router-ports", []): + if entry.get("bridge") == brname: + return entry.get("ports", []) + return [] + + def multicast_filters(iplink, vid): filt = ["dev", iplink["ifname"]] + (["vid", str(vid)] if vid else []) brmdb = HOST.run_json(["bridge", "-j", "mdb", "show"] + filt)[0]["mdb"] @@ -253,6 +260,9 @@ def multicast(iplink, info, mctldata): if interval := mctlq.get("interval"): mcast["query-interval"] = interval + if ports := mctl_router_ports(iplink["ifname"], mctldata): + mcast["router-ports"] = ports + return mcast From 7dd746d586e5e07252f816ccc830c1181bafeaba Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Apr 2026 16:11:19 +0200 Subject: [PATCH 3/8] Fix BPi-R64 onboard WiFi probing The mt7622 soc has a built-in wmac, same driver as the mt7615, but it lacked MODULE_DEVICE_TABLE() so udev could not autoload driver and firmware. This commit also adds the wmac+bluetooth specific firmware. Signed-off-by: Joachim Wiberg --- board/aarch64/bananapi-bpi-r64/README.md | 17 ++++++++-- buildroot | 2 +- ...-add-MODULE_DEVICE_TABLE-for-mt7622-.patch | 31 +++++++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 patches/linux/6.18.25/0049-wifi-mt76-mt7615-add-MODULE_DEVICE_TABLE-for-mt7622-.patch diff --git a/board/aarch64/bananapi-bpi-r64/README.md b/board/aarch64/bananapi-bpi-r64/README.md index e4b1ee42..6c4c3832 100644 --- a/board/aarch64/bananapi-bpi-r64/README.md +++ b/board/aarch64/bananapi-bpi-r64/README.md @@ -14,7 +14,7 @@ The Banana Pi BPI-R64 is a networking board based on the MediaTek MT7622 - 8 GB eMMC storage - microSD card slot - MT7531 Gigabit Ethernet switch (4x LAN + 1x WAN) -- MT7603E built-in 2.4 GHz WiFi +- MT7622 WMAC built-in 2.4 GHz 802.11ac WiFi - USB 3.0 port - 2x Mini PCIe slots @@ -24,7 +24,7 @@ Infix comes preconfigured with: - **LAN ports** (lan0-lan3): Bridged for internal networking - **WAN port**: DHCP client enabled for internet connectivity -- **WiFi** (wifi0-ap): Bridged to LAN (MT7615 PCIe card if fitted, otherwise MT7603E) +- **WiFi** (wifi0-ap): Bridged to LAN (MT7622 WMAC, 2.4 GHz; or MT7615 PCIe card if fitted) ## Boot Switch Reference @@ -113,6 +113,19 @@ mmc partconf 0 1 1 0 ## Platform Notes +### WiFi + +The MT7622 SoC includes a built-in WMAC that provides 2.4 GHz 802.11b/g/n/ac +(2×2) — there is no 5 GHz capability from the onboard radio. + +For dual-band operation, a PCIe card can be fitted in one of the two Mini PCIe +slots. The [Banana Pi BPI-MT7615][bpi-mt7615] is a purpose-built dual-band +(2.4 + 5 GHz, 802.11ac 4×4) module designed for BPI router boards and is a +natural fit. With it installed, Infix will use it in preference to the onboard +WMAC for the `wifi0-ap` interface. + +[bpi-mt7615]: https://docs.banana-pi.org/en/BPI-MT7615/BananaPi_MT7615 + ### mmc0 = eMMC, mmc1 = SD On MT7622, MSDC0 (mmc0) is the 8-bit eMMC controller and MSDC1 (mmc1) is the diff --git a/buildroot b/buildroot index e411d462..168601af 160000 --- a/buildroot +++ b/buildroot @@ -1 +1 @@ -Subproject commit e411d462cc46b4dd7d64eff527c762c9a04a51f6 +Subproject commit 168601af48567b93bfe0d7bdae3cc8eb266718a5 diff --git a/patches/linux/6.18.25/0049-wifi-mt76-mt7615-add-MODULE_DEVICE_TABLE-for-mt7622-.patch b/patches/linux/6.18.25/0049-wifi-mt76-mt7615-add-MODULE_DEVICE_TABLE-for-mt7622-.patch new file mode 100644 index 00000000..3835043d --- /dev/null +++ b/patches/linux/6.18.25/0049-wifi-mt76-mt7615-add-MODULE_DEVICE_TABLE-for-mt7622-.patch @@ -0,0 +1,31 @@ +From 925bc823c4f4ece91c090371a358134e238e7cc4 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Tue, 28 Apr 2026 15:30:01 +0200 +Subject: [PATCH] wifi: mt76: mt7615: add MODULE_DEVICE_TABLE for mt7622 wmac +Organization: Wires + +Without MODULE_DEVICE_TABLE(of, ...) the OF compatible alias is never +exported to modules.alias, so udev cannot autoload mt7615e when the +mediatek,mt7622-wmac platform device is probed. Add the export so the +module loads automatically on BPI-R64 and similar MT7622-based boards. + +Signed-off-by: Joachim Wiberg +--- + drivers/net/wireless/mediatek/mt76/mt7615/soc.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/drivers/net/wireless/mediatek/mt76/mt7615/soc.c b/drivers/net/wireless/mediatek/mt76/mt7615/soc.c +index 06a0f2a141e8..538ea1dc333b 100644 +--- a/drivers/net/wireless/mediatek/mt76/mt7615/soc.c ++++ b/drivers/net/wireless/mediatek/mt76/mt7615/soc.c +@@ -56,6 +56,7 @@ static const struct of_device_id mt7622_wmac_of_match[] = { + { .compatible = "mediatek,mt7622-wmac" }, + {}, + }; ++MODULE_DEVICE_TABLE(of, mt7622_wmac_of_match); + + struct platform_driver mt7622_wmac_driver = { + .driver = { +-- +2.43.0 + From 6aed1f7df4f6027e41a92ebaf3627793e757452e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Apr 2026 17:50:37 +0200 Subject: [PATCH 4/8] confd: add support for static ARP and neighbor cache Add support for static ARP (IPv4) and neighbor cache (IPv6) entries per interface. Static entries are installed as permanent kernel neighbor table entries that are never evicted by normal ARP/NDP aging. Fixes #819 Signed-off-by: Joachim Wiberg --- doc/ip.md | 60 +++++++++++++++ src/confd/src/interfaces.c | 2 + src/confd/src/interfaces.h | 2 + src/confd/src/ip.c | 73 +++++++++++++++++++ src/confd/yang/confd.inc | 2 +- src/confd/yang/confd/infix-ip.yang | 11 +-- ...25-11-02.yang => infix-ip@2026-04-28.yang} | 0 .../python/yanger/ietf_interfaces/common.py | 14 ++++ src/statd/python/yanger/ietf_interfaces/ip.py | 63 +++++++++++++++- test/case/interfaces/all.yaml | 3 + .../interfaces/neighbor_cache/Readme.adoc | 1 + test/case/interfaces/neighbor_cache/test.adoc | 25 +++++++ test/case/interfaces/neighbor_cache/test.py | 63 ++++++++++++++++ .../interfaces/neighbor_cache/topology.dot | 24 ++++++ .../interfaces/neighbor_cache/topology.svg | 42 +++++++++++ test/infamy/iface.py | 22 ++++++ 16 files changed, 397 insertions(+), 10 deletions(-) rename src/confd/yang/confd/{infix-ip@2025-11-02.yang => infix-ip@2026-04-28.yang} (100%) create mode 120000 test/case/interfaces/neighbor_cache/Readme.adoc create mode 100644 test/case/interfaces/neighbor_cache/test.adoc create mode 100755 test/case/interfaces/neighbor_cache/test.py create mode 100644 test/case/interfaces/neighbor_cache/topology.dot create mode 100644 test/case/interfaces/neighbor_cache/topology.svg diff --git a/doc/ip.md b/doc/ip.md index 69d105de..a42302ff 100644 --- a/doc/ip.md +++ b/doc/ip.md @@ -435,6 +435,66 @@ admin@example:/config/interface/eth0/> leave admin@example:/> +## ARP and Neighbor Cache + +Static ARP entries (IPv4) and neighbor cache entries (IPv6) can be +configured per interface. The most common reasons to do so are: + +- **Security** — prevent ARP/NDP spoofing by locking critical hosts + (e.g., a default gateway) to their known MAC addresses +- **Reliability** — ensure reachability of a host even when ARP/NDP + traffic is suppressed or filtered (e.g., across a strict firewall) + +Dynamic entries are learned automatically by the kernel using ARP and +Neighbor Discovery Protocol (NDP), and are visible as read-only +operational state alongside the static ones. + +### Static IPv4 ARP Entry + +
admin@example:/> configure
+admin@example:/config/> edit interface eth0 ipv4
+admin@example:/config/interface/eth0/ipv4/> set neighbor 192.168.1.100 link-layer-address 00:11:22:33:44:55
+admin@example:/config/interface/eth0/ipv4/> diff
++interfaces {
++  interface eth0 {
++    ipv4 {
++      neighbor 192.168.1.100 {
++        link-layer-address 00:11:22:33:44:55;
++      }
++    }
++  }
++}
+admin@example:/config/interface/eth0/ipv4/> leave
+admin@example:/>
+
+ +### Static IPv6 Neighbor Entry + +
admin@example:/> configure
+admin@example:/config/> edit interface eth0 ipv6
+admin@example:/config/interface/eth0/ipv6/> set neighbor 2001:db8::100 link-layer-address 00:11:22:33:44:55
+admin@example:/config/interface/eth0/ipv6/> diff
++interfaces {
++  interface eth0 {
++    ipv6 {
++      neighbor 2001:db8::100 {
++        link-layer-address 00:11:22:33:44:55;
++      }
++    }
++  }
++}
+admin@example:/config/interface/eth0/ipv6/> leave
+admin@example:/>
+
+ +The full neighbor table — including dynamically learned entries (origin: +*dynamic*) — is available as operational state via NETCONF or RESTCONF. + +> [!NOTE] +> Static neighbor entries take effect immediately on `leave` (commit). +> They are installed as *permanent* entries in the kernel neighbor table, +> which means they are never evicted by the normal ARP/NDP aging process. + [1]: https://www.rfc-editor.org/rfc/rfc3442 [2]: https://www.rfc-editor.org/rfc/rfc8344 [3]: https://www.rfc-editor.org/rfc/rfc8981 diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index a50879e6..a255c20e 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -681,6 +681,8 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net err = err ? : netdag_gen_link_addr(ip, cif, dif); err = err ? : netdag_gen_ip_addrs(net, ip, "ipv4", cif, dif); err = err ? : netdag_gen_ip_addrs(net, ip, "ipv6", cif, dif); + err = err ? : netdag_gen_ip_neighs(net, ip, "ipv4", cif, dif); + err = err ? : netdag_gen_ip_neighs(net, ip, "ipv6", cif, dif); if (err) goto err_close_ip; diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h index 44968f0d..a427872f 100644 --- a/src/confd/src/interfaces.h +++ b/src/confd/src/interfaces.h @@ -114,6 +114,8 @@ int netdag_gen_ipv4_autoconf(struct dagger *net, struct lyd_node *cif, struct lyd_node *dif); int netdag_gen_ip_addrs(struct dagger *net, FILE *ip, const char *proto, struct lyd_node *cif, struct lyd_node *dif); +int netdag_gen_ip_neighs(struct dagger *net, FILE *ip, const char *proto, + struct lyd_node *cif, struct lyd_node *dif); /* if-bridge.c */ int bridge_mstpd_gen(struct lyd_node *cifs); diff --git a/src/confd/src/ip.c b/src/confd/src/ip.c index 1c843195..a83c1017 100644 --- a/src/confd/src/ip.c +++ b/src/confd/src/ip.c @@ -242,6 +242,79 @@ static int netdag_set_conf_addrs(FILE *ip, const char *ifname, return 0; } +static int netdag_gen_diff_neigh(FILE *ip, const char *ifname, + struct lyd_node *neigh) +{ + enum lydx_op op = lydx_get_op(neigh); + struct lyd_node *addr, *lladdr; + struct lydx_diff addrd, lladrd; + + addr = lydx_get_child(neigh, "ip"); + if (!addr) + return -EINVAL; + + lydx_get_diff(addr, &addrd); + + if (op == LYDX_OP_DELETE) { + fprintf(ip, "neigh del %s dev %s\n", addrd.old, ifname); + return 0; + } + + lladdr = lydx_get_child(neigh, "link-layer-address"); + if (!lladdr) + return -EINVAL; + + lydx_get_diff(lladdr, &lladrd); + fprintf(ip, "neigh replace %s lladdr %s dev %s nud permanent\n", + addrd.new, lladrd.new, ifname); + return 0; +} + +static int netdag_set_conf_neighs(FILE *ip, const char *ifname, + struct lyd_node *ipvx) +{ + struct lyd_node *neigh; + + LYX_LIST_FOR_EACH(lyd_child(ipvx), neigh, "neighbor") { + fprintf(ip, "neigh replace %s lladdr %s dev %s nud permanent\n", + lydx_get_cattr(neigh, "ip"), + lydx_get_cattr(neigh, "link-layer-address"), + ifname); + } + + return 0; +} + +int netdag_gen_ip_neighs(struct dagger *net, FILE *ip, const char *proto, + struct lyd_node *cif, struct lyd_node *dif) +{ + struct lyd_node *ipconf = lydx_get_child(cif, proto); + struct lyd_node *ipdiff = lydx_get_child(dif, proto); + const char *ifname = lydx_get_cattr(dif, "name"); + struct lyd_node *neigh; + int err = 0; + + if (!ipconf || !lydx_is_enabled(ipconf, "enabled")) { + FILE *fp = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_PRE, "flush-neigh.sh"); + if (fp) { + fprintf(fp, "ip -%c neigh flush dev %s nud permanent\n", proto[3], ifname); + fclose(fp); + } + return 0; + } + + if (lydx_get_op(lydx_get_child(ipdiff, "enabled")) == LYDX_OP_REPLACE) + return netdag_set_conf_neighs(ip, ifname, ipconf); + + LYX_LIST_FOR_EACH(lyd_child(ipdiff), neigh, "neighbor") { + err = netdag_gen_diff_neigh(ip, ifname, neigh); + if (err) + break; + } + + return err; +} + int netdag_gen_ip_addrs(struct dagger *net, FILE *ip, const char *proto, struct lyd_node *cif, struct lyd_node *dif) { diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index a3c64651..6a6505b0 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -29,7 +29,7 @@ MODULES=( "ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor" "infix-hardware@2026-02-08.yang" "ieee802-dot1q-types@2022-10-29.yang" - "infix-ip@2025-11-02.yang" + "infix-ip@2026-04-28.yang" "infix-if-type@2026-01-07.yang" "infix-routing@2026-03-11.yang" "ieee802-dot1ab-lldp@2022-03-15.yang" diff --git a/src/confd/yang/confd/infix-ip.yang b/src/confd/yang/confd/infix-ip.yang index 094a972a..8a21675a 100644 --- a/src/confd/yang/confd/infix-ip.yang +++ b/src/confd/yang/confd/infix-ip.yang @@ -18,6 +18,10 @@ module infix-ip { description "This module augments ietf-ip with Infix extensions and deviations."; + revision 2026-04-28 { + description "Add support for ARP and neighbor cache (ietf-ip neighbor lists)."; + reference "Internal, issue #819."; + } revision 2025-11-02 { description "Change autoconf to presence container, removing enabled leaf."; reference "Internal, issue #1109."; @@ -65,15 +69,8 @@ module infix-ip { deviate not-supported; } - deviation "/if:interfaces/if:interface/ip:ipv4/ip:neighbor" { - deviate not-supported; - } - deviation "/if:interfaces/if:interface/ip:ipv6/ip:address/ip:status" { deviate not-supported; } - deviation "/if:interfaces/if:interface/ip:ipv6/ip:neighbor" { - deviate not-supported; - } } diff --git a/src/confd/yang/confd/infix-ip@2025-11-02.yang b/src/confd/yang/confd/infix-ip@2026-04-28.yang similarity index 100% rename from src/confd/yang/confd/infix-ip@2025-11-02.yang rename to src/confd/yang/confd/infix-ip@2026-04-28.yang diff --git a/src/statd/python/yanger/ietf_interfaces/common.py b/src/statd/python/yanger/ietf_interfaces/common.py index a2b0b255..b5ccfc89 100644 --- a/src/statd/python/yanger/ietf_interfaces/common.py +++ b/src/statd/python/yanger/ietf_interfaces/common.py @@ -28,3 +28,17 @@ def ipaddrs(ifname=None, netns=None): return HOST.run_json(pre + ["ip", "-j", "addr", "show"] + filt) return { addr["ifname"]: addr for addr in _ipaddrs(ifname, netns) } + + +@cache +def ipneighs(ifname=None, netns=None): + def _ipneighs(ifname, netns): + pre = ["ip", "netns", "exec", netns] if netns else [] + filt = ["dev", ifname] if ifname else [] + return HOST.run_json(pre + ["ip", "-j", "neigh", "show"] + filt, []) + + result = {} + for e in _ipneighs(ifname, netns): + if dev := e.get("dev"): + result.setdefault(dev, []).append(e) + return result diff --git a/src/statd/python/yanger/ietf_interfaces/ip.py b/src/statd/python/yanger/ietf_interfaces/ip.py index f4a1a5fd..924525b8 100644 --- a/src/statd/python/yanger/ietf_interfaces/ip.py +++ b/src/statd/python/yanger/ietf_interfaces/ip.py @@ -1,4 +1,55 @@ +import ipaddress + from ..host import HOST +from . import common + + +def neigh_state(states): + xlate = { + "REACHABLE": "reachable", + "STALE": "stale", + "DELAY": "delay", + "PROBE": "probe", + "INCOMPLETE": "incomplete", + } + return next((xlate[s] for s in states if s in xlate), None) + + +def neighbors(ifname, family): + result = [] + for entry in common.ipneighs().get(ifname, []): + dst = entry.get("dst", "") + try: + version = ipaddress.ip_address(dst).version + except ValueError: + continue + + if version != (4 if family == "inet" else 6): + continue + + lladdr = entry.get("lladdr") + states = entry.get("state", []) + + if not lladdr: + continue + + origin = "static" if "PERMANENT" in states else "dynamic" + neigh = { + "ip": dst, + "link-layer-address": lladdr, + "origin": origin, + } + + if family == "inet6": + if state := neigh_state(states): + neigh["state"] = state + if entry.get("router"): + neigh["is-router"] = [None] + + result.append(neigh) + + return result + def inet2yang_origin(inet): """Translate kernel IP address origin to YANG""" @@ -34,23 +85,31 @@ def addresses(ipaddr, proto): def ipv4(ipaddr): ipv4 = {} + ifname = ipaddr.get("ifname") mtu = ipaddr.get("mtu") - if mtu and ipaddr.get("ifname") != "lo": + if mtu and ifname != "lo": ipv4["mtu"] = mtu if addrs := addresses(ipaddr, "inet"): ipv4["address"] = addrs + if neighs := neighbors(ifname, "inet"): + ipv4["neighbor"] = neighs + return ipv4 def ipv6(ipaddr): ipv6 = {} + ifname = ipaddr.get("ifname") - if mtu := HOST.read(f"/proc/sys/net/ipv6/conf/{ipaddr['ifname']}/mtu"): + if mtu := HOST.read(f"/proc/sys/net/ipv6/conf/{ifname}/mtu"): ipv6["mtu"] = int(mtu.strip()) if addrs := addresses(ipaddr, "inet6"): ipv6["address"] = addrs + if neighs := neighbors(ifname, "inet6"): + ipv6["neighbor"] = neighs + return ipv6 diff --git a/test/case/interfaces/all.yaml b/test/case/interfaces/all.yaml index 2a3c95ac..e5db2e85 100644 --- a/test/case/interfaces/all.yaml +++ b/test/case/interfaces/all.yaml @@ -5,6 +5,9 @@ - name: Interface with IPv4 case: ipv4_address/test.py +- name: ARP and Neighbor Cache + case: neighbor_cache/test.py + - name: Interface IPv6 Autoconf for Bridges case: ipv6_address/test.py diff --git a/test/case/interfaces/neighbor_cache/Readme.adoc b/test/case/interfaces/neighbor_cache/Readme.adoc new file mode 120000 index 00000000..ae32c841 --- /dev/null +++ b/test/case/interfaces/neighbor_cache/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/neighbor_cache/test.adoc b/test/case/interfaces/neighbor_cache/test.adoc new file mode 100644 index 00000000..30d07d13 --- /dev/null +++ b/test/case/interfaces/neighbor_cache/test.adoc @@ -0,0 +1,25 @@ +=== ARP and Neighbor Cache + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/neighbor_cache] + +==== Description + +Verify that static ARP entries (IPv4) and neighbor cache entries (IPv6) +can be configured on an interface and are immediately visible in the +operational datastore with origin "static". Also verify that removing +the entries causes them to disappear from the operational datastore. + +==== Topology + +image::topology.svg[ARP and Neighbor Cache topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Configure static IPv4 ARP and IPv6 neighbor entries on target:data +. Verify static IPv4 ARP entry is visible in operational state +. Verify static IPv6 neighbor entry is visible in operational state +. Remove static neighbor entries by clearing IPv4 and IPv6 config +. Verify static neighbor entries are no longer present + + diff --git a/test/case/interfaces/neighbor_cache/test.py b/test/case/interfaces/neighbor_cache/test.py new file mode 100755 index 00000000..69f413c0 --- /dev/null +++ b/test/case/interfaces/neighbor_cache/test.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +ARP and Neighbor Cache + +Verify that static ARP entries (IPv4) and neighbor cache entries (IPv6) +can be configured on an interface and are immediately visible in the +operational datastore with origin "static". Also verify that removing +the entries causes them to disappear from the operational datastore. +""" +import infamy +import infamy.iface as iface + +from infamy.util import until + +IPV4_NEIGH = "192.0.2.1" +IPV4_LLADR = "de:ad:be:ef:ca:fe" +IPV6_NEIGH = "2001:db8::1" +IPV6_LLADR = "de:ad:be:ef:ca:ff" + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + _, tport = env.ltop.xlate("target", "data") + + with test.step("Configure static IPv4 ARP and IPv6 neighbor entries on target:data"): + target.put_config_dict("ietf-interfaces", { + "interfaces": { + "interface": [{ + "name": tport, + "ipv4": { + "neighbor": [{ + "ip": IPV4_NEIGH, + "link-layer-address": IPV4_LLADR, + }] + }, + "ipv6": { + "neighbor": [{ + "ip": IPV6_NEIGH, + "link-layer-address": IPV6_LLADR, + }] + } + }] + } + }) + + with test.step("Verify static IPv4 ARP entry is visible in operational state"): + until(lambda: iface.neighbor_exist(target, tport, IPV4_NEIGH, IPV4_LLADR, "static")) + + with test.step("Verify static IPv6 neighbor entry is visible in operational state"): + until(lambda: iface.neighbor_exist(target, tport, IPV6_NEIGH, IPV6_LLADR, "static")) + + with test.step("Remove static neighbor entries by clearing IPv4 and IPv6 config"): + target.delete_xpath( + f"/ietf-interfaces:interfaces/interface[name='{tport}']/ietf-ip:ipv4") + target.delete_xpath( + f"/ietf-interfaces:interfaces/interface[name='{tport}']/ietf-ip:ipv6") + + with test.step("Verify static neighbor entries are no longer present"): + until(lambda: not iface.neighbor_exist(target, tport, IPV4_NEIGH)) + until(lambda: not iface.neighbor_exist(target, tport, IPV6_NEIGH)) + + test.succeed() diff --git a/test/case/interfaces/neighbor_cache/topology.dot b/test/case/interfaces/neighbor_cache/topology.dot new file mode 100644 index 00000000..ebb673d5 --- /dev/null +++ b/test/case/interfaces/neighbor_cache/topology.dot @@ -0,0 +1,24 @@ +graph "1x2" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data } | target", + pos="10,12!", + + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey] + host:data -- target:data [color=black] +} diff --git a/test/case/interfaces/neighbor_cache/topology.svg b/test/case/interfaces/neighbor_cache/topology.svg new file mode 100644 index 00000000..ff3d246b --- /dev/null +++ b/test/case/interfaces/neighbor_cache/topology.svg @@ -0,0 +1,42 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + diff --git a/test/infamy/iface.py b/test/infamy/iface.py index 2831bb68..6ee368cd 100644 --- a/test/infamy/iface.py +++ b/test/infamy/iface.py @@ -108,6 +108,28 @@ def is_oper_up(target, iface): return get_oper_status(target, iface) == "up" +def _get_neighbors(target, iface, proto): + interface = target.get_iface(iface) + if interface is None: + return None + ip = interface.get(proto) or interface.get(f"ietf-ip:{proto}") + return ip.get("neighbor") if ip else None + + +def neighbor_exist(target, iface, address, lladdr=None, origin=None): + """Check if neighbor 'address' exists on iface, optionally matching lladdr and origin""" + for proto in ("ipv4", "ipv6"): + for n in _get_neighbors(target, iface, proto) or []: + if n.get("ip") != address: + continue + if lladdr and n.get("link-layer-address") != lladdr: + continue + if origin and n.get("origin") != origin: + continue + return True + return False + + def exist_bridge_multicast_filter(target, group, iface, bridge): """Check if a bridge has a multicast filter for group with iface""" # The interface array is different in restconf/netconf, netconf has From 4d8b2e537063756c47c094f0ffbacd6184526828 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Apr 2026 19:18:16 +0200 Subject: [PATCH 5/8] confd: warn if multicast flooding is disabled Fixes #520 Signed-off-by: Joachim Wiberg --- src/confd/src/if-bridge-mcd.c | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/confd/src/if-bridge-mcd.c b/src/confd/src/if-bridge-mcd.c index 7ea3e92a..bf9a457d 100644 --- a/src/confd/src/if-bridge-mcd.c +++ b/src/confd/src/if-bridge-mcd.c @@ -12,6 +12,56 @@ #include "interfaces.h" +static void warn_mcast_flood_port(struct lyd_node *port_cif) +{ + struct lyd_node *bp, *flood; + + bp = lydx_get_child(port_cif, "bridge-port"); + if (!bp) + return; + + flood = lydx_get_child(bp, "flood"); + if (!lydx_is_enabled(flood, "multicast")) + WARN("%s: multicast flood disabled, topology changes will black-hole multicast until IGMP/MLD rejoin", + lydx_get_cattr(port_cif, "name")); +} + +static void warn_mcast_flood_bridge(struct lyd_node *cif, const char *brname) +{ + struct ly_set *ports; + uint32_t i; + + ports = lydx_find_xpathf(cif, "../interface[bridge-port/bridge='%s']", brname); + if (!ports) + return; + + for (i = 0; i < ports->count; i++) + warn_mcast_flood_port(ports->dnodes[i]); + + ly_set_free(ports, NULL); +} + +static void warn_mcast_flood_vlan(struct lyd_node *cif, struct lyd_node *vlan) +{ + static const char *modes[] = { "tagged", "untagged", NULL }; + struct lyd_node *portentry; + struct ly_set *set; + const char **mode; + + for (mode = modes; *mode; mode++) { + LYX_LIST_FOR_EACH(lyd_child(vlan), portentry, *mode) { + set = lydx_find_xpathf(cif, "../interface[name='%s']", + lyd_get_value(portentry)); + if (!set || !set->count) { + ly_set_free(set, NULL); + continue; + } + warn_mcast_flood_port(set->dnodes[0]); + ly_set_free(set, NULL); + } + } +} + static int gen_vlan(struct lyd_node *cif, struct lyd_node *vlan, FILE *conf) { const char *iface, *querier, *upper; @@ -26,6 +76,8 @@ static int gen_vlan(struct lyd_node *cif, struct lyd_node *vlan, FILE *conf) if (!strcmp(querier, "off")) return 0; + warn_mcast_flood_vlan(cif, vlan); + interval = atoi(lydx_get_cattr(mcast, "query-interval")); iface = lydx_get_cattr(cif, "name"); @@ -66,6 +118,8 @@ static int gen_bridge(struct lyd_node *cif, FILE *conf) interval = atoi(lydx_get_cattr(mcast, "query-interval")); + warn_mcast_flood_bridge(cif, iface); + fprintf(conf, "iface %s enable %s igmpv3 query-interval %d\n", iface, !strcmp(querier, "proxy") ? "proxy-queries" : "", interval); From 624bfd927cada4eb9fc2b5fc5ad93bb4e912f4c8 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Apr 2026 18:33:03 +0200 Subject: [PATCH 6/8] doc: add section on static mutlicast filters Fixes #790 Signed-off-by: Joachim Wiberg --- doc/bridging.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/doc/bridging.md b/doc/bridging.md index c751b90c..187cb980 100644 --- a/doc/bridging.md +++ b/doc/bridging.md @@ -183,6 +183,48 @@ In this setup we have a lot more going on. Multiple multicast router ports have been detected, and behind the scenes someone has also added an IGMP/MLD fast-leave port. +### Static Multicast Filters + +When IGMP/MLD snooping is in use, traffic for an unregistered group is +flooded to all ports until a receiver joins. For MAC multicast groups, +or for groups where snooping cannot learn membership automatically, you +can add static entries to the MDB that immediately restrict forwarding +to a given set of ports. + +> [!NOTE] +> Snooping must be enabled on the bridge (or per VLAN) before static +> multicast filters can be configured. + +On a plain (non-VLAN) bridge, add a static IPv4 or MAC multicast filter +like this: + +
admin@example:/> configure
+admin@example:/config/> edit interface br0
+admin@example:/config/interface/br0/> set bridge multicast-filters multicast-filter 224.1.1.1 ports e2
+admin@example:/config/interface/br0/> set bridge multicast-filters multicast-filter 224.1.1.1 ports e3
+admin@example:/config/interface/br0/> set bridge multicast-filters multicast-filter 01:00:5e:01:01:01 ports e2
+admin@example:/config/interface/br0/> leave
+admin@example:/> copy running-config startup-config
+
+ +Each `ports` entry for the same group adds one port to the filter. +Receivers on all other ports will not see traffic for that group. + +On a VLAN-filtering bridge the filter is scoped per VLAN: + +
admin@example:/config/interface/br1/> set bridge vlans vlan 10 multicast-filters multicast-filter 224.2.2.2 ports e5
+admin@example:/config/interface/br1/> set bridge vlans vlan 10 multicast-filters multicast-filter 224.2.2.2 ports e6
+
+ +To verify the MDB — both statically configured and dynamically learned +entries — use: + +
admin@example:/> show bridge mdb
+BRIDGE   VID  GROUP                 PORTS                              
+br0           224.1.1.1             e2, e3
+br0           01:00:5e:01:01:01     e2
+
+ ### Terminology & Abbreviations - **IGMP**: Internet Group Membership Protocol, multicast subscription From 5defdfdeace15b856caba18f0ee1c3b274947540 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Apr 2026 18:41:04 +0200 Subject: [PATCH 7/8] doc: add section on dummy interfaces Fixes #769 Signed-off-by: Joachim Wiberg --- doc/iface.md | 43 +++++++++++++++++++++++++ src/confd/yang/confd/infix-if-type.yang | 4 ++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/doc/iface.md b/doc/iface.md index 3ec25285..9cecd8da 100644 --- a/doc/iface.md +++ b/doc/iface.md @@ -138,6 +138,49 @@ admin@example:/config/interface/veth0a/> set custom-phys-address chassis offs +## Dummy Interface + +A dummy interface is a virtual interface that is always administratively +and operationally UP, regardless of any physical link state. It can +hold IP addresses just like any other interface. + +The two most common uses are: + +- **Stable OSPF router-ID**: OSPF picks its router-ID from an interface + address. If that interface goes down, adjacencies can flap. Binding + the router-ID to a /32 address on a dummy avoids this. +- **Stable management address**: A /32 on a dummy gives the device a + permanent identity on the network, reachable as long as at least one + uplink is up and the address is redistributed into the routing domain. + +> [!TIP] +> WiFi interfaces also use dummies as placeholders when the radio +> hardware is not detected at boot (e.g., a USB dongle that was +> unplugged). See [WiFi](wifi.md) for details. + +### Example: Stable OSPF Router-ID + +Create a dummy interface with a /32 address and use it as the OSPF +router-ID so that the ID never changes when physical ports bounce: + +
admin@example:/> configure
+admin@example:/config/> edit interface lo0
+admin@example:/config/interface/lo0/> set type dummy
+admin@example:/config/interface/lo0/> set ipv4 address 192.0.2.1 prefix-length 32
+admin@example:/config/interface/lo0/> leave
+admin@example:/config/> edit routing control-plane-protocol ospfv2 name default ospf
+admin@example:/config/routing/…/ospf/> set explicit-router-id 192.0.2.1
+admin@example:/config/routing/…/ospf/> leave
+admin@example:/> copy running-config startup-config
+
+ +To also make the address reachable by other routers, redistribute +connected routes (or add `lo0` as an OSPF interface): + +
admin@example:/config/routing/…/ospf/> set redistribute connected
+
+ + [^1]: A YANG deviation was previously used to make it possible to set `phys-address`, but this has been replaced with the more flexible `custom-phys-address`. diff --git a/src/confd/yang/confd/infix-if-type.yang b/src/confd/yang/confd/infix-if-type.yang index f64338ef..3674376a 100644 --- a/src/confd/yang/confd/infix-if-type.yang +++ b/src/confd/yang/confd/infix-if-type.yang @@ -68,7 +68,9 @@ module infix-if-type { identity dummy { base infix-interface-type; base ianaift:other; - description "Linux dummy interface. Useful mostly for testing."; + description "Linux virtual interface that is always UP. Useful for + stable loopback-style addresses, e.g., as a permanent + management address or OSPF router-ID source."; } identity ethernet { base infix-interface-type; From 25fcfba0cb09ca96f6c97c55e4a4a8777539e727 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Apr 2026 10:33:52 +0200 Subject: [PATCH 8/8] doc: update ChangeLog for v26.04.0 release [skip ci] Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 0dda4878..b4e0c88d 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -3,29 +3,55 @@ Change Log All notable changes to the project are documented in this file. -[v26.04.0][UNRELEASED] - +[v26.04.0][] - 2026-04-30 ------------------------- ### Changes - Upgrade Linux kernel to 6.18.25 (LTS) - Upgrade Buildroot to 2025.02.13 (LTS) +- Add support for per-bridge multicast router port in operational, issue #395 +- Add support for static ARP (IPv4) and neighbor cache (IPv6) entries per + interface, issue #819. Static entries are installed as permanent kernel + neighbor table entries that are never evicted by normal ARP/NDP aging - Add support for PTP/gPTP (IEEE 1588-2019 / 802.1AS) clock synchronization. Supported clock types: Ordinary Clock, Boundary Clock, and Transparent Clock. See the User Guide for configuration details - Add support for [Banana Pi BPI-R4][BPI-R4], quad-core Cortex-A73 router with 4x 2.5 GbE switching, dual 10 GbE SFP+. Variants BPI-R4-2g5 and BPI-R4P have one SFP+ replaced by a 2.5 GbE RJ45, with optional PoE on the R4P +- Update [Marvell ESPRESSObin][ESPRESSObin] board support. Allow booting with + stock U-Boot, which only supports ext4 rootfs partitions; to use, apply the + `ext4` developer snippet before building (`make apply-ext4 all`) +- Fix onboard WiFi support on the Banana Pi BPi-R64 ### Fixes +- Fix #520: warn in syslog if multicast flooding is disabled +- Fix #769: document dummy interfaces in user guide +- Fix #790: document static multicast filters in user guide +- Fix #1439: changing hostname does not regenerate DHCP client conf until restart - Fix #1458: `show ntp tracking` displaying a truncated Reference ID, e.g., `92.2` instead of `92.246.137.39` - Fix #1466: `show container` showing no output for containers whose command line includes environment variables -- Fix #1439: changing hostname does not regenerate DHCP client conf until restart +- Fix issue with IGMP queries sent with all-zeroes source MAC address +- Fix missing IGMP query startup burst when assuming IGMP querier role, as + defined in RFC3376 §8.6/§8.7 +- Fix Raspberry Pi 4 and Pi 400 display instability after soft reboot. + Previously the touchscreen/DSI display required a full power cycle to + reinitialise correctly; it now works reliably after `reboot`. Please note, + you need a fairly up-to-date EEPROM version as well +- Fix [BPI-R4][] board README showing inverted DIP switch values for eMMC and + SPI NAND boot modes, which would prevent the board from booting correctly +- Fix [SAMA7G54][] U-Boot build system selection that caused build failures +- Fix [BPI-R3][] PCIe devices failing to initialize on boot due to a missing + clock definition in the device tree +[BPI-R3]: https://wiki.banana-pi.org/Banana_Pi_BPI-R3 [BPI-R4]: https://docs.banana-pi.org/en/BPI-R4/BananaPi_BPI-R4 +[ESPRESSObin]: https://espressobin.net/ +[SAMA7G54]: https://www.microchip.com/en-us/development-tool/ev21h18a [v26.03.0][] - 2026-03-31 -------------------------