at master 14 kB view raw
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)