From 1fc29aea534e454f1065f85601c86ee9560a5b26 Mon Sep 17 00:00:00 2001 From: Nico Felbinger Date: Sun, 25 Jan 2026 01:26:00 +0100 Subject: [PATCH 1/3] tests/dhcpv6: init (#30) --- tests/README.md | 41 ++++----- tests/dhcpv6/default.nix | 174 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 tests/dhcpv6/default.nix diff --git a/tests/README.md b/tests/README.md index 5bee1a4..afd0f51 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,22 +1,23 @@ # Software Components -| Test Name | Tested Software Components | -|-------------------------------------------|----------------------------| -| bgp-bird-tcpao | BIRD3 | -| bgp-extended-nexthop | FRR, BIRD3 | -| bgp-frr-unnumbered | FRR | -| bgp-md5 | FRR, BIRD3 | -| bgp-prefsource | FRR, BIRD3 | -| bgp-simple | FRR, BIRD3, GoBGP | -| bgp-ttl-security | FRR, BIRD3 | -| dhcpv4 | Kea DHCP Server, dhclient | -| dns-knot | Knot DNS Server | -| dns-knot-dnssec | Knot DNS Server | -| dns-knot-xfr | Knot DNS Server | -| dns-knot-xfr-dnssec | Knot DNS Server | -| dns-knot-xfr-tsig | Knot DNS Server | -| dns-knot-xfr-tsig-explicit-notify | Knot DNS Server | -| ipsec-transport | strongswan | -| nat64-dns64 | Jool, BIND9 | -| ospf | FRR, BIRD3 | -| ping6-local-link | | +| Test Name | Tested Software Components | +|-------------------------------------------|---------------------------------------------------------------| +| bgp-bird-tcpao | BIRD3 | +| bgp-extended-nexthop | FRR, BIRD3 | +| bgp-frr-unnumbered | FRR | +| bgp-md5 | FRR, BIRD3 | +| bgp-prefsource | FRR, BIRD3 | +| bgp-simple | FRR, BIRD3, GoBGP | +| bgp-ttl-security | FRR, BIRD3 | +| dhcpv4 | Kea DHCP Server, dhclient | +| dhcpv6 | Kea DHCP Server, radvd, dhclient, systemd-networkd, NetworkManager | +| dns-knot | Knot DNS Server | +| dns-knot-dnssec | Knot DNS Server | +| dns-knot-xfr | Knot DNS Server | +| dns-knot-xfr-dnssec | Knot DNS Server | +| dns-knot-xfr-tsig | Knot DNS Server | +| dns-knot-xfr-tsig-explicit-notify | Knot DNS Server | +| ipsec-transport | strongswan | +| nat64-dns64 | Jool, BIND9 | +| ospf | FRR, BIRD3 | +| ping6-local-link | | diff --git a/tests/dhcpv6/default.nix b/tests/dhcpv6/default.nix new file mode 100644 index 0000000..418937d --- /dev/null +++ b/tests/dhcpv6/default.nix @@ -0,0 +1,174 @@ +{ pkgs, lib, ... }: +{ + name = "dhcpv6"; + + defaults = { + networking = { + firewall.enable = false; + useDHCP = lib.mkDefault false; + }; + + # remove all existing addresses + virtualisation.interfaces.eth1 = { + vlan = 1; + assignIP = false; + }; + }; + + nodes = { + server = { + networking.interfaces.eth1 = { + ipv6.addresses = [ + { + address = "2001:db8::1"; + prefixLength = 64; + } + ]; + }; + + boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1; + + services = { + kea.dhcp6 = { + enable = true; + settings = { + interfaces-config.interfaces = [ "eth1" ]; + subnet6 = [ + { + id = 1; + subnet = "2001:db8::/64"; + interface = "eth1"; + pools = [ + { + pool = "2001:db8::100-2001:db8::1ff"; + } + ]; + option-data = [ + { + name = "dns-servers"; + data = "2001:db8::1"; + } + ]; + } + ]; + + }; + }; + radvd = { + enable = true; + config = '' + interface eth1 { + AdvSendAdvert on; + + # Tell clients to use DHCPv6 + AdvManagedFlag on; + AdvOtherConfigFlag on; + + prefix 2001:db8::/64 { + AdvOnLink on; + + # Disable SLAAC, force DHCPv6 + AdvAutonomous off; + }; + }; + ''; + }; + }; + }; + dhcpcd = { + networking = { + interfaces.eth1.useDHCP = true; + dhcpcd = { + enable = true; + extraConfig = '' + interface eth1 + dhcp6 + ipv6only + ''; + }; + }; + }; + networkd = { + systemd.network = { + enable = true; + networks."10-eth1" = { + matchConfig.Name = "eth1"; + networkConfig = { + IPv6AcceptRA = true; + DHCP = "ipv6"; + }; + }; + }; + }; + nm = { + networking.networkmanager = { + enable = true; + # this is needed so NM doesn't generate 'Wired Connection' profiles and instead uses the default one + settings.main.no-auto-default = "*"; + ensureProfiles.profiles.eth1 = { + connection = { + id = "eth1"; + type = "ethernet"; + interface-name = "eth1"; + autoconnect = true; + }; + ipv6.method = "auto"; + }; + }; + }; + }; + + interactive.nodes = lib.listToAttrs ( + map + (name: { + inherit name; + value.environment.systemPackages = with pkgs; [ + tcpdump + ]; + }) + [ + "dhcpcd" + "networkd" + "nm" + "server" + ] + ); + + testScript = '' + server.start() + server.wait_for_unit("network.target") + server.wait_for_unit("radvd.service") + server.wait_for_unit("kea-dhcp6-server.service") + + # Wait for IPv6 Duplicate Address Detection (DAD) to complete. + server.wait_until_succeeds(""" + ip -j -6 a sh eth1 | \ + ${lib.getExe pkgs.jq} -e -r '.[] | .addr_info | .[] | select((.family == "inet6") and .scope == "link") | has("tentative") == false' + """) + + server.succeed("systemctl restart kea-dhcp6-server") + + start_all() + + with subtest("dhcpcd"): + dhcpcd.wait_for_unit("dhcpcd.service") + + dhcpcd.wait_until_succeeds("ip -6 -br a | grep -E 'eth1.*2001:db8::1[0-9a-f]{2}'", timeout=30) + dhcpcd.succeed("cat /etc/resolv.conf | grep 'nameserver 2001:db8::1'") + dhcpcd.succeed("ip -6 route show default dev eth1 | grep default") + + with subtest("systemd-networkd"): + networkd.wait_for_unit("systemd-networkd.service") + + networkd.wait_until_succeeds("ip -6 -br a | grep -E 'eth1.*2001:db8::1[0-9a-f]{2}'", timeout=30) + networkd.succeed("resolvectl status eth1 | grep 'DNS Servers: 2001:db8::1'") + networkd.succeed("ip -6 route show default dev eth1 | grep default") + + with subtest("network manager"): + nm.wait_for_unit("NetworkManager.service") + + nm.wait_until_succeeds("ip -6 -br a | grep -E 'eth1.*2001:db8::1[0-9a-f]{2}'", timeout=30) + nm.succeed("cat /etc/resolv.conf | grep 'nameserver 2001:db8::1'") + nm.succeed("ip -6 route show default dev eth1 | grep default") + ''; +} From e6ddd9e7eada0435f22aef5101b3ed82a1531609 Mon Sep 17 00:00:00 2001 From: Nico Felbinger Date: Sun, 25 Jan 2026 16:46:29 +0100 Subject: [PATCH 2/3] tests/dhcpv6-pd: init (#30) --- tests/README.md | 1 + tests/dhcpv6-pd/default.nix | 161 ++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 tests/dhcpv6-pd/default.nix diff --git a/tests/README.md b/tests/README.md index afd0f51..54f7a77 100644 --- a/tests/README.md +++ b/tests/README.md @@ -11,6 +11,7 @@ | bgp-ttl-security | FRR, BIRD3 | | dhcpv4 | Kea DHCP Server, dhclient | | dhcpv6 | Kea DHCP Server, radvd, dhclient, systemd-networkd, NetworkManager | +| dhcpv6-pd | Kea DHCP Server, radvd, systemd-networkd | | dns-knot | Knot DNS Server | | dns-knot-dnssec | Knot DNS Server | | dns-knot-xfr | Knot DNS Server | diff --git a/tests/dhcpv6-pd/default.nix b/tests/dhcpv6-pd/default.nix new file mode 100644 index 0000000..6ff3e9f --- /dev/null +++ b/tests/dhcpv6-pd/default.nix @@ -0,0 +1,161 @@ +{ pkgs, lib, ... }: +{ + name = "dhcpv6-pd"; + + defaults = { + networking = { + firewall.enable = false; + useDHCP = lib.mkDefault false; + }; + }; + + nodes = { + server = { + virtualisation.interfaces.eth1 = { + vlan = 1; + assignIP = false; + }; + + networking.interfaces.eth1.ipv6.addresses = [ + { + address = "2001:db8::1"; + prefixLength = 64; + } + ]; + + boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1; + + services = { + kea.dhcp6 = { + enable = true; + settings = { + interfaces-config.interfaces = [ "eth1" ]; + subnet6 = [ + { + id = 1; + subnet = "2001:db8::/48"; + interface = "eth1"; + pools = [ + { + pool = "2001:db8::100-2001:db8::1ff"; + } + ]; + pd-pools = [ + { + prefix = "2001:db8:0:1000::"; + prefix-len = 52; + delegated-len = 56; + } + ]; + } + ]; + + }; + }; + radvd = { + enable = true; + config = '' + interface eth1 { + AdvSendAdvert on; + + # Tell clients to use DHCPv6 + AdvManagedFlag on; + AdvOtherConfigFlag on; + + prefix 2001:db8::/64 { + AdvOnLink on; + + # Disable SLAAC, force DHCPv6 + AdvAutonomous off; + }; + }; + ''; + }; + }; + }; + router = { + virtualisation.interfaces = { + eth1 = { + vlan = 1; + assignIP = false; + }; + eth2 = { + vlan = 2; + assignIP = false; + }; + }; + + boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1; + + systemd.network = { + enable = true; + networks = { + "10-eth1" = { + matchConfig.Name = "eth1"; + networkConfig = { + IPv6AcceptRA = true; + DHCP = "ipv6"; + }; + dhcpV6Config.PrefixDelegationHint = 56; + }; + "20-eth2" = { + matchConfig.Name = "eth2"; + networkConfig = { + IPv6SendRA = true; + DHCPPrefixDelegation = true; + }; + dhcpPrefixDelegationConfig.UplinkInterface = "eth1"; + }; + }; + }; + }; + client = { + virtualisation.interfaces.eth1 = { + vlan = 2; + assignIP = false; + }; + networking.interfaces.eth1.ipv6.addresses = [ ]; + }; + }; + + interactive.nodes = lib.listToAttrs ( + map + (name: { + inherit name; + value.environment.systemPackages = with pkgs; [ + tcpdump + ]; + }) + [ + "server" + "router" + "client" + ] + ); + + testScript = '' + start_all() + + server.wait_for_unit("network.target") + server.wait_for_unit("radvd.service") + server.wait_for_unit("kea-dhcp6-server.service") + + # Wait for IPv6 Duplicate Address Detection (DAD) to complete. + server.wait_until_succeeds(""" + ip -j -6 a sh eth1 | \ + ${lib.getExe pkgs.jq} -e -r '.[] | .addr_info | .[] | select((.family == "inet6") and .scope == "link") | has("tentative") == false' + """) + + server.succeed("systemctl restart kea-dhcp6-server") + + router.wait_for_unit("systemd-networkd.service") + + router.wait_until_succeeds("ip -6 -br a | grep -E 'eth1.*2001:db8::1[0-9a-f]{2}'", timeout=30) + router.succeed("ip -6 route show default dev eth1 | grep default") + + router.succeed("ip -6 -br a | grep -E 'eth2.*2001:db8:0:1000:'") + + client.succeed("ip -6 -br a | grep -E 'eth1.*2001:db8:0:1000:'") + client.succeed("ip -6 route show default dev eth1 | grep default") + ''; +} From 1b01f2f44f42c6ef92278fab8a64a0db82ff7fb3 Mon Sep 17 00:00:00 2001 From: Nico Felbinger Date: Sun, 25 Jan 2026 17:40:18 +0100 Subject: [PATCH 3/3] tests/dhcpv6-pd: add hook script to add routes for delegated prefixes not working yet - will be squashed into previous commit --- tests/dhcpv6-pd/default.nix | 11 ++++++++ tests/dhcpv6-pd/hook.sh | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 tests/dhcpv6-pd/hook.sh diff --git a/tests/dhcpv6-pd/default.nix b/tests/dhcpv6-pd/default.nix index 6ff3e9f..10437ee 100644 --- a/tests/dhcpv6-pd/default.nix +++ b/tests/dhcpv6-pd/default.nix @@ -30,6 +30,15 @@ enable = true; settings = { interfaces-config.interfaces = [ "eth1" ]; + hooks-libraries = [ + { + library = "${pkgs.kea}/lib/kea/hooks/libdhcp_run_script.so"; + parameters = { + name = pkgs.writeShellScript "kea-dhcpv6-pd-add-routes-hook.sh" (builtins.readFile ./hook.sh); + sync = false; + }; + } + ]; subnet6 = [ { id = 1; @@ -157,5 +166,7 @@ client.succeed("ip -6 -br a | grep -E 'eth1.*2001:db8:0:1000:'") client.succeed("ip -6 route show default dev eth1 | grep default") + + client.succeed("ping -c 1 2001:db8::1") ''; } diff --git a/tests/dhcpv6-pd/hook.sh b/tests/dhcpv6-pd/hook.sh new file mode 100644 index 0000000..9aaaf71 --- /dev/null +++ b/tests/dhcpv6-pd/hook.sh @@ -0,0 +1,52 @@ +# https://kea.readthedocs.io/en/latest/arm/hooks.html#run-script-run-script-support-for-external-hook-scripts +# This script adds/removes IPv6 routes on prefix-delegation from KEA/DHCP server + +lease6_renew () { + if [ "$LEASE6_TYPE" = "IA_PD" ]; then + # Add route for delegated prefix (next hop is the client) + ip -6 route replace "${LEASE6_ADDRESS}/${LEASE6_PREFIX_LEN}" via "${QUERY6_REMOTE_ADDR}" dev "${QUERY6_IFACE_NAME}" proto static + fi + exit 0 + +} + +lease6_expire () { + if [ "$LEASE6_TYPE" = "IA_PD" ]; then + # Remove route for delegated prefix + ip -6 route del "${LEASE6_ADDRESS}/${LEASE6_PREFIX_LEN}" proto static + fi + exit 0 +} + +leases6_committed () { + # TODO: If i.e. addresses are also available via DHCP, there can be more than a single AT[index], so Loop 0..($LEASES6_SIZE-1) + # if [ "$LEASES6_AT0_TYPE" = "IA_NA" ]; then it's an address + if [ "$LEASES6_AT0_TYPE" = "IA_PD" ]; then + # Add route for delegated prefix (next hop is the client). Remote-addr (via) will typically be LinkLocal, unless KEA listens on Unicast + ip -6 route replace "${LEASES6_AT0_ADDRESS}/${LEASES6_AT0_PREFIX_LEN}" via "${QUERY6_REMOTE_ADDR}" dev "${QUERY6_IFACE_NAME}" proto static + fi + exit 0 +} + +lease6_release () { + if [ "$LEASE6_TYPE" = "IA_PD" ]; then + # Remove route for delegated prefix + ip -6 route del "${LEASE6_ADDRESS}/${LEASE6_PREFIX_LEN}" proto static + fi + exit 0 +} + +case "$1" in + "lease6_renew") + lease6_renew + ;; + "lease6_expire") + lease6_expire + ;; + "leases6_committed") + leases6_committed + ;; + "lease6_release") + lease6_release + ;; +esac