1# This test verifies that we can request and assign IPv6 prefixes from upstream
2# (e.g. ISP) routers.
3# The setup consists 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 (
11 { pkgs, lib, ... }:
12 {
13 name = "systemd-networkd-ipv6-prefix-delegation";
14 meta = with lib.maintainers; {
15 maintainers = [
16 andir
17 hexa
18 ];
19 };
20 nodes = {
21
22 # The ISP's routers job is to delegate IPv6 prefixes via DHCPv6. Like with
23 # regular IPv6 auto-configuration it will also emit IPv6 router
24 # advertisements (RAs). Those RA's will not carry a prefix but in contrast
25 # just set the "Other" flag to indicate to the receiving nodes that they
26 # should attempt DHCPv6.
27 #
28 # Note: On the ISPs device we don't really care if we are using networkd in
29 # this example. That being said we can't use it (yet) as networkd doesn't
30 # implement the serving side of DHCPv6. We will use ISC Kea for that task.
31 isp =
32 { lib, pkgs, ... }:
33 {
34 virtualisation.vlans = [ 1 ];
35 networking = {
36 useDHCP = false;
37 firewall.enable = false;
38 interfaces.eth1 = lib.mkForce { }; # Don't use scripted networking
39 };
40
41 systemd.network = {
42 enable = true;
43
44 networks = {
45 "eth1" = {
46 matchConfig.Name = "eth1";
47 address = [
48 "2001:DB8::1/64"
49 ];
50 networkConfig.IPv4Forwarding = true;
51 networkConfig.IPv6Forwarding = true;
52 };
53 };
54 };
55
56 # Since we want to program the routes that we delegate to the "customer"
57 # into our routing table we must provide kea with the required capability.
58 systemd.services.kea-dhcp6-server.serviceConfig = {
59 AmbientCapabilities = [ "CAP_NET_ADMIN" ];
60 CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
61 };
62
63 services = {
64 # Configure the DHCPv6 server to hand out both IA_NA and IA_PD.
65 #
66 # We will hand out /48 prefixes from the subnet 2001:DB8:F000::/36.
67 # That gives us ~8k prefixes. That should be enough for this test.
68 #
69 # Since (usually) you will not receive a prefix with the router
70 # advertisements we also hand out /128 leases from the range
71 # 2001:DB8:0000:0000:FFFF::/112.
72 kea.dhcp6 = {
73 enable = true;
74 settings = {
75 interfaces-config.interfaces = [ "eth1" ];
76 subnet6 = [
77 {
78 id = 1;
79 interface = "eth1";
80 subnet = "2001:DB8::/32";
81 pd-pools = [
82 {
83 prefix = "2001:DB8:1000::";
84 prefix-len = 36;
85 delegated-len = 48;
86 }
87 ];
88 pools = [
89 {
90 pool = "2001:DB8:0000:0000::-2001:DB8:0FFF:FFFF::FFFF";
91 }
92 ];
93 }
94 ];
95
96 # This is the glue between Kea and the Kernel FIB. DHCPv6
97 # rightfully has no concept of setting up a route in your
98 # FIB. This step really depends on your setup.
99 #
100 # In a production environment your DHCPv6 server is likely
101 # not the router. You might want to consider BGP, NETCONF
102 # calls, … in those cases.
103 #
104 # In this example we use the run script hook, that lets use
105 # execute anything and passes information via the environment.
106 # https://kea.readthedocs.io/en/kea-2.2.0/arm/hooks.html#run-script-run-script-support-for-external-hook-scripts
107 hooks-libraries = [
108 {
109 library = "${pkgs.kea}/lib/kea/hooks/libdhcp_run_script.so";
110 parameters = {
111 name = pkgs.writeShellScript "kea-run-hooks" ''
112 export PATH="${
113 lib.makeBinPath (
114 with pkgs;
115 [
116 coreutils
117 iproute2
118 ]
119 )
120 }"
121
122 set -euxo pipefail
123
124 leases6_committed() {
125 for i in $(seq $LEASES6_SIZE); do
126 idx=$((i-1))
127 prefix_var="LEASES6_AT''${idx}_ADDRESS"
128 plen_var="LEASES6_AT''${idx}_PREFIX_LEN"
129
130 ip -6 route replace ''${!prefix_var}/''${!plen_var} via $QUERY6_REMOTE_ADDR dev $QUERY6_IFACE_NAME
131 done
132 }
133
134 unknown_handler() {
135 echo "Unhandled function call ''${*}"
136 exit 123
137 }
138
139 case "$1" in
140 "leases6_committed")
141 leases6_committed
142 ;;
143 *)
144 unknown_handler "''${@}"
145 ;;
146 esac
147 '';
148 sync = false;
149 };
150 }
151 ];
152 };
153 };
154
155 # Finally we have to set up the router advertisements. While we could be
156 # using networkd or bird for this task `radvd` is probably the most
157 # venerable of them all. It was made explicitly for this purpose and
158 # the configuration is much more straightforward than what networkd
159 # requires.
160 # As outlined above we will have to set the `Managed` flag as otherwise
161 # the clients will not know if they should do DHCPv6. (Some do
162 # anyway/always)
163 radvd = {
164 enable = true;
165 config = ''
166 interface eth1 {
167 AdvSendAdvert on;
168 AdvManagedFlag on;
169 AdvOtherConfigFlag off; # we don't really have DNS or NTP or anything like that to distribute
170 prefix ::/64 {
171 AdvOnLink on;
172 AdvAutonomous on;
173 };
174 };
175 '';
176 };
177
178 };
179 };
180
181 # This will be our (residential) router that receives the IPv6 prefix (IA_PD)
182 # and /128 (IA_NA) allocation.
183 #
184 # Here we will actually start using networkd.
185 router = {
186 virtualisation.vlans = [
187 1
188 2
189 ];
190 systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
191
192 boot.kernel.sysctl = {
193 # we want to forward packets from the ISP to the client and back.
194 "net.ipv6.conf.all.forwarding" = 1;
195 };
196
197 networking = {
198 useNetworkd = true;
199 useDHCP = false;
200 # Consider enabling this in production and generating firewall rules
201 # for fowarding/input from the configured interfaces so you do not have
202 # to manage multiple places
203 firewall.enable = false;
204 interfaces.eth1.ipv6.addresses = lib.mkForce [ ];
205 };
206
207 systemd.network = {
208 networks = {
209 # systemd-networkd will load the first network unit file
210 # that matches, ordered lexiographically by filename.
211 # /etc/systemd/network/{40-eth1,99-main}.network already
212 # exists. This network unit must be loaded for the test,
213 # however, hence why this network is named such.
214
215 # Configuration of the interface to the ISP.
216 # We must request accept RAs and request the PD prefix.
217 "01-eth1" = {
218 name = "eth1";
219 networkConfig = {
220 Description = "ISP interface";
221 IPv6AcceptRA = true;
222 #DHCP = false; # no need for legacy IP
223 };
224 linkConfig = {
225 # We care about this interface when talking about being "online".
226 # If this interface is in the `routable` state we can reach
227 # others and they should be able to reach us.
228 RequiredForOnline = "routable";
229 };
230 # This configures the DHCPv6 client part towards the ISPs DHCPv6 server.
231 dhcpV6Config = {
232 # We have to include a request for a prefix in our DHCPv6 client
233 # request packets.
234 # Otherwise the upstream DHCPv6 server wouldn't know if we want a
235 # prefix or not. Note: On some installation it makes sense to
236 # always force that option on the DHPCv6 server since there are
237 # certain CPEs that are just not setting this field but happily
238 # accept the delegated prefix.
239 PrefixDelegationHint = "::/48";
240 };
241 ipv6SendRAConfig = {
242 # Let networkd know that we would very much like to use DHCPv6
243 # to obtain the "managed" information. Not sure why they can't
244 # just take that from the upstream RAs.
245 Managed = true;
246 };
247 };
248
249 # Interface to the client. Here we should redistribute a /64 from
250 # the prefix we received from the ISP.
251 "01-eth2" = {
252 name = "eth2";
253 networkConfig = {
254 Description = "Client interface";
255 # The client shouldn't be allowed to send us RAs, that would be weird.
256 IPv6AcceptRA = false;
257
258 # Delegate prefixes from the DHCPv6 PD pool.
259 DHCPPrefixDelegation = true;
260 IPv6SendRA = true;
261 };
262
263 # In a production environment you should consider setting these as well:
264 # ipv6SendRAConfig = {
265 #EmitDNS = true;
266 #EmitDomains = true;
267 #DNS= = "fe80::1"; # or whatever "well known" IP your router will have on the inside.
268 # };
269
270 # This adds a "random" ULA prefix to the interface that is being
271 # advertised to the clients.
272 # Not used in this test.
273 # ipv6Prefixes = [
274 # {
275 # ipv6PrefixConfig = {
276 # AddressAutoconfiguration = true;
277 # PreferredLifetimeSec = 1800;
278 # ValidLifetimeSec = 1800;
279 # };
280 # }
281 # ];
282 };
283
284 # finally we are going to add a static IPv6 unique local address to
285 # the "lo" interface. This will serve as ICMPv6 echo target to
286 # verify connectivity from the client to the router.
287 "01-lo" = {
288 name = "lo";
289 addresses = [
290 { Address = "FD42::1/128"; }
291 ];
292 };
293 };
294 };
295 };
296
297 # This is the client behind the router. We should be receiving router
298 # advertisements for both the ULA and the delegated prefix.
299 # All we have to do is boot with the default (networkd) configuration.
300 client = {
301 virtualisation.vlans = [ 2 ];
302 systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
303 networking = {
304 useNetworkd = true;
305 useDHCP = false;
306 interfaces.eth1.ipv6.addresses = lib.mkForce [ ];
307 };
308 };
309 };
310
311 testScript = ''
312 # First start the router and wait for it it reach a state where we are
313 # certain networkd is up and it is able to send out RAs
314 router.start()
315 router.wait_for_unit("systemd-networkd.service")
316
317 # After that we can boot the client and wait for the network online target.
318 # Since we only care about IPv6 that should not involve waiting for legacy
319 # IP leases.
320 client.start()
321 client.systemctl("start network-online.target")
322 client.wait_for_unit("network-online.target")
323
324 # the static address on the router should not be reachable
325 client.wait_until_succeeds("ping -6 -c 1 FD42::1")
326
327 # the global IP of the ISP router should still not be a reachable
328 router.fail("ping -6 -c 1 2001:DB8::1")
329
330 # Once we have internal connectivity boot up the ISP
331 isp.start()
332
333 # Since for the ISP "being online" should have no real meaning we just
334 # wait for the target where all the units have been started.
335 # It probably still takes a few more seconds for all the RA timers to be
336 # fired etc..
337 isp.wait_for_unit("multi-user.target")
338
339 # wait until the uplink interface has a good status
340 router.systemctl("start network-online.target")
341 router.wait_for_unit("network-online.target")
342 router.wait_until_succeeds("ping -6 -c1 2001:DB8::1")
343
344 # shortly after that the client should have received it's global IPv6
345 # address and thus be able to ping the ISP
346 client.wait_until_succeeds("ping -6 -c1 2001:DB8::1")
347
348 # verify that we got a globally scoped address in eth1 from the
349 # documentation prefix
350 ip_output = client.succeed("ip --json -6 address show dev eth1")
351
352 import json
353
354 ip_json = json.loads(ip_output)[0]
355 assert any(
356 addr["local"].upper().startswith("2001:DB8:")
357 for addr in ip_json["addr_info"]
358 if addr["scope"] == "global"
359 )
360 '';
361 }
362)