diff --git a/board/aarch64/bananapi-bpi-r64/README.md b/board/aarch64/bananapi-bpi-r64/README.md index e4b1ee42d..6c4c3832a 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 e411d462c..168601af4 160000 --- a/buildroot +++ b/buildroot @@ -1 +1 @@ -Subproject commit e411d462cc46b4dd7d64eff527c762c9a04a51f6 +Subproject commit 168601af48567b93bfe0d7bdae3cc8eb266718a5 diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 0dda4878a..b4e0c88d4 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 ------------------------- diff --git a/doc/bridging.md b/doc/bridging.md index c751b90c1..187cb980a 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 diff --git a/doc/iface.md b/doc/iface.md index 3ec252850..9cecd8daa 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/doc/ip.md b/doc/ip.md index 69d105de6..a42302ff4 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/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 e10e66557..000000000 --- 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 1c9bdb721..000000000 --- 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 0925b673c..000000000 --- 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 fa95a00e6..d761d2d60 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 27326b74d..09bea6a26 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 diff --git a/package/mcd/mcd.hash b/package/mcd/mcd.hash index d8575d93e..2c5048418 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 77fa36931..b8478d0b9 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/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 000000000..3835043d3 --- /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 + diff --git a/src/confd/src/if-bridge-mcd.c b/src/confd/src/if-bridge-mcd.c index 7ea3e92a1..bf9a457d4 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); diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index a50879e68..a255c20ea 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 44968f0db..a427872f0 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 1c843195e..a83c10170 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 83bea27e4..6a6505b0f 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" @@ -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 c3af782c8..3e4d32a2a 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-if-type.yang b/src/confd/yang/confd/infix-if-type.yang index f64338ef4..3674376a8 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; diff --git a/src/confd/yang/confd/infix-interfaces.yang b/src/confd/yang/confd/infix-interfaces.yang index 0ace0fe2c..03c9c8a61 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/confd/yang/confd/infix-ip.yang b/src/confd/yang/confd/infix-ip.yang index 094a972ae..8a21675a6 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/bridge.py b/src/statd/python/yanger/ietf_interfaces/bridge.py index 3c3bbd8c7..218962ba9 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 diff --git a/src/statd/python/yanger/ietf_interfaces/common.py b/src/statd/python/yanger/ietf_interfaces/common.py index a2b0b255e..b5ccfc89e 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, netns): 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 f4a1a5fde..924525b8f 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 2a3c95ac7..e5db2e85d 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 000000000..ae32c8412 --- /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 000000000..30d07d133 --- /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 000000000..69f413c0e --- /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 000000000..ebb673d5f --- /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 000000000..ff3d246be --- /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 2831bb680..6ee368cd8 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