nixos/wireguard-networkd: init (#259092)

authored by misuzu.tngl.sh and committed by GitHub afe27494 d88cb592

+458 -17
+2
nixos/doc/manual/release-notes/rl-2505.section.md
··· 102 102 103 103 - Cinnamon has been updated to 6.4. 104 104 105 + - `networking.wireguard` now has an optional networkd backend. It is enabled by default when `networking.useNetworkd` is enabled, and it can be enabled alongside scripted networking with `networking.wireguard.useNetworkd`. Some `networking.wireguard` options have slightly different behavior with the networkd and script-based backends, documented in each option. Before upgrading, make sure the `privateKeyFile` and `presharedKeyFile` paths are readable by the `systemd-network` user if using the networkd backend. 106 + 105 107 - `services.avahi.ipv6` now defaults to true. 106 108 107 109 - `bind.cacheNetworks` now only controls access for recursive queries, where it previously controlled access for all queries.
+1
nixos/modules/module-list.nix
··· 1286 1286 ./services/networking/wg-quick.nix 1287 1287 ./services/networking/wgautomesh.nix 1288 1288 ./services/networking/wireguard.nix 1289 + ./services/networking/wireguard-networkd.nix 1289 1290 ./services/networking/wpa_supplicant.nix 1290 1291 ./services/networking/wstunnel.nix 1291 1292 ./services/networking/x2goserver.nix
+207
nixos/modules/services/networking/wireguard-networkd.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + 8 + let 9 + inherit (lib) types; 10 + inherit (lib.attrsets) 11 + filterAttrs 12 + mapAttrs 13 + mapAttrs' 14 + mapAttrsToList 15 + nameValuePair 16 + ; 17 + inherit (lib.lists) concatMap concatLists; 18 + inherit (lib.modules) mkIf; 19 + inherit (lib.options) literalExpression mkOption; 20 + inherit (lib.strings) hasInfix; 21 + inherit (lib.trivial) flip; 22 + 23 + removeNulls = filterAttrs (_: v: v != null); 24 + 25 + generateNetdev = 26 + name: interface: 27 + nameValuePair "40-${name}" { 28 + netdevConfig = removeNulls { 29 + Kind = "wireguard"; 30 + Name = name; 31 + MTUBytes = interface.mtu; 32 + }; 33 + wireguardConfig = removeNulls { 34 + PrivateKeyFile = interface.privateKeyFile; 35 + ListenPort = interface.listenPort; 36 + FirewallMark = interface.fwMark; 37 + RouteTable = if interface.allowedIPsAsRoutes then interface.table else null; 38 + RouteMetric = interface.metric; 39 + }; 40 + wireguardPeers = map generateWireguardPeer interface.peers; 41 + }; 42 + 43 + generateWireguardPeer = 44 + peer: 45 + removeNulls { 46 + PublicKey = peer.publicKey; 47 + PresharedKeyFile = peer.presharedKeyFile; 48 + AllowedIPs = peer.allowedIPs; 49 + Endpoint = peer.endpoint; 50 + PersistentKeepalive = peer.persistentKeepalive; 51 + }; 52 + 53 + generateNetwork = name: interface: { 54 + matchConfig.Name = name; 55 + address = interface.ips; 56 + }; 57 + 58 + cfg = config.networking.wireguard; 59 + 60 + refreshEnabledInterfaces = filterAttrs ( 61 + name: interface: interface.dynamicEndpointRefreshSeconds != 0 62 + ) cfg.interfaces; 63 + 64 + generateRefreshTimer = 65 + name: interface: 66 + nameValuePair "wireguard-dynamic-refresh-${name}" { 67 + partOf = [ "wireguard-dynamic-refresh-${name}.service" ]; 68 + wantedBy = [ "timers.target" ]; 69 + description = "Wireguard dynamic endpoint refresh (${name}) timer"; 70 + timerConfig.OnBootSec = interface.dynamicEndpointRefreshSeconds; 71 + timerConfig.OnUnitInactiveSec = interface.dynamicEndpointRefreshSeconds; 72 + }; 73 + 74 + generateRefreshService = 75 + name: interface: 76 + nameValuePair "wireguard-dynamic-refresh-${name}" { 77 + description = "Wireguard dynamic endpoint refresh (${name})"; 78 + after = [ "network-online.target" ]; 79 + wants = [ "network-online.target" ]; 80 + path = with pkgs; [ 81 + iproute2 82 + systemd 83 + ]; 84 + # networkd doesn't provide a mechanism for refreshing endpoints. 85 + # See: https://github.com/systemd/systemd/issues/9911 86 + # This hack does the job but takes down the whole interface to do it. 87 + script = '' 88 + ip link delete ${name} 89 + networkctl reload 90 + ''; 91 + }; 92 + 93 + in 94 + { 95 + meta.maintainers = [ lib.maintainers.majiir ]; 96 + 97 + options.networking.wireguard = { 98 + useNetworkd = mkOption { 99 + default = config.networking.useNetworkd; 100 + defaultText = literalExpression "config.networking.useNetworkd"; 101 + type = types.bool; 102 + description = '' 103 + Whether to use networkd as the network configuration backend for 104 + Wireguard instead of the legacy script-based system. 105 + 106 + ::: {.warning} 107 + Some options have slightly different behavior with the networkd and 108 + script-based backends. Check the documentation for each Wireguard 109 + option you use before enabling this option. 110 + ::: 111 + ''; 112 + }; 113 + }; 114 + 115 + config = mkIf (cfg.enable && cfg.useNetworkd) { 116 + 117 + # TODO: Some of these options may be possible to support in networkd. 118 + # 119 + # privateKey and presharedKey are trivial to support, but we deliberately 120 + # don't in order to discourage putting secrets in the /nix store. 121 + # 122 + # generatePrivateKeyFile can be supported if we can order a service before 123 + # networkd configures interfaces. There is also a systemd feature request 124 + # for key generation: https://github.com/systemd/systemd/issues/14282 125 + # 126 + # preSetup, postSetup, preShutdown and postShutdown may be possible, but 127 + # networkd is not likely to support script hooks like this directly. See: 128 + # https://github.com/systemd/systemd/issues/11629 129 + # 130 + # socketNamespace and interfaceNamespace can be implemented once networkd 131 + # supports setting a netdev's namespace. See: 132 + # https://github.com/systemd/systemd/issues/11629 133 + # https://github.com/systemd/systemd/pull/14915 134 + 135 + assertions = concatLists ( 136 + flip mapAttrsToList cfg.interfaces ( 137 + name: interface: 138 + [ 139 + # Interface assertions 140 + { 141 + assertion = interface.privateKey == null; 142 + message = "networking.wireguard.interfaces.${name}.privateKey cannot be used with networkd. Use privateKeyFile instead."; 143 + } 144 + { 145 + assertion = !interface.generatePrivateKeyFile; 146 + message = "networking.wireguard.interfaces.${name}.generatePrivateKeyFile cannot be used with networkd."; 147 + } 148 + { 149 + assertion = interface.preSetup == ""; 150 + message = "networking.wireguard.interfaces.${name}.preSetup cannot be used with networkd."; 151 + } 152 + { 153 + assertion = interface.postSetup == ""; 154 + message = "networking.wireguard.interfaces.${name}.postSetup cannot be used with networkd."; 155 + } 156 + { 157 + assertion = interface.preShutdown == ""; 158 + message = "networking.wireguard.interfaces.${name}.preShutdown cannot be used with networkd."; 159 + } 160 + { 161 + assertion = interface.postShutdown == ""; 162 + message = "networking.wireguard.interfaces.${name}.postShutdown cannot be used with networkd."; 163 + } 164 + { 165 + assertion = interface.socketNamespace == null; 166 + message = "networking.wireguard.interfaces.${name}.socketNamespace cannot be used with networkd."; 167 + } 168 + { 169 + assertion = interface.interfaceNamespace == null; 170 + message = "networking.wireguard.interfaces.${name}.interfaceNamespace cannot be used with networkd."; 171 + } 172 + ] 173 + ++ flip concatMap interface.ips (ip: [ 174 + # IP assertions 175 + { 176 + assertion = hasInfix "/" ip; 177 + message = "networking.wireguard.interfaces.${name}.ips value \"${ip}\" requires a subnet (e.g. 192.0.2.1/32) with networkd."; 178 + } 179 + ]) 180 + ++ flip concatMap interface.peers (peer: [ 181 + # Peer assertions 182 + { 183 + assertion = peer.presharedKey == null; 184 + message = "networking.wireguard.interfaces.${name}.peers[].presharedKey cannot be used with networkd. Use presharedKeyFile instead."; 185 + } 186 + { 187 + assertion = peer.dynamicEndpointRefreshSeconds == null; 188 + message = "networking.wireguard.interfaces.${name}.peers[].dynamicEndpointRefreshSeconds cannot be used with networkd. Use networking.wireguard.interfaces.${name}.dynamicEndpointRefreshSeconds instead."; 189 + } 190 + { 191 + assertion = peer.dynamicEndpointRefreshRestartSeconds == null; 192 + message = "networking.wireguard.interfaces.${name}.peers[].dynamicEndpointRefreshRestartSeconds cannot be used with networkd."; 193 + } 194 + ]) 195 + ) 196 + ); 197 + 198 + systemd.network = { 199 + enable = true; 200 + netdevs = mapAttrs' generateNetdev cfg.interfaces; 201 + networks = mapAttrs generateNetwork cfg.interfaces; 202 + }; 203 + 204 + systemd.timers = mapAttrs' generateRefreshTimer refreshEnabledInterfaces; 205 + systemd.services = mapAttrs' generateRefreshService refreshEnabledInterfaces; 206 + }; 207 + }
+54 -17
nixos/modules/services/networking/wireguard.nix
··· 49 49 default = null; 50 50 description = '' 51 51 Private key file as generated by {command}`wg genkey`. 52 + 53 + When {option}`networking.wireguard.useNetworkd` is enabled, this file 54 + must be readable by the `systemd-network` user. 52 55 ''; 53 56 }; 54 57 ··· 182 185 Set the metric of routes related to this Wireguard interface. 183 186 ''; 184 187 }; 188 + 189 + dynamicEndpointRefreshSeconds = mkOption { 190 + default = 0; 191 + example = 300; 192 + type = with types; int; 193 + description = '' 194 + Periodically refresh the endpoint hostname or address for all peers. 195 + Allows WireGuard to notice DNS and IPv4/IPv6 connectivity changes. 196 + This option can be set or overridden for individual peers. 197 + 198 + Setting this to `0` disables periodic refresh. 199 + 200 + ::: {.warning} 201 + When {option}`networking.wireguard.useNetworkd` is enabled, this 202 + option deletes the Wireguard interface and brings it back up by 203 + reconfiguring the network with `networkctl reload` on every refresh. 204 + This could have adverse effects on your network and cause brief 205 + connectivity blips. See [systemd/systemd#9911](https://github.com/systemd/systemd/issues/9911) 206 + for an upstream feature request that can make this less hacky. 207 + ::: 208 + ''; 209 + }; 185 210 }; 186 211 187 212 }; ··· 234 259 Optional, and may be omitted. This option adds an additional layer of 235 260 symmetric-key cryptography to be mixed into the already existing 236 261 public-key cryptography, for post-quantum resistance. 262 + 263 + When {option}`networking.wireguard.useNetworkd` is enabled, this file 264 + must be readable by the `systemd-network` user. 237 265 ''; 238 266 }; 239 267 ··· 269 297 }; 270 298 271 299 dynamicEndpointRefreshSeconds = mkOption { 272 - default = 0; 300 + default = null; 301 + defaultText = literalExpression "config.networking.wireguard.interfaces.<name>.dynamicEndpointRefreshSeconds"; 273 302 example = 5; 274 - type = with types; int; 303 + type = with types; nullOr int; 275 304 description = '' 276 305 Periodically re-execute the `wg` utility every 277 306 this many seconds in order to let WireGuard notice DNS / hostname 278 307 changes. 279 308 280 309 Setting this to `0` disables periodic reexecution. 310 + 311 + ::: {.note} 312 + This peer-level setting is not available when {option}`networking.wireguard.useNetworkd` 313 + is enabled. The interface-level setting may be used instead. 314 + ::: 281 315 ''; 282 316 }; 283 317 ··· 349 383 in 350 384 "wireguard-${interfaceName}-peer-${peerName}${refreshSuffix}"; 351 385 386 + dynamicRefreshSeconds = interfaceCfg: peer: 387 + if peer.dynamicEndpointRefreshSeconds != null 388 + then peer.dynamicEndpointRefreshSeconds 389 + else interfaceCfg.dynamicEndpointRefreshSeconds; 390 + 352 391 generatePeerUnit = { interfaceName, interfaceCfg, peer }: 353 392 let 354 393 psk = ··· 359 398 dst = interfaceCfg.interfaceNamespace; 360 399 ip = nsWrap "ip" src dst; 361 400 wg = nsWrap "wg" src dst; 362 - dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0; 401 + dynamicEndpointRefreshSeconds = dynamicRefreshSeconds interfaceCfg peer; 402 + dynamicRefreshEnabled = dynamicEndpointRefreshSeconds != 0; 363 403 # We generate a different name (a `-refresh` suffix) when `dynamicEndpointRefreshSeconds` 364 404 # to avoid that the same service switches `Type` (`oneshot` vs `simple`), 365 405 # with the intent to make scripting more obvious. ··· 395 435 Restart = "always"; 396 436 RestartSec = if null != peer.dynamicEndpointRefreshRestartSeconds 397 437 then peer.dynamicEndpointRefreshRestartSeconds 398 - else peer.dynamicEndpointRefreshSeconds; 438 + else dynamicEndpointRefreshSeconds; 399 439 }; 400 440 unitConfig = lib.optionalAttrs dynamicRefreshEnabled { 401 441 StartLimitIntervalSec = 0; ··· 419 459 ${wg_setup} 420 460 ${route_setup} 421 461 422 - ${optionalString (peer.dynamicEndpointRefreshSeconds != 0) '' 462 + ${optionalString (dynamicEndpointRefreshSeconds != 0) '' 423 463 # Re-execute 'wg' periodically to notice DNS / hostname changes. 424 464 # Note this will not time out on transient DNS failures such as DNS names 425 465 # because we have set 'WG_ENDPOINT_RESOLUTION_RETRIES=infinity'. 426 466 # Also note that 'wg' limits its maximum retry delay to 20 seconds as of writing. 427 467 while ${wg_setup}; do 428 - sleep "${toString peer.dynamicEndpointRefreshSeconds}"; 468 + sleep "${toString dynamicEndpointRefreshSeconds}"; 429 469 done 430 470 ''} 431 471 ''; ··· 445 485 # the target is required to start new peer units when they are added 446 486 generateInterfaceTarget = name: values: 447 487 let 448 - mkPeerUnit = peer: (peerUnitServiceName name peer.name (peer.dynamicEndpointRefreshSeconds != 0)) + ".service"; 488 + mkPeerUnit = peer: (peerUnitServiceName name peer.name (dynamicRefreshSeconds values peer != 0)) + ".service"; 449 489 in 450 490 nameValuePair "wireguard-${name}" 451 491 rec { ··· 530 570 description = '' 531 571 Whether to enable WireGuard. 532 572 533 - Please note that {option}`systemd.network.netdevs` has more features 534 - and is better maintained. When building new things, it is advised to 535 - use that instead. 573 + ::: {.note} 574 + By default, this module is powered by a script-based backend. You can 575 + enable the networkd backend with {option}`networking.wireguard.useNetworkd`. 576 + ::: 536 577 ''; 537 578 type = types.bool; 538 579 # 2019-05-25: Backwards compatibility. ··· 544 585 interfaces = mkOption { 545 586 description = '' 546 587 WireGuard interfaces. 547 - 548 - Please note that {option}`systemd.network.netdevs` has more features 549 - and is better maintained. When building new things, it is advised to 550 - use that instead. 551 588 ''; 552 589 default = {}; 553 590 example = { ··· 597 634 boot.kernelModules = [ "wireguard" ]; 598 635 environment.systemPackages = [ pkgs.wireguard-tools ]; 599 636 600 - systemd.services = 637 + systemd.services = mkIf (!cfg.useNetworkd) ( 601 638 (mapAttrs' generateInterfaceUnit cfg.interfaces) 602 639 // (listToAttrs (map generatePeerUnit all_peers)) 603 640 // (mapAttrs' generateKeyServiceUnit 604 - (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces)); 641 + (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces))); 605 642 606 - systemd.targets = mapAttrs' generateInterfaceTarget cfg.interfaces; 643 + systemd.targets = mkIf (!cfg.useNetworkd) (mapAttrs' generateInterfaceTarget cfg.interfaces); 607 644 } 608 645 ); 609 646
+3
nixos/tests/wireguard/default.nix
··· 11 11 tests = let callTest = p: args: import p ({ inherit system pkgs; } // args); in { 12 12 basic = callTest ./basic.nix; 13 13 namespaces = callTest ./namespaces.nix; 14 + networkd = callTest ./networkd.nix; 14 15 wg-quick = callTest ./wg-quick.nix; 15 16 wg-quick-nftables = args: callTest ./wg-quick.nix ({ nftables = true; } // args); 16 17 generated = callTest ./generated.nix; 18 + dynamic-refresh = callTest ./dynamic-refresh.nix; 19 + dynamic-refresh-networkd = args: callTest ./dynamic-refresh.nix ({ useNetworkd = true; } // args); 17 20 }; 18 21 in 19 22
+102
nixos/tests/wireguard/dynamic-refresh.nix
··· 1 + import ../make-test-python.nix ( 2 + { 3 + pkgs, 4 + lib, 5 + kernelPackages ? null, 6 + useNetworkd ? false, 7 + ... 8 + }: 9 + let 10 + wg-snakeoil-keys = import ./snakeoil-keys.nix; 11 + in 12 + { 13 + name = "wireguard-dynamic-refresh"; 14 + meta = with lib.maintainers; { 15 + maintainers = [ majiir ]; 16 + }; 17 + 18 + nodes = { 19 + server = { 20 + virtualisation.vlans = [ 21 + 1 22 + 2 23 + ]; 24 + boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; }; 25 + networking.firewall.allowedUDPPorts = [ 23542 ]; 26 + networking.useDHCP = false; 27 + networking.wireguard.useNetworkd = useNetworkd; 28 + networking.wireguard.interfaces.wg0 = { 29 + ips = [ "10.23.42.1/32" ]; 30 + listenPort = 23542; 31 + 32 + # !!! Don't do this with real keys. The /nix store is world-readable! 33 + privateKeyFile = toString (pkgs.writeText "privateKey" wg-snakeoil-keys.peer0.privateKey); 34 + 35 + peers = lib.singleton { 36 + allowedIPs = [ "10.23.42.2/32" ]; 37 + 38 + inherit (wg-snakeoil-keys.peer1) publicKey; 39 + }; 40 + }; 41 + }; 42 + 43 + client = 44 + { nodes, ... }: 45 + { 46 + virtualisation.vlans = [ 47 + 1 48 + 2 49 + ]; 50 + boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; }; 51 + networking.useDHCP = false; 52 + networking.wireguard.useNetworkd = useNetworkd; 53 + networking.wireguard.interfaces.wg0 = { 54 + ips = [ "10.23.42.2/32" ]; 55 + 56 + # !!! Don't do this with real keys. The /nix store is world-readable! 57 + privateKeyFile = toString (pkgs.writeText "privateKey" wg-snakeoil-keys.peer1.privateKey); 58 + 59 + dynamicEndpointRefreshSeconds = 2; 60 + 61 + peers = lib.singleton { 62 + allowedIPs = [ 63 + "0.0.0.0/0" 64 + "::/0" 65 + ]; 66 + endpoint = "server:23542"; 67 + 68 + inherit (wg-snakeoil-keys.peer0) publicKey; 69 + }; 70 + }; 71 + 72 + specialisation.update-hosts.configuration = { 73 + networking.extraHosts = 74 + let 75 + testCfg = nodes.server.virtualisation.test; 76 + in 77 + lib.mkForce "192.168.2.${toString testCfg.nodeNumber} ${testCfg.nodeName}"; 78 + }; 79 + }; 80 + }; 81 + 82 + testScript = 83 + { nodes, ... }: 84 + '' 85 + start_all() 86 + 87 + server.wait_for_unit("network-online.target") 88 + client.wait_for_unit("network-online.target") 89 + 90 + client.succeed("ping -n -w 1 -c 1 10.23.42.1") 91 + 92 + client.succeed("ip link set down eth1") 93 + 94 + client.fail("ping -n -w 1 -c 1 10.23.42.1") 95 + 96 + with client.nested("update hosts file"): 97 + client.succeed("${nodes.client.system.build.toplevel}/specialisation/update-hosts/bin/switch-to-configuration test") 98 + 99 + client.succeed("sleep 5 && ping -n -w 1 -c 1 10.23.42.1") 100 + ''; 101 + } 102 + )
+89
nixos/tests/wireguard/networkd.nix
··· 1 + import ../make-test-python.nix ( 2 + { 3 + pkgs, 4 + lib, 5 + kernelPackages ? null, 6 + ... 7 + }: 8 + let 9 + wg-snakeoil-keys = import ./snakeoil-keys.nix; 10 + peer = (import ./make-peer.nix) { inherit lib; }; 11 + in 12 + { 13 + name = "wireguard-networkd"; 14 + meta = with pkgs.lib.maintainers; { 15 + maintainers = [ majiir ]; 16 + }; 17 + 18 + nodes = { 19 + peer0 = peer { 20 + ip4 = "192.168.0.1"; 21 + ip6 = "fd00::1"; 22 + extraConfig = { 23 + boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; }; 24 + networking.firewall.allowedUDPPorts = [ 23542 ]; 25 + networking.wireguard.useNetworkd = true; 26 + networking.wireguard.interfaces.wg0 = { 27 + ips = [ 28 + "10.23.42.1/32" 29 + "fc00::1/128" 30 + ]; 31 + listenPort = 23542; 32 + 33 + # !!! Don't do this with real keys. The /nix store is world-readable! 34 + privateKeyFile = toString (pkgs.writeText "privateKey" wg-snakeoil-keys.peer0.privateKey); 35 + 36 + peers = lib.singleton { 37 + allowedIPs = [ 38 + "10.23.42.2/32" 39 + "fc00::2/128" 40 + ]; 41 + 42 + inherit (wg-snakeoil-keys.peer1) publicKey; 43 + }; 44 + }; 45 + }; 46 + }; 47 + 48 + peer1 = peer { 49 + ip4 = "192.168.0.2"; 50 + ip6 = "fd00::2"; 51 + extraConfig = { 52 + boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; }; 53 + networking.wireguard.useNetworkd = true; 54 + networking.wireguard.interfaces.wg0 = { 55 + ips = [ 56 + "10.23.42.2/32" 57 + "fc00::2/128" 58 + ]; 59 + listenPort = 23542; 60 + 61 + # !!! Don't do this with real keys. The /nix store is world-readable! 62 + privateKeyFile = toString (pkgs.writeText "privateKey" wg-snakeoil-keys.peer1.privateKey); 63 + 64 + peers = lib.singleton { 65 + allowedIPs = [ 66 + "0.0.0.0/0" 67 + "::/0" 68 + ]; 69 + endpoint = "192.168.0.1:23542"; 70 + persistentKeepalive = 25; 71 + 72 + inherit (wg-snakeoil-keys.peer0) publicKey; 73 + }; 74 + }; 75 + }; 76 + }; 77 + }; 78 + 79 + testScript = '' 80 + start_all() 81 + 82 + peer0.wait_for_unit("systemd-networkd-wait-online.service") 83 + peer1.wait_for_unit("systemd-networkd-wait-online.service") 84 + 85 + peer1.succeed("ping -c5 fc00::1") 86 + peer1.succeed("ping -c5 10.23.42.1") 87 + ''; 88 + } 89 + )