Merge pull request #1380 from kernelkit/copilot/handle-ipv6-forwarding-netfilter

This commit is contained in:
Joachim Wiberg
2026-01-30 23:40:36 +01:00
committed by GitHub
10 changed files with 134 additions and 158 deletions
+5
View File
@@ -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
+5 -6
View File
@@ -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** |
|:-----------------------------------------|:------------|:-------------|
+5 -37
View File
@@ -335,9 +335,13 @@ 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.forwarding", ifname);
"net.ipv6.conf.%s.force_forwarding", ifname);
if (!strcmp(ifname, "lo")) /* skip for now */
goto skip_mtu;
@@ -353,39 +357,6 @@ skip_mtu:
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 +753,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
+5 -4
View File
@@ -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
+1 -1
View File
@@ -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
+9 -6
View File
@@ -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
+57 -60
View File
@@ -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 @@ with infamy.Test() as test:
_, hport1 = env.ltop.xlate("host", "data2")
with infamy.IsolatedMacVlan(hport0) as ns0, \
infamy.IsolatedMacVlan(hport1) as ns1 :
infamy.IsolatedMacVlan(hport1) as ns1:
with test.step("Setup host"):
ns0.addip("192.168.0.10")
ns0.addroute("default", "192.168.0.1")
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()
@@ -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"]
}
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"]
}
+31 -29
View File
@@ -3,55 +3,57 @@
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: routing_basic Pages: 1 -->
<svg width="440pt" height="79pt"
viewBox="0.00 0.00 440.03 79.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 75)">
<svg width="440pt" height="92pt"
viewBox="0.00 0.00 440.03 92.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 88)">
<title>routing_basic</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-75 436.03,-75 436.03,4 -4,4"/>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-88 436.03,-88 436.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-1.5 0,-70.5 108,-70.5 108,-1.5 0,-1.5"/>
<text text-anchor="middle" x="25" y="-32.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-1.5 50,-70.5 "/>
<text text-anchor="middle" x="79" y="-55.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-47.5 108,-47.5 "/>
<text text-anchor="middle" x="79" y="-32.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="50,-24.5 108,-24.5 "/>
<text text-anchor="middle" x="79" y="-9.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
<polygon fill="none" stroke="black" points="0,-14.5 0,-83.5 108,-83.5 108,-14.5 0,-14.5"/>
<text text-anchor="middle" x="25" y="-45.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-14.5 50,-83.5 "/>
<text text-anchor="middle" x="79" y="-68.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-60.5 108,-60.5 "/>
<text text-anchor="middle" x="79" y="-45.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="50,-37.5 108,-37.5 "/>
<text text-anchor="middle" x="79" y="-22.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="308.03,-1.5 308.03,-70.5 432.03,-70.5 432.03,-1.5 308.03,-1.5"/>
<text text-anchor="middle" x="337.03" y="-55.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="308.03,-47.5 366.03,-47.5 "/>
<text text-anchor="middle" x="337.03" y="-32.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="308.03,-24.5 366.03,-24.5 "/>
<text text-anchor="middle" x="337.03" y="-9.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
<polyline fill="none" stroke="black" points="366.03,-1.5 366.03,-70.5 "/>
<text text-anchor="middle" x="399.03" y="-32.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
<polygon fill="none" stroke="black" points="308.03,-14.5 308.03,-83.5 432.03,-83.5 432.03,-14.5 308.03,-14.5"/>
<text text-anchor="middle" x="337.03" y="-68.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="308.03,-60.5 366.03,-60.5 "/>
<text text-anchor="middle" x="337.03" y="-45.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="308.03,-37.5 366.03,-37.5 "/>
<text text-anchor="middle" x="337.03" y="-22.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
<polyline fill="none" stroke="black" points="366.03,-14.5 366.03,-83.5 "/>
<text text-anchor="middle" x="399.03" y="-45.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-59C108,-59 308.03,-59 308.03,-59"/>
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-72C108,-72 308.03,-72 308.03,-72"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge2" class="edge">
<title>host:data1&#45;&#45;target:data1</title>
<path fill="none" stroke="black" stroke-width="2" d="M108,-36C108,-36 308.03,-36 308.03,-36"/>
<text text-anchor="middle" x="162.52" y="-39.4" font-family="DejaVu Serif, Book" font-size="12.00">192.168.0.0/24</text>
<text text-anchor="middle" x="302.03" y="-39.4" font-family="DejaVu Serif, Book" font-size="12.00">.1</text>
<text text-anchor="middle" x="117.5" y="-26.4" font-family="DejaVu Serif, Book" font-size="12.00">.10</text>
<path fill="none" stroke="black" stroke-width="2" d="M108,-49C108,-49 308.03,-49 308.03,-49"/>
<text text-anchor="middle" x="161.02" y="-65.4" font-family="DejaVu Serif, Book" font-size="12.00">192.168.0.0/24</text>
<text text-anchor="middle" x="161.02" y="-52.4" font-family="DejaVu Serif, Book" font-size="12.00">2001:db8:0::/64</text>
<text text-anchor="middle" x="302.03" y="-52.4" font-family="DejaVu Serif, Book" font-size="12.00">.1</text>
<text text-anchor="middle" x="117.5" y="-39.4" font-family="DejaVu Serif, Book" font-size="12.00">.10</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge3" class="edge">
<title>host:data2&#45;&#45;target:data2</title>
<path fill="none" stroke="black" stroke-width="2" d="M108,-13C108,-13 308.03,-13 308.03,-13"/>
<text text-anchor="middle" x="174.02" y="-16.4" font-family="DejaVu Serif, Book" font-size="12.00">10.0.0.0/24</text>
<text text-anchor="middle" x="302.03" y="-16.4" font-family="DejaVu Serif, Book" font-size="12.00">.1</text>
<text text-anchor="middle" x="117.5" y="-3.4" font-family="DejaVu Serif, Book" font-size="12.00">.10</text>
<path fill="none" stroke="black" stroke-width="2" d="M108,-26C108,-26 308.03,-26 308.03,-26"/>
<text text-anchor="middle" x="161.02" y="-16.4" font-family="DejaVu Serif, Book" font-size="12.00">10.0.0.0/24</text>
<text text-anchor="middle" x="161.02" y="-3.4" font-family="DejaVu Serif, Book" font-size="12.00">2001:db8:1::/64</text>
<text text-anchor="middle" x="302.03" y="-29.4" font-family="DejaVu Serif, Book" font-size="12.00">.1</text>
<text text-anchor="middle" x="117.5" y="-29.4" font-family="DejaVu Serif, Book" font-size="12.00">.10</text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

+13 -12
View File
@@ -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