Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 5 additions & 6 deletions doc/ip.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** |
|:-----------------------------------------|:------------|:-------------|
Expand Down
42 changes: 5 additions & 37 deletions src/confd/src/interfaces.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -353,39 +357,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");
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/statd/python/yanger/ietf_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/case/interfaces/Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 9 additions & 6 deletions test/case/interfaces/routing_basic/test.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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


117 changes: 57 additions & 60 deletions test/case/interfaces/routing_basic/test.py
Original file line number Diff line number Diff line change
@@ -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"):
Expand All @@ -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()
6 changes: 3 additions & 3 deletions test/case/interfaces/routing_basic/topology.dot
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Loading