Skip to content

Commit d76b2c4

Browse files
committed
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.
1 parent 632d0cf commit d76b2c4

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed

nixos/tests/all-tests.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ in
395395
containers-hosts = runTest ./containers-hosts.nix;
396396
containers-imperative = runTest ./containers-imperative.nix;
397397
containers-ip = runTest ./containers-ip.nix;
398+
containers-ipv6-slaac = runTest ./containers-ipv6-slaac.nix;
398399
containers-macvlans = runTest ./containers-macvlans.nix;
399400
containers-names = runTest ./containers-names.nix;
400401
containers-nested = runTest ./containers-nested.nix;
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
let
2+
ulaPrefix = "fd5f:e1a2:4f0c::/64";
3+
hostMAC = "72:ec:00:8b:75:44";
4+
hostSLAACv6 = "fd5f:e1a2:4f0c:0:70ec:ff:fe8b:7544/64";
5+
containerMAC = "b2:65:3f:c9:6b:10";
6+
containerSLAACv6 = "fd5f:e1a2:4f0c:0:b065:3fff:fec9:6b10/64";
7+
in
8+
9+
{ pkgs, lib, ... }:
10+
{
11+
name = "containers-ipv6-slaac";
12+
meta = {
13+
maintainers = with lib.maintainers; [
14+
lschuermann
15+
];
16+
};
17+
18+
nodes.machine =
19+
{ pkgs, ... }:
20+
{
21+
imports = [ ../modules/installer/cd-dvd/channel.nix ];
22+
virtualisation.writableStore = true;
23+
24+
networking.useNetworkd = true;
25+
networking.useDHCP = false;
26+
27+
systemd.network.netdevs."br0".netdevConfig = {
28+
Name = "br0";
29+
Kind = "bridge";
30+
MACAddress = hostMAC;
31+
};
32+
33+
systemd.network.networks."br0" = {
34+
name = "br0";
35+
address = [
36+
"fe80::1/64"
37+
];
38+
39+
networkConfig = {
40+
ConfigureWithoutCarrier = true;
41+
42+
IPv6SendRA = true;
43+
IPv6PrivacyExtensions = false;
44+
IPv6AcceptRA = false;
45+
};
46+
47+
ipv6SendRAConfig = {
48+
Managed = false;
49+
# This router is not a default gateway, as we don't have an IPv6
50+
# upstream. This causes no default route to be inserted with the RA.
51+
RouterLifetimeSec = 0;
52+
RetransmitSec = 10;
53+
UplinkInterface = ":none";
54+
EmitDNS = false;
55+
};
56+
57+
ipv6Prefixes = [
58+
{
59+
# Assign addresses out of the configured ULA prefix:
60+
Prefix = ulaPrefix;
61+
AddressAutoconfiguration = true;
62+
# All other addresses in this subnet are reachable via Layer 2 (don't
63+
# need to go through the host as a router):
64+
OnLink = true;
65+
# Assign the host an address out of this subnet:
66+
Assign = true;
67+
# Use MAC address as the basis for SLAAC address generation:
68+
Token = "eui64";
69+
}
70+
];
71+
72+
# The router doesn't advertise itself as a default gateway, so we
73+
# announce our ULA prefix explicitly:
74+
ipv6RoutePrefixes = [
75+
{
76+
Route = ulaPrefix;
77+
LifetimeSec = 1800;
78+
}
79+
];
80+
81+
};
82+
83+
containers.webserver = {
84+
autoStart = true;
85+
privateNetwork = true;
86+
hostBridge = "br0";
87+
localMacAddress = containerMAC;
88+
config = {
89+
networking.useNetworkd = true;
90+
networking.useHostResolvConf = false;
91+
92+
systemd.network.networks."eth0" = {
93+
name = "eth0";
94+
DHCP = "no";
95+
96+
# Assign an IPv6 address out of the host-advertised prefix, disable
97+
# privacy extensions:
98+
networkConfig = {
99+
IPv6AcceptRA = true;
100+
IPv6PrivacyExtensions = true;
101+
};
102+
};
103+
104+
networking.firewall.allowedTCPPorts = [ 80 ];
105+
106+
services.httpd.enable = true;
107+
services.httpd.adminAddr = "[email protected]";
108+
};
109+
};
110+
111+
virtualisation.additionalPaths = [ pkgs.stdenv ];
112+
};
113+
114+
testScript = ''
115+
import time
116+
117+
machine.wait_for_unit("default.target")
118+
assert "webserver" in machine.succeed("nixos-container list")
119+
120+
with subtest("Start the webserver container"):
121+
assert "up" in machine.succeed("nixos-container status webserver")
122+
123+
with subtest("veth in container has correct MAC address"):
124+
assert "${containerMAC}" in machine.succeed(
125+
"nixos-container run webserver -- ip link show eth0",
126+
)
127+
128+
with subtest("Host gets assigned IPv6 in and route for ULA prefix"):
129+
# This is done by systemd-network internally, so should be available
130+
# instantly:
131+
print(machine.succeed("ip addr"))
132+
print(machine.succeed("ip -6 route show"))
133+
assert "${hostSLAACv6}" in machine.succeed(
134+
"ip addr show br0"
135+
)
136+
assert "${ulaPrefix}" in machine.succeed(
137+
"ip -6 route show"
138+
)
139+
140+
with subtest("Container gets assigned IPv6 in and route for ULA prefix"):
141+
# Give the container a few seconds to assign itself a v6 out of and set
142+
# up a route for the ULA prefix from the router advertisement:
143+
for _ in range(3):
144+
iface_ips = machine.succeed(
145+
"nixos-container run webserver -- ip addr show eth0",
146+
)
147+
v6_routes = machine.succeed(
148+
"nixos-container run webserver -- ip -6 route show",
149+
)
150+
if "${containerSLAACv6}" in iface_ips and "${ulaPrefix}" in v6_routes:
151+
break
152+
else:
153+
time.sleep(1)
154+
else:
155+
raise AssertionError(
156+
"Container either did not assign itself the expected SLAAC "
157+
+ "v6 out of the announced ULA prefix (${containerSLAACv6}) "
158+
+ "or did not assign a route for the URL prefix "
159+
+ f"(${ulaPrefix}).\n\n==> ip addr show eth0:\n{iface_ips}"
160+
+ f"\n\n==> ip -6 route show:\n{v6_routes}"
161+
)
162+
163+
ip6 = "${containerSLAACv6}".split("/")[0]
164+
165+
with subtest("Container reponds to ICMPv6 echo requests"):
166+
# IPv6 ND can take some time, so try at most 30 times:
167+
for i in range(30):
168+
print(f"Sending ICMP echo request, attempt #{i}")
169+
exit_status, _out = machine.execute(f"ping -n -c 1 {ip6}")
170+
if exit_status == 0:
171+
break
172+
else:
173+
time.sleep(1)
174+
else:
175+
raise AssertionError("Container doesn't respond to pings!")
176+
177+
with subtest("Container responds to HTTP requests"):
178+
machine.succeed(f"curl --fail http://[{ip6}]/ > /dev/null")
179+
180+
# Destroying a declarative container should fail.
181+
machine.fail("nixos-container destroy webserver")
182+
'';
183+
}

0 commit comments

Comments
 (0)