Files
nixpkgs/nixos/tests/containers-ipv6-slaac.nix
Leon Schuermann dee54f2955 nixos/tests: add test for NixOS container using IPv6 SLAAC
This adds a test for a NixOS container being assigned an IPv6 address
and route using stateless auto-configuration, using IPv6 router
advertisements sent using systemd-networkd by the host.

It exercises the newly added `macAddress` option for the container, as
it relies on the container self-assigning an address in the specific
IPv6 prefix based on its stable MAC address. It can also serve as an
example for how one may bridge a NixOS container into an existing
network, and assign it stable IPv4 / IPv6 addresses via DHCP and RAs.
2025-11-26 13:33:00 -05:00

179 lines
5.7 KiB
Nix

let
ulaPrefix = "fd5f:e1a2:4f0c::/64";
hostMAC = "72:ec:00:8b:75:44";
hostSLAACv6 = "fd5f:e1a2:4f0c:0:70ec:ff:fe8b:7544/64";
containerMAC = "b2:65:3f:c9:6b:10";
containerSLAACv6 = "fd5f:e1a2:4f0c:0:b065:3fff:fec9:6b10/64";
in
{ pkgs, lib, ... }:
{
name = "containers-ipv6-slaac";
meta = {
maintainers = with lib.maintainers; [
lschuermann
];
};
nodes.machine =
{ pkgs, ... }:
{
networking.useNetworkd = true;
networking.useDHCP = false;
systemd.network.netdevs."br0".netdevConfig = {
Name = "br0";
Kind = "bridge";
MACAddress = hostMAC;
};
systemd.network.networks."br0" = {
name = "br0";
networkConfig = {
IPv6SendRA = true;
# Disable privacy extensions, which would assign the host a random
# address in the ULA prefix (defeating the purpose of setting the
# bridge's `MACAddress` to assign it a stable address explicitly):
IPv6PrivacyExtensions = false;
};
ipv6SendRAConfig = {
# We assign addresses exclusively through SLAAC, not via DHCPv6:
Managed = false;
# This router is not a default gateway, as we don't have an IPv6
# upstream. This causes no default route to be inserted with the RA.
RouterLifetimeSec = 0;
UplinkInterface = ":none";
};
ipv6Prefixes = [
{
# Assign addresses out of the configured ULA prefix:
Prefix = ulaPrefix;
AddressAutoconfiguration = true;
# All other addresses in this subnet are reachable via Layer 2 (don't
# need to go through the host as a router):
OnLink = true;
# Assign the host an address out of this subnet:
Assign = true;
# Use MAC address as the basis for SLAAC address generation:
Token = "eui64";
}
];
# The router doesn't advertise itself as a default gateway, so we
# announce our ULA prefix explicitly:
ipv6RoutePrefixes = [
{
Route = ulaPrefix;
LifetimeSec = 1800;
}
];
};
containers.webserver = {
autoStart = true;
privateNetwork = true;
hostBridge = "br0";
localMacAddress = containerMAC;
config = {
networking.useNetworkd = true;
networking.useHostResolvConf = false;
systemd.network.networks."eth0" = {
name = "eth0";
DHCP = "no";
# Assign an IPv6 address out of the host-advertised prefix, disable
# privacy extensions (which would assign a random address in the
# announced prefix, defeating the purpose of setting the
# `localMacAddress` option to assign the container a stable
# address):
networkConfig = {
IPv6AcceptRA = true;
IPv6PrivacyExtensions = false;
};
};
services.httpd.enable = true;
networking.firewall.allowedTCPPorts = [ 80 ];
};
};
};
testScript = ''
import time
machine.wait_for_unit("default.target")
assert "webserver" in machine.succeed("nixos-container list")
with subtest("Start the webserver container"):
assert "up" in machine.succeed("nixos-container status webserver")
with subtest("veth in container has correct MAC address"):
assert "${containerMAC}" in machine.succeed(
"nixos-container run webserver -- ip link show eth0",
)
with subtest("Host gets assigned IPv6 in and route for ULA prefix"):
# This is done by systemd-network internally, so should be available
# instantly:
assert "${hostSLAACv6}" in machine.succeed(
"ip addr show br0"
)
assert "${ulaPrefix}" in machine.succeed(
"ip -6 route show"
)
with subtest("Container gets assigned IPv6 in and route for ULA prefix"):
# Give the container a few seconds to assign itself a v6 out of and set
# up a route for the ULA prefix from the router advertisement:
for _ in range(3):
iface_ips = machine.succeed(
"nixos-container run webserver -- ip addr show eth0",
)
v6_routes = machine.succeed(
"nixos-container run webserver -- ip -6 route show",
)
if "${containerSLAACv6}" in iface_ips and "${ulaPrefix}" in v6_routes:
break
else:
time.sleep(1)
else:
raise AssertionError(
"Container either did not assign itself the expected SLAAC "
+ "v6 out of the announced ULA prefix (${containerSLAACv6}) "
+ "or did not assign a route for the URL prefix "
+ f"(${ulaPrefix}).\n\n==> ip addr show eth0:\n{iface_ips}"
+ f"\n\n==> ip -6 route show:\n{v6_routes}"
)
ip6 = "${containerSLAACv6}".split("/")[0]
with subtest("Container reponds to ICMPv6 echo requests"):
# IPv6 ND can take some time, so try at most 30 times:
for i in range(30):
print(f"Sending ICMP echo request, attempt #{i}")
exit_status, _out = machine.execute(f"ping -n -c 1 {ip6}")
if exit_status == 0:
break
else:
time.sleep(1)
else:
raise AssertionError("Container doesn't respond to pings!")
with subtest("Container responds to HTTP requests"):
machine.succeed(f"curl --fail http://[{ip6}]/ > /dev/null")
# Destroying a declarative container should fail.
machine.fail("nixos-container destroy webserver")
'';
}