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