1# This test verifies that we can request and assign IPv6 prefixes from upstream
2# (e.g. ISP) routers.
3# The setup consits of three VMs. One for the ISP, as your residential router
4# and the third as a client machine in the residential network.
5#
6# There are two VLANs in this test:
7# - VLAN 1 is the connection between the ISP and the router
8# - VLAN 2 is the connection between the router and the client
9
10import ./make-test-python.nix ({pkgs, ...}: {
11 name = "systemd-networkd-ipv6-prefix-delegation";
12 meta = with pkgs.lib.maintainers; {
13 maintainers = [ andir ];
14 };
15 nodes = {
16
17 # The ISP's routers job is to delegate IPv6 prefixes via DHCPv6. Like with
18 # regular IPv6 auto-configuration it will also emit IPv6 router
19 # advertisements (RAs). Those RA's will not carry a prefix but in contrast
20 # just set the "Other" flag to indicate to the receiving nodes that they
21 # should attempt DHCPv6.
22 #
23 # Note: On the ISPs device we don't really care if we are using networkd in
24 # this example. That being said we can't use it (yet) as networkd doesn't
25 # implement the serving side of DHCPv6. We will use ISC's well aged dhcpd6
26 # for that task.
27 isp = { lib, pkgs, ... }: {
28 virtualisation.vlans = [ 1 ];
29 networking = {
30 useDHCP = false;
31 firewall.enable = false;
32 interfaces.eth1.ipv4.addresses = lib.mkForce []; # no need for legacy IP
33 interfaces.eth1.ipv6.addresses = lib.mkForce [
34 { address = "2001:DB8::1"; prefixLength = 64; }
35 ];
36 };
37
38 # Since we want to program the routes that we delegate to the "customer"
39 # into our routing table we must have a way to gain the required privs.
40 # This security wrapper will do in our test setup.
41 #
42 # DO NOT COPY THIS TO PRODUCTION AS IS. Think about it at least twice.
43 # Everyone on the "isp" machine will be able to add routes to the kernel.
44 security.wrappers.add-dhcpd-lease = {
45 owner = "root";
46 group = "root";
47 source = pkgs.writeShellScript "add-dhcpd-lease" ''
48 exec ${pkgs.iproute2}/bin/ip -6 route replace "$1" via "$2"
49 '';
50 capabilities = "cap_net_admin+ep";
51 };
52 services = {
53 # Configure the DHCPv6 server
54 #
55 # We will hand out /48 prefixes from the subnet 2001:DB8:F000::/36.
56 # That gives us ~8k prefixes. That should be enough for this test.
57 #
58 # Since (usually) you will not receive a prefix with the router
59 # advertisements we also hand out /128 leases from the range
60 # 2001:DB8:0000:0000:FFFF::/112.
61 dhcpd6 = {
62 enable = true;
63 interfaces = [ "eth1" ];
64 extraConfig = ''
65 subnet6 2001:DB8::/36 {
66 range6 2001:DB8:0000:0000:FFFF:: 2001:DB8:0000:0000:FFFF::FFFF;
67 prefix6 2001:DB8:F000:: 2001:DB8:FFFF:: /48;
68 }
69
70 # This is the secret sauce. We have to extract the prefix and the
71 # next hop when commiting the lease to the database. dhcpd6
72 # (rightfully) has not concept of adding routes to the systems
73 # routing table. It really depends on the setup.
74 #
75 # In a production environment your DHCPv6 server is likely not the
76 # router. You might want to consider BGP, custom NetConf calls, …
77 # in those cases.
78 on commit {
79 set IP = pick-first-value(binary-to-ascii(16, 16, ":", substring(option dhcp6.ia-na, 16, 16)), "n/a");
80 set Prefix = pick-first-value(binary-to-ascii(16, 16, ":", suffix(option dhcp6.ia-pd, 16)), "n/a");
81 set PrefixLength = pick-first-value(binary-to-ascii(10, 8, ":", substring(suffix(option dhcp6.ia-pd, 17), 0, 1)), "n/a");
82 log(concat(IP, " ", Prefix, " ", PrefixLength));
83 execute("/run/wrappers/bin/add-dhcpd-lease", concat(Prefix,"/",PrefixLength), IP);
84 }
85 '';
86 };
87
88 # Finally we have to set up the router advertisements. While we could be
89 # using networkd or bird for this task `radvd` is probably the most
90 # venerable of them all. It was made explicitly for this purpose and
91 # the configuration is much more straightforward than what networkd
92 # requires.
93 # As outlined above we will have to set the `Managed` flag as otherwise
94 # the clients will not know if they should do DHCPv6. (Some do
95 # anyway/always)
96 radvd = {
97 enable = true;
98 config = ''
99 interface eth1 {
100 AdvSendAdvert on;
101 AdvManagedFlag on;
102 AdvOtherConfigFlag off; # we don't really have DNS or NTP or anything like that to distribute
103 prefix ::/64 {
104 AdvOnLink on;
105 AdvAutonomous on;
106 };
107 };
108 '';
109 };
110
111 };
112 };
113
114 # This will be our (residential) router that receives the IPv6 prefix (IA_PD)
115 # and /128 (IA_NA) allocation.
116 #
117 # Here we will actually start using networkd.
118 router = {
119 virtualisation.vlans = [ 1 2 ];
120 systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
121
122 boot.kernel.sysctl = {
123 # we want to forward packets from the ISP to the client and back.
124 "net.ipv6.conf.all.forwarding" = 1;
125 };
126
127 networking = {
128 useNetworkd = true;
129 useDHCP = false;
130 # Consider enabling this in production and generating firewall rules
131 # for fowarding/input from the configured interfaces so you do not have
132 # to manage multiple places
133 firewall.enable = false;
134 };
135
136 systemd.network = {
137 networks = {
138 # systemd-networkd will load the first network unit file
139 # that matches, ordered lexiographically by filename.
140 # /etc/systemd/network/{40-eth1,99-main}.network already
141 # exists. This network unit must be loaded for the test,
142 # however, hence why this network is named such.
143
144 # Configuration of the interface to the ISP.
145 # We must request accept RAs and request the PD prefix.
146 "01-eth1" = {
147 name = "eth1";
148 networkConfig = {
149 Description = "ISP interface";
150 IPv6AcceptRA = true;
151 #DHCP = false; # no need for legacy IP
152 };
153 linkConfig = {
154 # We care about this interface when talking about being "online".
155 # If this interface is in the `routable` state we can reach
156 # others and they should be able to reach us.
157 RequiredForOnline = "routable";
158 };
159 # This configures the DHCPv6 client part towards the ISPs DHCPv6 server.
160 dhcpV6Config = {
161 # We have to include a request for a prefix in our DHCPv6 client
162 # request packets.
163 # Otherwise the upstream DHCPv6 server wouldn't know if we want a
164 # prefix or not. Note: On some installation it makes sense to
165 # always force that option on the DHPCv6 server since there are
166 # certain CPEs that are just not setting this field but happily
167 # accept the delegated prefix.
168 PrefixDelegationHint = "::/48";
169 };
170 ipv6SendRAConfig = {
171 # Let networkd know that we would very much like to use DHCPv6
172 # to obtain the "managed" information. Not sure why they can't
173 # just take that from the upstream RAs.
174 Managed = true;
175 };
176 };
177
178 # Interface to the client. Here we should redistribute a /64 from
179 # the prefix we received from the ISP.
180 "01-eth2" = {
181 name = "eth2";
182 networkConfig = {
183 Description = "Client interface";
184 # The client shouldn't be allowed to send us RAs, that would be weird.
185 IPv6AcceptRA = false;
186
187 # Delegate prefixes from the DHCPv6 PD pool.
188 DHCPv6PrefixDelegation = true;
189 IPv6SendRA = true;
190 };
191
192 # In a production environment you should consider setting these as well:
193 # ipv6SendRAConfig = {
194 #EmitDNS = true;
195 #EmitDomains = true;
196 #DNS= = "fe80::1"; # or whatever "well known" IP your router will have on the inside.
197 # };
198
199 # This adds a "random" ULA prefix to the interface that is being
200 # advertised to the clients.
201 # Not used in this test.
202 # ipv6Prefixes = [
203 # {
204 # ipv6PrefixConfig = {
205 # AddressAutoconfiguration = true;
206 # PreferredLifetimeSec = 1800;
207 # ValidLifetimeSec = 1800;
208 # };
209 # }
210 # ];
211 };
212
213 # finally we are going to add a static IPv6 unique local address to
214 # the "lo" interface. This will serve as ICMPv6 echo target to
215 # verify connectivity from the client to the router.
216 "01-lo" = {
217 name = "lo";
218 addresses = [
219 { addressConfig.Address = "FD42::1/128"; }
220 ];
221 };
222 };
223 };
224
225 # make the network-online target a requirement, we wait for it in our test script
226 systemd.targets.network-online.wantedBy = [ "multi-user.target" ];
227 };
228
229 # This is the client behind the router. We should be receving router
230 # advertisements for both the ULA and the delegated prefix.
231 # All we have to do is boot with the default (networkd) configuration.
232 client = {
233 virtualisation.vlans = [ 2 ];
234 systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
235 networking = {
236 useNetworkd = true;
237 useDHCP = false;
238 };
239
240 # make the network-online target a requirement, we wait for it in our test script
241 systemd.targets.network-online.wantedBy = [ "multi-user.target" ];
242 };
243 };
244
245 testScript = ''
246 # First start the router and wait for it it reach a state where we are
247 # certain networkd is up and it is able to send out RAs
248 router.start()
249 router.wait_for_unit("systemd-networkd.service")
250
251 # After that we can boot the client and wait for the network online target.
252 # Since we only care about IPv6 that should not involve waiting for legacy
253 # IP leases.
254 client.start()
255 client.wait_for_unit("network-online.target")
256
257 # the static address on the router should not be reachable
258 client.wait_until_succeeds("ping -6 -c 1 FD42::1")
259
260 # the global IP of the ISP router should still not be a reachable
261 router.fail("ping -6 -c 1 2001:DB8::1")
262
263 # Once we have internal connectivity boot up the ISP
264 isp.start()
265
266 # Since for the ISP "being online" should have no real meaning we just
267 # wait for the target where all the units have been started.
268 # It probably still takes a few more seconds for all the RA timers to be
269 # fired etc..
270 isp.wait_for_unit("multi-user.target")
271
272 # wait until the uplink interface has a good status
273 router.wait_for_unit("network-online.target")
274 router.wait_until_succeeds("ping -6 -c1 2001:DB8::1")
275
276 # shortly after that the client should have received it's global IPv6
277 # address and thus be able to ping the ISP
278 client.wait_until_succeeds("ping -6 -c1 2001:DB8::1")
279
280 # verify that we got a globally scoped address in eth1 from the
281 # documentation prefix
282 ip_output = client.succeed("ip --json -6 address show dev eth1")
283
284 import json
285
286 ip_json = json.loads(ip_output)[0]
287 assert any(
288 addr["local"].upper().startswith("2001:DB8:")
289 for addr in ip_json["addr_info"]
290 if addr["scope"] == "global"
291 )
292 '';
293})