Add a static ULA per LAN, announced via SLAAC and RDNSS

lans.<n>.ipv6.address (a ULA /64) is assigned to the LAN, announced
with an explicit prefix (networkd only announces the delegated prefix
when prefix delegation is on) and advertised as RDNSS; Unbound listens
and answers on it. Checked at build time: valid IPv6 CIDR, /64, fc00::/7.

Covered by the onestick VM test (client SLAAC address, ping and DNS over
the ULA, RA captured on the wire), assertion cases and a unit test for
the /64 prefix helper.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 15:48:47 +01:00
co-authored by Claude Sonnet 5.5
parent 70dddad206
commit c395a43a78
9 changed files with 150 additions and 7 deletions
+21
View File
@@ -979,6 +979,26 @@ let
"also used by a LAN"
];
};
ulaValid = {
router.lans.main = lan { ipv6.address = "fd2c:cdef:cde9::1/64"; };
expect = [ ];
};
ulaNotUla = {
router.lans.main = lan { ipv6.address = "2001:db8::1/64"; };
expect = [ "unique-local" ];
};
ulaNot64 = {
router.lans.main = lan { ipv6.address = "fd2c:cdef:cde9::1/48"; };
expect = [ "must be a /64" ];
};
ulaGarbage = {
router.lans.main = lan { ipv6.address = "nonsense"; };
expect = [ "not an IPv6 address in CIDR" ];
};
ulaIpv4 = {
router.lans.main = lan { ipv6.address = "192.168.1.1/24"; };
expect = [ "not an IPv6 address in CIDR" ];
};
nonOverlappingLans = {
router.lans = {
a = lan { };
@@ -1028,6 +1048,7 @@ let
"wanValid"
"sharedLinkValid"
"pppTransportValid"
"ulaValid"
"dnsValid"
];
+18
View File
@@ -6,7 +6,25 @@ let
inherit (r) ipv4;
ip = ipv4.parse;
inherit (r) ipv6;
failures = lib.runTests {
testPrefix64 = {
expr = map ipv6.prefix64 [
"fd2c:cdef:cde9::1/64"
"fd2c:cdef:cde9:1:2:3:4:5"
"fd00::1"
"fd12:3456:789a:bc::1/64"
"fd12::5:6"
];
expected = [
"fd2c:cdef:cde9:0::/64"
"fd2c:cdef:cde9:1::/64"
"fd00:0:0:0::/64"
"fd12:3456:789a:bc::/64"
"fd12:0:0:0::/64"
];
};
testParse = {
expr = ip "192.168.10.1";
expected = 3232238081;
+23 -2
View File
@@ -192,6 +192,7 @@ pkgs.testers.runNixOSTest {
internet = true;
ipv4.address = "192.168.10.1";
ipv6.pd = true;
ipv6.address = "fd2c:cdef:cde9::1/64";
dhcp.pool = {
start = "192.168.10.100";
end = "192.168.10.199";
@@ -209,7 +210,11 @@ pkgs.testers.runNixOSTest {
dnat = true;
}
];
dns.forwarders = [ ispV4 ];
dns = {
forwarders = [ ispV4 ];
domain = "lan.test";
hosts.nas = [ "192.168.10.20" ];
};
};
};
@@ -294,7 +299,7 @@ pkgs.testers.runNixOSTest {
with subtest("the client autoconfigures from the delegated prefix, without NAT"):
client.wait_until_succeeds("ip -6 addr show dev eth1 scope global | grep -q 2001:db8:2:")
client_v6 = client.succeed("ip -6 -o addr show dev eth1 scope global | awk '{print $4}' | cut -d/ -f1 | head -1").strip()
client_v6 = client.succeed("ip -6 -o addr show dev eth1 scope global | awk '{print $4}' | cut -d/ -f1 | grep 2001:db8:2: | head -1").strip()
client.wait_until_succeeds("ping -6 -c 1 -W 2 2001:db8:1::1")
out = client.succeed("curl -sf -g --max-time 5 'http://[2001:db8:1::1]/'")
assert out.strip() == client_v6, f"expected {client_v6}, got {out!r}"
@@ -313,5 +318,21 @@ pkgs.testers.runNixOSTest {
upstream.fail(f"ping -6 -c 1 -W 2 {client_v6}")
upstream.fail(f"nc -6 -z -w 2 {client_v6} 22")
upstream.fail(f"nc -6 -z -w 2 {ll}%ppp0 22")
with subtest("the ULA is announced and the resolver answers on it"):
router.succeed("ip -6 addr show dev eth1 | grep -q fd2c:cdef:cde9::1/64")
client.wait_until_succeeds("ip -6 addr show dev eth1 scope global | grep -q fd2c:cdef:cde9:")
client.succeed("ping -6 -c 1 -W 2 fd2c:cdef:cde9::1")
out = client.succeed("dig +short +time=2 +tries=1 @fd2c:cdef:cde9::1 nas.lan.test").strip()
assert out == "192.168.10.20", out
# The RA carries the ULA prefix (SLAAC, on-link) and the resolver as RDNSS.
# A link bounce makes the client solicit one right away.
router.succeed("(timeout 20 tcpdump -nn -vv -i eth1 -c 1 'icmp6 and ip6[40] == 134' > /tmp/ra.txt 2>&1 &)")
router.wait_until_succeeds("pgrep tcpdump")
router.sleep(1)
client.succeed("ip link set eth1 down; ip link set eth1 up")
ra = router.wait_until_succeeds("grep -q 'router lifetime' /tmp/ra.txt && cat /tmp/ra.txt")
assert "fd2c:cdef:cde9::/64, Flags [onlink, auto]" in ra, ra
assert "rdnss option" in ra and "fd2c:cdef:cde9::1" in ra, ra
'';
}