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