1 # Test of IPv6 functionality in NixOS, including whether router
2 # solicication/advertisement using radvd works.
4 import ./make-test-python.nix ({ pkgs, lib, ...} : {
6 meta = with pkgs.lib.maintainers; {
7 maintainers = [ eelco ];
12 # We use lib.mkForce here to remove the interface configuration
13 # provided by makeTest, so that the interfaces are all configured
16 # This client should use privacy extensions fully, having a
17 # completely-default network configuration.
18 client_defaults.networking.interfaces = lib.mkForce {};
20 # Both of these clients should obtain temporary addresses, but
21 # not use them as the default source IP. We thus run the same
22 # checks against them — but the configuration resulting in this
23 # behaviour is different.
25 # Here, by using an altered default value for the global setting...
26 client_global_setting = {
27 networking.interfaces = lib.mkForce {};
28 networking.tempAddresses = "enabled";
30 # and here, by setting this on the interface explicitly.
31 client_interface_setting = {
32 networking.tempAddresses = "disabled";
33 networking.interfaces = lib.mkForce {
34 eth1.tempAddress = "enabled";
39 { services.httpd.enable = true;
40 services.httpd.adminAddr = "foo@example.org";
41 networking.firewall.allowedTCPPorts = [ 80 ];
46 { services.radvd.enable = true;
47 services.radvd.config =
51 # ULA prefix (RFC 4193).
52 prefix fd60:cc69:b537:1::/64 { };
62 # Start the router first so that it respond to router solicitations.
63 router.wait_for_unit("radvd")
65 clients = [client_defaults, client_global_setting, client_interface_setting]
69 for client in clients:
70 client.wait_for_unit("network.target")
71 server.wait_for_unit("network.target")
72 server.wait_for_unit("httpd.service")
74 # Wait until the given interface has a non-tentative address of
75 # the desired scope (i.e. has completed Duplicate Address
77 def wait_for_address(machine, iface, scope, temporary=False):
78 temporary_flag = "temporary" if temporary else "-temporary"
79 cmd = f"ip -o -6 addr show dev {iface} scope {scope} -tentative {temporary_flag}"
81 machine.wait_until_succeeds(f"[ `{cmd} | wc -l` -eq 1 ]")
82 output = machine.succeed(cmd)
83 ip = re.search(r"inet6 ([0-9a-f:]{2,})/", output).group(1)
86 scope = scope + " temporary"
87 machine.log(f"{scope} address on {iface} is {ip}")
91 with subtest("Loopback address can be pinged"):
92 client_defaults.succeed("ping -c 1 ::1 >&2")
93 client_defaults.fail("ping -c 1 2001:db8:: >&2")
95 with subtest("Local link addresses can be obtained and pinged"):
96 for client in clients:
97 client_ip = wait_for_address(client, "eth1", "link")
98 server_ip = wait_for_address(server, "eth1", "link")
99 client.succeed(f"ping -c 1 {client_ip}%eth1 >&2")
100 client.succeed(f"ping -c 1 {server_ip}%eth1 >&2")
102 with subtest("Global addresses can be obtained, pinged, and reached via http"):
103 for client in clients:
104 client_ip = wait_for_address(client, "eth1", "global")
105 server_ip = wait_for_address(server, "eth1", "global")
106 client.succeed(f"ping -c 1 {client_ip} >&2")
107 client.succeed(f"ping -c 1 {server_ip} >&2")
108 client.succeed(f"curl --fail -g http://[{server_ip}]")
109 client.fail(f"curl --fail -g http://[{client_ip}]")
112 "Privacy extensions: Global temporary address is used as default source address"
114 ip = wait_for_address(client_defaults, "eth1", "global", temporary=True)
115 # Default route should have "src <temporary address>" in it
116 client_defaults.succeed(f"ip route get 2001:db8:: | grep 'src {ip}'")
118 for client, setting_desc in (
119 (client_global_setting, "global"),
120 (client_interface_setting, "interface"),
122 with subtest(f'Privacy extensions: "enabled" through {setting_desc} setting)'):
123 # We should be obtaining both a temporary address and an EUI-64 address...
124 ip = wait_for_address(client, "eth1", "global")
126 ip_temp = wait_for_address(client, "eth1", "global", temporary=True)
127 # But using the EUI-64 one.
128 client.succeed(f"ip route get 2001:db8:: | grep 'src {ip}'")