From 717c1c02f107d7f978171e4e49c2ca3a250c4e70 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 20:40:17 +0000 Subject: [PATCH 1/6] confd: use force_forwarding sysctl for IPv6 Introduced in Linux 6.17, the force_forwding flag corresponds to the ipv4 forwarding flag, which maps perfectly to the ietf-ip.yang model. Fixes #515 Signed-off-by: Joachim Wiberg --- src/confd/src/interfaces.c | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index ddf426052..86639ebdb 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -337,7 +337,7 @@ static int netdag_gen_sysctl(struct dagger *net, node = lydx_get_descendant(lyd_child(dif), "ipv6", "forwarding", NULL); err = err ? : netdag_gen_sysctl_setting(net, ifname, &sysctl, 1, "0", node, - "net.ipv6.conf.%s.forwarding", ifname); + "net.ipv6.conf.%s.force_forwarding", ifname); if (!strcmp(ifname, "lo")) /* skip for now */ goto skip_mtu; @@ -353,39 +353,6 @@ static int netdag_gen_sysctl(struct dagger *net, return err; } -/* - * The global IPv6 forwarding lever is off by default, enabled when any - * interface has IPv6 forwarding enabled. - */ -static int netdag_ipv6_forwarding(struct lyd_node *cifs, struct dagger *net) -{ - struct lyd_node *cif; - FILE *sysctl = NULL; - int ena = 0; - - LYX_LIST_FOR_EACH(cifs, cif, "interface") - ena |= lydx_is_enabled(lydx_get_child(cif, "ipv6"), "forwarding"); - - if (ena) - sysctl = dagger_fopen_next(net, "init", "@post", NETDAG_INIT_POST, "ipv6.sysctl"); - else - sysctl = dagger_fopen_current(net, "exit", "@pre", NETDAG_EXIT_PRE, "ipv6.sysctl"); - if (!sysctl) { - /* - * Cannot create exit code in gen: -1. Safe to ignore - * since ipv6 forwarding is disabled by default. - */ - if (dagger_is_bootstrap(net) && !ena) - return 0; - return -EIO; - } - - fprintf(sysctl, "net.ipv6.conf.all.forwarding = %d\n", ena); - fclose(sysctl); - - return 0; -} - static int dummy_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip) { const char *ifname = lydx_get_cattr(cif, "name"); @@ -782,9 +749,6 @@ static sr_error_t ifchange_post(sr_session_ctx_t *session, struct dagger *net, { int err = 0; - /* Figure out value of global IPv6 forwarding flag. Issue #785 */ - err |= netdag_ipv6_forwarding(cifs, net); - /* For each configured bridge, the corresponding multicast * querier settings depend on both the bridge config and on * the presence of matching VLAN uppers. Since these can be From 129f92afdd5d3b94be7b86704f824c0b86547406 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 20:48:24 +0000 Subject: [PATCH 2/6] statd: use IPv6 force_forwarding in operational state Signed-off-by: Joachim Wiberg --- src/statd/python/yanger/ietf_routing.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/statd/python/yanger/ietf_routing.py b/src/statd/python/yanger/ietf_routing.py index 024442438..9d7b1d9b9 100644 --- a/src/statd/python/yanger/ietf_routing.py +++ b/src/statd/python/yanger/ietf_routing.py @@ -125,7 +125,7 @@ def add_protocol(routes, proto): def get_routing_interfaces(): - """Get list of interfaces with IPv4 forwarding enabled""" + """Get list of interfaces with IPv4 or IPv6 forwarding enabled""" import json # Get all interfaces @@ -139,11 +139,12 @@ def get_routing_interfaces(): continue # Check if IPv4 forwarding is enabled - # Note: We only check IPv4 forwarding. IPv6 forwarding behaves differently - # and will be handled separately when Linux 6.17+ force_forwarding is available. ipv4_fwd = HOST.run(tuple(['sysctl', '-n', f'net.ipv4.conf.{ifname}.forwarding']), default="0").strip() - if ipv4_fwd == "1": + # Check if IPv6 force_forwarding is enabled (available since Linux 6.17) + ipv6_fwd = HOST.run(tuple(['sysctl', '-n', f'net.ipv6.conf.{ifname}.force_forwarding']), default="0").strip() + + if ipv4_fwd == "1" or ipv6_fwd == "1": routing_ifaces.append(ifname) return routing_ifaces From db0c6417b282ccc0d9a9abc511f979aac3aa081d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 22:06:45 +0000 Subject: [PATCH 3/6] test: add IPv6 forwarding verification to routing-basic Signed-off-by: Joachim Wiberg --- test/case/interfaces/Readme.adoc | 2 +- test/case/interfaces/routing_basic/test.adoc | 15 ++- test/case/interfaces/routing_basic/test.py | 117 +++++++++--------- .../interfaces/routing_basic/topology.dot | 6 +- .../interfaces/routing_basic/topology.svg | 60 ++++----- 5 files changed, 101 insertions(+), 99 deletions(-) diff --git a/test/case/interfaces/Readme.adoc b/test/case/interfaces/Readme.adoc index ad2d20f87..4e1c8b372 100644 --- a/test/case/interfaces/Readme.adoc +++ b/test/case/interfaces/Readme.adoc @@ -7,7 +7,7 @@ Tests verifying interface configuration and management: - IPv4 and IPv6 address assignment and management - IPv4 address auto-configuration mechanisms - Interface aliases and physical address configuration - - Basic routing table configuration and operation + - IPv4 and IPv6 forwarding between interfaces - Linux bridge creation, STP, and VLAN handling - Link aggregation (LAG) setup and failover behavior - IGMP multicast group management and forwarding diff --git a/test/case/interfaces/routing_basic/test.adoc b/test/case/interfaces/routing_basic/test.adoc index fb1ce7aad..1f896230f 100644 --- a/test/case/interfaces/routing_basic/test.adoc +++ b/test/case/interfaces/routing_basic/test.adoc @@ -4,8 +4,11 @@ ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/routing_basic] ==== Description -Verify routing between interfaces is possible. That enable/disable routing -in configuration has the expected result. +Verify that the ietf-ip forwarding setting controls whether IPv4 +and IPv6 traffic is routed between interfaces. When forwarding is +enabled, hosts on separate subnets can reach each other through +the device. When forwarding is disabled, that connectivity is +expected to be lost. ==== Topology @@ -14,11 +17,11 @@ image::topology.svg[Routing basic topology, align=center, scaledwidth=75%] ==== Sequence . Set up topology and attach to target DUTs -. Setup host +. Set up host addresses and default routes . Enable forwarding on target:data1 and target:data2 -. Verify ping from host:data1 to 10.0.0.10 -. Verify ping from host:data2 to 192.168.0.10 +. Verify cross-subnet IPv4 connectivity +. Verify cross-subnet IPv6 connectivity . Disable forwarding on target:data1 and target:data2 -. Verify ping does not work host:data1 to 10.0.0.10 and host:data2 to 192.168.0.10 +. Verify cross-subnet connectivity is lost diff --git a/test/case/interfaces/routing_basic/test.py b/test/case/interfaces/routing_basic/test.py index 01e2ae3f9..636e3f859 100755 --- a/test/case/interfaces/routing_basic/test.py +++ b/test/case/interfaces/routing_basic/test.py @@ -1,58 +1,53 @@ #!/usr/bin/env python3 -# ,------------------------------------------------, -# | [TARGET] | -# | | -# | | -# | target:mgmt target:data0 targetgt:data1 | -# | [192.168.0.1] [10.0.0.1] | -# '------------------------------------------------' -# | | | -# | | | -# ,----------------------------------------------, -# | host:mgmt host:data0 host:data1 | -# | [192.168.0.10] [10.0.0.10] | -# | (ns0) (ns1) | -# | | -# | [ HOST ] | -# '----------------------------------------------' - """ Routing basic -Verify routing between interfaces is possible. That enable/disable routing -in configuration has the expected result. +Verify that the ietf-ip forwarding setting controls whether IPv4 +and IPv6 traffic is routed between interfaces. When forwarding is +enabled, hosts on separate subnets can reach each other through +the device. When forwarding is disabled, that connectivity is +expected to be lost. """ import infamy +SUBNETS = [ + {"ipv4": {"gw": "192.168.0.1", "host": "192.168.0.10", "prefix": 24}, + "ipv6": {"gw": "2001:db8:0::1", "host": "2001:db8:0::10", "prefix": 64}}, + {"ipv4": {"gw": "10.0.0.1", "host": "10.0.0.10", "prefix": 24}, + "ipv6": {"gw": "2001:db8:1::1", "host": "2001:db8:1::10", "prefix": 64}}, +] + +def iface_cfg(port, subnet, enable_fwd): + """Build interface config with both IPv4 and IPv6.""" + cfg = {"name": port, "enabled": True} + for family in ("ipv4", "ipv6"): + af = subnet[family] + cfg[family] = { + "forwarding": enable_fwd, + "address": [{"ip": af["gw"], "prefix-length": af["prefix"]}], + } + return cfg + def config_target(target, tport0, tport1, enable_fwd): + """Configure forwarding and addresses for both address families.""" target.put_config_dict("ietf-interfaces", { - "interfaces": { - "interface": [ - { - "name": tport0, - "enabled": True, - "ipv4": { - "forwarding": enable_fwd, - "address": [{ - "ip": "192.168.0.1", - "prefix-length": 24 - }] - } - }, - { - "name": tport1, - "enabled": True, - "ipv4": { - "forwarding": enable_fwd, - "address": [{ - "ip": "10.0.0.1", - "prefix-length": 24 - }] - } - } - ] - } - }) + "interfaces": { + "interface": [ + iface_cfg(tport0, SUBNETS[0], enable_fwd), + iface_cfg(tport1, SUBNETS[1], enable_fwd), + ] + } + }) + +def setup_host(ns, subnet): + """Add IPv4 and IPv6 addresses and default routes.""" + v4 = subnet["ipv4"] + ns.addip(v4["host"]) + ns.addroute("default", v4["gw"]) + + v6 = subnet["ipv6"] + ns.addip(v6["host"], prefix_length=v6["prefix"], proto="ipv6") + ns.addroute("default", v6["gw"], proto="ipv6") with infamy.Test() as test: with test.step("Set up topology and attach to target DUTs"): @@ -65,29 +60,31 @@ def config_target(target, tport0, tport1, enable_fwd): _, hport1 = env.ltop.xlate("host", "data2") with infamy.IsolatedMacVlan(hport0) as ns0, \ - infamy.IsolatedMacVlan(hport1) as ns1 : - - with test.step("Setup host"): - ns0.addip("192.168.0.10") - ns0.addroute("default", "192.168.0.1") + infamy.IsolatedMacVlan(hport1) as ns1: - ns1.addip("10.0.0.10") - ns1.addroute("default", "10.0.0.1") + with test.step("Set up host addresses and default routes"): + setup_host(ns0, SUBNETS[0]) + setup_host(ns1, SUBNETS[1]) with test.step("Enable forwarding on target:data1 and target:data2"): config_target(target, tport0, tport1, True) - with test.step("Verify ping from host:data1 to 10.0.0.10"): - ns0.must_reach("10.0.0.10") + with test.step("Verify cross-subnet IPv4 connectivity"): + ns0.must_reach(SUBNETS[1]["ipv4"]["host"]) + ns1.must_reach(SUBNETS[0]["ipv4"]["host"]) - with test.step("Verify ping from host:data2 to 192.168.0.10"): - ns1.must_reach("192.168.0.10") + with test.step("Verify cross-subnet IPv6 connectivity"): + ns0.must_reach(SUBNETS[1]["ipv6"]["host"]) + ns1.must_reach(SUBNETS[0]["ipv6"]["host"]) with test.step("Disable forwarding on target:data1 and target:data2"): config_target(target, tport0, tport1, False) - with test.step("Verify ping does not work host:data1 to 10.0.0.10 and host:data2 to 192.168.0.10"): - infamy.parallel(lambda: ns0.must_not_reach("10.0.0.10"), - lambda: ns1.must_not_reach("192.168.0.10")) + with test.step("Verify cross-subnet connectivity is lost"): + infamy.parallel( + lambda: ns0.must_not_reach(SUBNETS[1]["ipv4"]["host"]), + lambda: ns1.must_not_reach(SUBNETS[0]["ipv4"]["host"]), + lambda: ns0.must_not_reach(SUBNETS[1]["ipv6"]["host"]), + lambda: ns1.must_not_reach(SUBNETS[0]["ipv6"]["host"])) test.succeed() diff --git a/test/case/interfaces/routing_basic/topology.dot b/test/case/interfaces/routing_basic/topology.dot index d83b37809..25d3b8c70 100644 --- a/test/case/interfaces/routing_basic/topology.dot +++ b/test/case/interfaces/routing_basic/topology.dot @@ -19,6 +19,6 @@ graph "routing_basic" { ]; host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] - host:data1 -- target:data1 [color=black, fontcolor=black, fontsize=12, taillabel=".10", label="192.168.0.0/24", headlabel=".1"] - host:data2 -- target:data2 [color=black, fontcolor=black, fontsize=12, taillabel=".10", label="10.0.0.0/24", headlabel=".1"] -} \ No newline at end of file + host:data1 -- target:data1 [color=black, fontcolor=black, fontsize=12, taillabel=".10", label="192.168.0.0/24\n2001:db8:0::/64", headlabel=".1"] + host:data2 -- target:data2 [color=black, fontcolor=black, fontsize=12, taillabel=".10", label="10.0.0.0/24\n2001:db8:1::/64", headlabel=".1"] +} diff --git a/test/case/interfaces/routing_basic/topology.svg b/test/case/interfaces/routing_basic/topology.svg index 3c706e87a..581d8c768 100644 --- a/test/case/interfaces/routing_basic/topology.svg +++ b/test/case/interfaces/routing_basic/topology.svg @@ -3,55 +3,57 @@ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> - - + + routing_basic - + host - -host - -mgmt - -data1 - -data2 + +host + +mgmt + +data1 + +data2 target - -mgmt - -data1 - -data2 - -target + +mgmt + +data1 + +data2 + +target host:mgmt--target:mgmt - + host:data1--target:data1 - -192.168.0.0/24 -.1 -.10 + +192.168.0.0/24 +2001:db8:0::/64 +.1 +.10 host:data2--target:data2 - -10.0.0.0/24 -.1 -.10 + +10.0.0.0/24 +2001:db8:1::/64 +.1 +.10 From 4f876bc58edba735c3e81cbd8a01d4ac5f8a937d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 30 Jan 2026 07:57:34 +0100 Subject: [PATCH 4/6] test: update nacm basic test spec. Signed-off-by: Joachim Wiberg --- test/case/system/nacm-basic/test.adoc | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/test/case/system/nacm-basic/test.adoc b/test/case/system/nacm-basic/test.adoc index 62626851c..d18b8d16b 100644 --- a/test/case/system/nacm-basic/test.adoc +++ b/test/case/system/nacm-basic/test.adoc @@ -5,21 +5,20 @@ ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/nacm-basic] ==== Description Test that NACM groups (admin, operator, guest) correctly enforce -access control with path-based rules and default policies. +access control with permissive defaults and targeted denials. -Creates three user privilege levels from scratch: +The NACM design is "permit by default, deny sensitive items": -- admin: Full access (permit-all rule) -- operator: Can manage interfaces/routing, cannot modify system config -- guest: Read-only access (write-default: deny, exec deny rule) +- admin: Full unrestricted access (permit-all rule) +- operator: Can configure everything EXCEPT passwords, keystore, truststore +- guest: Read-only access (explicit deny of create/update/delete/exec) Verifies that: -- All users can read configuration (read-default: permit) -- Operators can modify interfaces (path-specific permit rule) -- Operators cannot modify system configuration (write-default: deny) -- Guests cannot modify any configuration (write-default: deny) -- Admin can modify all configuration (permit-all rule bypasses defaults) +- Operators can read and modify most configuration (hostname, interfaces) +- Operators CANNOT read or write password hashes (protected path) +- Guests can read but cannot modify any configuration +- Admin can access everything including passwords ==== Topology @@ -31,9 +30,11 @@ image::topology.svg[Basic NACM permissions topology, align=center, scaledwidth=7 . Configure NACM groups, rules, and test users . Verify operator can read configuration . Verify operator can modify interface configuration -. Verify operator cannot modify system configuration +. Verify operator can modify hostname +. Verify operator cannot read password hashes +. Verify operator cannot write password hashes . Verify guest can read configuration . Verify guest cannot modify configuration -. Verify admin can modify configuration +. Verify admin can access passwords From 93fceb9a5ffef85c468085c0384538ddd8b8feea Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 20:53:05 +0000 Subject: [PATCH 5/6] doc: update IPv6 forwarding documentation Signed-off-by: Joachim Wiberg --- doc/ip.md | 11 +++++------ src/confd/src/interfaces.c | 4 ++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/doc/ip.md b/doc/ip.md index 1f79b1211..69d105de6 100644 --- a/doc/ip.md +++ b/doc/ip.md @@ -415,13 +415,12 @@ admin@example:/> ## IPv6 forwarding -Due to how the Linux kernel manages IPv6 forwarding, we can not fully -control it per interface via this setting like how IPv4 works. Instead, -IPv6 forwarding is globally enabled when at least one interface enable -forwarding, otherwise it is disabled. +Forwarding must be enabled on an interface for it to route IPv6 +traffic (static or dynamic). The setting is per-interface and works +the same way as IPv4 forwarding. -The following table shows the system IPv6 features that the `forwarding` -setting control when it is *Enabled* or *Disabled: +The following table shows the IPv6 features that the `forwarding` +setting controls when it is *Enabled* or *Disabled*: | **IPv6 Feature** | **Enabled** | **Disabled** | |:-----------------------------------------|:------------|:-------------| diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index 86639ebdb..022b44483 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -335,6 +335,10 @@ static int netdag_gen_sysctl(struct dagger *net, err = err ? : netdag_gen_sysctl_setting(net, ifname, &sysctl, 1, "0", node, "net.ipv4.conf.%s.forwarding", ifname); + /* + * Use force_forwarding for IPv6 (available since Linux 6.17) which provides + * true per-interface control, unlike the legacy forwarding sysctl. + */ node = lydx_get_descendant(lyd_child(dif), "ipv6", "forwarding", NULL); err = err ? : netdag_gen_sysctl_setting(net, ifname, &sysctl, 1, "0", node, "net.ipv6.conf.%s.force_forwarding", ifname); From 7ce668904a7b1c9f434f70ecc7316eb36cfc7b3d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 23:34:34 +0100 Subject: [PATCH 6/6] doc: update ChangeLog [skip ci] Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 7e1ee2e25..b7718fe2c 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -66,12 +66,17 @@ Noteworthy changes and additions in this release are marked below in bold text. ### Fixes +- Fix #515: add per-interface IPv6 forwarding control using the Linux 6.17+ + `force_forwarding` sysctl. This provides true per-interface IPv6 forwarding + similar to IPv4, correctly mapping to the ietf-ip.yang model semantics - Fix #1082: Wi-Fi interfaces always scanned, introduce a `scan-mode` to the Wi-Fi concept in Infix - Fix #1314: Raspberry Pi 4B with 1 or 8 GiB RAM does not boot. This was due newer EEPROM firmware in newer boards require a newer rpi-firmware package - Fix #1345: firewall not updating when interfaces become bridge/lag ports - Fix #1346: firewall complains in syslog, missing `/etc/firewalld/firewalld.conf` +- Fix Raspberry Pi 2B build, among other things, the `aarch32_defconfig` did + not include a dtb. Please note, the platform has now been renamed to `arm` - Fix default password hash in `do password encrypt` command. New hash is the same as the more commonly used `change password` command, *yescrypt* - Prevent MOTD from showing on non-shell user login attempts