at 22.05-pre 12 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 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})