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