at 23.11-pre 22 kB view raw
1{ config, lib, options, pkgs, ... }: 2 3with lib; 4 5let 6 7 cfg = config.networking.wireguard; 8 opt = options.networking.wireguard; 9 10 kernel = config.boot.kernelPackages; 11 12 # interface options 13 14 interfaceOpts = { ... }: { 15 16 options = { 17 18 ips = mkOption { 19 example = [ "192.168.2.1/24" ]; 20 default = []; 21 type = with types; listOf str; 22 description = lib.mdDoc "The IP addresses of the interface."; 23 }; 24 25 privateKey = mkOption { 26 example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk="; 27 type = with types; nullOr str; 28 default = null; 29 description = lib.mdDoc '' 30 Base64 private key generated by {command}`wg genkey`. 31 32 Warning: Consider using privateKeyFile instead if you do not 33 want to store the key in the world-readable Nix store. 34 ''; 35 }; 36 37 generatePrivateKeyFile = mkOption { 38 default = false; 39 type = types.bool; 40 description = lib.mdDoc '' 41 Automatically generate a private key with 42 {command}`wg genkey`, at the privateKeyFile location. 43 ''; 44 }; 45 46 privateKeyFile = mkOption { 47 example = "/private/wireguard_key"; 48 type = with types; nullOr str; 49 default = null; 50 description = lib.mdDoc '' 51 Private key file as generated by {command}`wg genkey`. 52 ''; 53 }; 54 55 listenPort = mkOption { 56 default = null; 57 type = with types; nullOr int; 58 example = 51820; 59 description = lib.mdDoc '' 60 16-bit port for listening. Optional; if not specified, 61 automatically generated based on interface name. 62 ''; 63 }; 64 65 preSetup = mkOption { 66 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"''; 67 default = ""; 68 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines; 69 description = lib.mdDoc '' 70 Commands called at the start of the interface setup. 71 ''; 72 }; 73 74 postSetup = mkOption { 75 example = literalExpression '' 76 '''printf "nameserver 10.200.100.1" | ''${pkgs.openresolv}/bin/resolvconf -a wg0 -m 0''' 77 ''; 78 default = ""; 79 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines; 80 description = lib.mdDoc "Commands called at the end of the interface setup."; 81 }; 82 83 postShutdown = mkOption { 84 example = literalExpression ''"''${pkgs.openresolv}/bin/resolvconf -d wg0"''; 85 default = ""; 86 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines; 87 description = lib.mdDoc "Commands called after shutting down the interface."; 88 }; 89 90 table = mkOption { 91 default = "main"; 92 type = types.str; 93 description = lib.mdDoc '' 94 The kernel routing table to add this interface's 95 associated routes to. Setting this is useful for e.g. policy routing 96 ("ip rule") or virtual routing and forwarding ("ip vrf"). Both 97 numeric table IDs and table names (/etc/rt_tables) can be used. 98 Defaults to "main". 99 ''; 100 }; 101 102 peers = mkOption { 103 default = []; 104 description = lib.mdDoc "Peers linked to the interface."; 105 type = with types; listOf (submodule peerOpts); 106 }; 107 108 allowedIPsAsRoutes = mkOption { 109 example = false; 110 default = true; 111 type = types.bool; 112 description = lib.mdDoc '' 113 Determines whether to add allowed IPs as routes or not. 114 ''; 115 }; 116 117 socketNamespace = mkOption { 118 default = null; 119 type = with types; nullOr str; 120 example = "container"; 121 description = lib.mdDoc ''The pre-existing network namespace in which the 122 WireGuard interface is created, and which retains the socket even if the 123 interface is moved via {option}`interfaceNamespace`. When 124 `null`, the interface is created in the init namespace. 125 See [documentation](https://www.wireguard.com/netns/). 126 ''; 127 }; 128 129 interfaceNamespace = mkOption { 130 default = null; 131 type = with types; nullOr str; 132 example = "init"; 133 description = lib.mdDoc ''The pre-existing network namespace the WireGuard 134 interface is moved to. The special value `init` means 135 the init namespace. When `null`, the interface is not 136 moved. 137 See [documentation](https://www.wireguard.com/netns/). 138 ''; 139 }; 140 141 fwMark = mkOption { 142 default = null; 143 type = with types; nullOr str; 144 example = "0x6e6978"; 145 description = lib.mdDoc '' 146 Mark all wireguard packets originating from 147 this interface with the given firewall mark. The firewall mark can be 148 used in firewalls or policy routing to filter the wireguard packets. 149 This can be useful for setup where all traffic goes through the 150 wireguard tunnel, because the wireguard packets need to be routed 151 differently. 152 ''; 153 }; 154 155 mtu = mkOption { 156 default = null; 157 type = with types; nullOr int; 158 example = 1280; 159 description = lib.mdDoc '' 160 Set the maximum transmission unit in bytes for the wireguard 161 interface. Beware that the wireguard packets have a header that may 162 add up to 80 bytes to the mtu. By default, the MTU is (1500 - 80) = 163 1420. However, if the MTU of the upstream network is lower, the MTU 164 of the wireguard network has to be adjusted as well. 165 ''; 166 }; 167 }; 168 169 }; 170 171 # peer options 172 173 peerOpts = self: { 174 175 options = { 176 177 name = mkOption { 178 default = 179 replaceStrings 180 [ "/" "-" " " "+" "=" ] 181 [ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ] 182 self.config.publicKey; 183 defaultText = literalExpression "publicKey"; 184 example = "bernd"; 185 type = types.str; 186 description = lib.mdDoc "Name used to derive peer unit name."; 187 }; 188 189 publicKey = mkOption { 190 example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; 191 type = types.singleLineStr; 192 description = lib.mdDoc "The base64 public key of the peer."; 193 }; 194 195 presharedKey = mkOption { 196 default = null; 197 example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I="; 198 type = with types; nullOr str; 199 description = lib.mdDoc '' 200 Base64 preshared key generated by {command}`wg genpsk`. 201 Optional, and may be omitted. This option adds an additional layer of 202 symmetric-key cryptography to be mixed into the already existing 203 public-key cryptography, for post-quantum resistance. 204 205 Warning: Consider using presharedKeyFile instead if you do not 206 want to store the key in the world-readable Nix store. 207 ''; 208 }; 209 210 presharedKeyFile = mkOption { 211 default = null; 212 example = "/private/wireguard_psk"; 213 type = with types; nullOr str; 214 description = lib.mdDoc '' 215 File pointing to preshared key as generated by {command}`wg genpsk`. 216 Optional, and may be omitted. This option adds an additional layer of 217 symmetric-key cryptography to be mixed into the already existing 218 public-key cryptography, for post-quantum resistance. 219 ''; 220 }; 221 222 allowedIPs = mkOption { 223 example = [ "10.192.122.3/32" "10.192.124.1/24" ]; 224 type = with types; listOf str; 225 description = lib.mdDoc ''List of IP (v4 or v6) addresses with CIDR masks from 226 which this peer is allowed to send incoming traffic and to which 227 outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may 228 be specified for matching all IPv4 addresses, and ::/0 may be specified 229 for matching all IPv6 addresses.''; 230 }; 231 232 endpoint = mkOption { 233 default = null; 234 example = "demo.wireguard.io:12913"; 235 type = with types; nullOr str; 236 description = lib.mdDoc '' 237 Endpoint IP or hostname of the peer, followed by a colon, 238 and then a port number of the peer. 239 240 Warning for endpoints with changing IPs: 241 The WireGuard kernel side cannot perform DNS resolution. 242 Thus DNS resolution is done once by the `wg` userspace 243 utility, when setting up WireGuard. Consequently, if the IP address 244 behind the name changes, WireGuard will not notice. 245 This is especially common for dynamic-DNS setups, but also applies to 246 any other DNS-based setup. 247 If you do not use IP endpoints, you likely want to set 248 {option}`networking.wireguard.dynamicEndpointRefreshSeconds` 249 to refresh the IPs periodically. 250 ''; 251 }; 252 253 dynamicEndpointRefreshSeconds = mkOption { 254 default = 0; 255 example = 5; 256 type = with types; int; 257 description = lib.mdDoc '' 258 Periodically re-execute the `wg` utility every 259 this many seconds in order to let WireGuard notice DNS / hostname 260 changes. 261 262 Setting this to `0` disables periodic reexecution. 263 ''; 264 }; 265 266 dynamicEndpointRefreshRestartSeconds = mkOption { 267 default = null; 268 example = 5; 269 type = with types; nullOr ints.unsigned; 270 description = lib.mdDoc '' 271 When the dynamic endpoint refresh that is configured via 272 dynamicEndpointRefreshSeconds exits (likely due to a failure), 273 restart that service after this many seconds. 274 275 If set to `null` the value of 276 {option}`networking.wireguard.dynamicEndpointRefreshSeconds` 277 will be used as the default. 278 ''; 279 }; 280 281 persistentKeepalive = mkOption { 282 default = null; 283 type = with types; nullOr int; 284 example = 25; 285 description = lib.mdDoc ''This is optional and is by default off, because most 286 users will not need it. It represents, in seconds, between 1 and 65535 287 inclusive, how often to send an authenticated empty packet to the peer, 288 for the purpose of keeping a stateful firewall or NAT mapping valid 289 persistently. For example, if the interface very rarely sends traffic, 290 but it might at anytime receive traffic from a peer, and it is behind 291 NAT, the interface might benefit from having a persistent keepalive 292 interval of 25 seconds; however, most users will not need this.''; 293 }; 294 295 }; 296 297 }; 298 299 generateKeyServiceUnit = name: values: 300 assert values.generatePrivateKeyFile; 301 nameValuePair "wireguard-${name}-key" 302 { 303 description = "WireGuard Tunnel - ${name} - Key Generator"; 304 wantedBy = [ "wireguard-${name}.service" ]; 305 requiredBy = [ "wireguard-${name}.service" ]; 306 before = [ "wireguard-${name}.service" ]; 307 path = with pkgs; [ wireguard-tools ]; 308 309 serviceConfig = { 310 Type = "oneshot"; 311 RemainAfterExit = true; 312 }; 313 314 script = '' 315 set -e 316 317 # If the parent dir does not already exist, create it. 318 # Otherwise, does nothing, keeping existing permissions intact. 319 mkdir -p --mode 0755 "${dirOf values.privateKeyFile}" 320 321 if [ ! -f "${values.privateKeyFile}" ]; then 322 # Write private key file with atomically-correct permissions. 323 (set -e; umask 077; wg genkey > "${values.privateKeyFile}") 324 fi 325 ''; 326 }; 327 328 peerUnitServiceName = interfaceName: peerName: dynamicRefreshEnabled: 329 let 330 refreshSuffix = optionalString dynamicRefreshEnabled "-refresh"; 331 in 332 "wireguard-${interfaceName}-peer-${peerName}${refreshSuffix}"; 333 334 generatePeerUnit = { interfaceName, interfaceCfg, peer }: 335 let 336 psk = 337 if peer.presharedKey != null 338 then pkgs.writeText "wg-psk" peer.presharedKey 339 else peer.presharedKeyFile; 340 src = interfaceCfg.socketNamespace; 341 dst = interfaceCfg.interfaceNamespace; 342 ip = nsWrap "ip" src dst; 343 wg = nsWrap "wg" src dst; 344 dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0; 345 # We generate a different name (a `-refresh` suffix) when `dynamicEndpointRefreshSeconds` 346 # to avoid that the same service switches `Type` (`oneshot` vs `simple`), 347 # with the intent to make scripting more obvious. 348 serviceName = peerUnitServiceName interfaceName peer.name dynamicRefreshEnabled; 349 in nameValuePair serviceName 350 { 351 description = "WireGuard Peer - ${interfaceName} - ${peer.name}" 352 + optionalString (peer.name != peer.publicKey) " (${peer.publicKey})"; 353 requires = [ "wireguard-${interfaceName}.service" ]; 354 wants = [ "network-online.target" ]; 355 after = [ "wireguard-${interfaceName}.service" "network-online.target" ]; 356 wantedBy = [ "wireguard-${interfaceName}.service" ]; 357 environment.DEVICE = interfaceName; 358 environment.WG_ENDPOINT_RESOLUTION_RETRIES = "infinity"; 359 path = with pkgs; [ iproute2 wireguard-tools ]; 360 361 serviceConfig = 362 if !dynamicRefreshEnabled 363 then 364 { 365 Type = "oneshot"; 366 RemainAfterExit = true; 367 } 368 else 369 { 370 Type = "simple"; # re-executes 'wg' indefinitely 371 # Note that `Type = "oneshot"` services with `RemainAfterExit = true` 372 # cannot be used with systemd timers (see `man systemd.timer`), 373 # which is why `simple` with a loop is the best choice here. 374 # It also makes starting and stopping easiest. 375 # 376 # Restart if the service exits (e.g. when wireguard gives up after "Name or service not known" dns failures): 377 Restart = "always"; 378 RestartSec = if null != peer.dynamicEndpointRefreshRestartSeconds 379 then peer.dynamicEndpointRefreshRestartSeconds 380 else peer.dynamicEndpointRefreshSeconds; 381 }; 382 unitConfig = lib.optionalAttrs dynamicRefreshEnabled { 383 StartLimitIntervalSec = 0; 384 }; 385 386 script = let 387 wg_setup = concatStringsSep " " ( 388 [ ''${wg} set ${interfaceName} peer "${peer.publicKey}"'' ] 389 ++ optional (psk != null) ''preshared-key "${psk}"'' 390 ++ optional (peer.endpoint != null) ''endpoint "${peer.endpoint}"'' 391 ++ optional (peer.persistentKeepalive != null) ''persistent-keepalive "${toString peer.persistentKeepalive}"'' 392 ++ optional (peer.allowedIPs != []) ''allowed-ips "${concatStringsSep "," peer.allowedIPs}"'' 393 ); 394 route_setup = 395 optionalString interfaceCfg.allowedIPsAsRoutes 396 (concatMapStringsSep "\n" 397 (allowedIP: 398 ''${ip} route replace "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"'' 399 ) peer.allowedIPs); 400 in '' 401 ${wg_setup} 402 ${route_setup} 403 404 ${optionalString (peer.dynamicEndpointRefreshSeconds != 0) '' 405 # Re-execute 'wg' periodically to notice DNS / hostname changes. 406 # Note this will not time out on transient DNS failures such as DNS names 407 # because we have set 'WG_ENDPOINT_RESOLUTION_RETRIES=infinity'. 408 # Also note that 'wg' limits its maximum retry delay to 20 seconds as of writing. 409 while ${wg_setup}; do 410 sleep "${toString peer.dynamicEndpointRefreshSeconds}"; 411 done 412 ''} 413 ''; 414 415 postStop = let 416 route_destroy = optionalString interfaceCfg.allowedIPsAsRoutes 417 (concatMapStringsSep "\n" 418 (allowedIP: 419 ''${ip} route delete "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"'' 420 ) peer.allowedIPs); 421 in '' 422 ${wg} set "${interfaceName}" peer "${peer.publicKey}" remove 423 ${route_destroy} 424 ''; 425 }; 426 427 # the target is required to start new peer units when they are added 428 generateInterfaceTarget = name: values: 429 let 430 mkPeerUnit = peer: (peerUnitServiceName name peer.name (peer.dynamicEndpointRefreshSeconds != 0)) + ".service"; 431 in 432 nameValuePair "wireguard-${name}" 433 rec { 434 description = "WireGuard Tunnel - ${name}"; 435 wantedBy = [ "multi-user.target" ]; 436 wants = [ "wireguard-${name}.service" ] ++ map mkPeerUnit values.peers; 437 after = wants; 438 }; 439 440 generateInterfaceUnit = name: values: 441 # exactly one way to specify the private key must be set 442 #assert (values.privateKey != null) != (values.privateKeyFile != null); 443 let privKey = if values.privateKeyFile != null then values.privateKeyFile else pkgs.writeText "wg-key" values.privateKey; 444 src = values.socketNamespace; 445 dst = values.interfaceNamespace; 446 ipPreMove = nsWrap "ip" src null; 447 ipPostMove = nsWrap "ip" src dst; 448 wg = nsWrap "wg" src dst; 449 ns = if dst == "init" then "1" else dst; 450 451 in 452 nameValuePair "wireguard-${name}" 453 { 454 description = "WireGuard Tunnel - ${name}"; 455 after = [ "network-pre.target" ]; 456 wants = [ "network.target" ]; 457 before = [ "network.target" ]; 458 environment.DEVICE = name; 459 path = with pkgs; [ kmod iproute2 wireguard-tools ]; 460 461 serviceConfig = { 462 Type = "oneshot"; 463 RemainAfterExit = true; 464 }; 465 466 script = '' 467 ${optionalString (!config.boot.isContainer) "modprobe wireguard || true"} 468 469 ${values.preSetup} 470 471 ${ipPreMove} link add dev "${name}" type wireguard 472 ${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) ''${ipPreMove} link set "${name}" netns "${ns}"''} 473 ${optionalString (values.mtu != null) ''${ipPostMove} link set "${name}" mtu ${toString values.mtu}''} 474 475 ${concatMapStringsSep "\n" (ip: 476 ''${ipPostMove} address add "${ip}" dev "${name}"'' 477 ) values.ips} 478 479 ${concatStringsSep " " ( 480 [ ''${wg} set "${name}" private-key "${privKey}"'' ] 481 ++ optional (values.listenPort != null) ''listen-port "${toString values.listenPort}"'' 482 ++ optional (values.fwMark != null) ''fwmark "${values.fwMark}"'' 483 )} 484 485 ${ipPostMove} link set up dev "${name}" 486 487 ${values.postSetup} 488 ''; 489 490 postStop = '' 491 ${ipPostMove} link del dev "${name}" 492 ${values.postShutdown} 493 ''; 494 }; 495 496 nsWrap = cmd: src: dst: 497 let 498 nsList = filter (ns: ns != null) [ src dst ]; 499 ns = last nsList; 500 in 501 if (length nsList > 0 && ns != "init") then ''ip netns exec "${ns}" "${cmd}"'' else cmd; 502in 503 504{ 505 506 ###### interface 507 508 options = { 509 510 networking.wireguard = { 511 512 enable = mkOption { 513 description = lib.mdDoc '' 514 Whether to enable WireGuard. 515 516 Please note that {option}`systemd.network.netdevs` has more features 517 and is better maintained. When building new things, it is advised to 518 use that instead. 519 ''; 520 type = types.bool; 521 # 2019-05-25: Backwards compatibility. 522 default = cfg.interfaces != {}; 523 defaultText = literalExpression "config.${opt.interfaces} != { }"; 524 example = true; 525 }; 526 527 interfaces = mkOption { 528 description = lib.mdDoc '' 529 WireGuard interfaces. 530 531 Please note that {option}`systemd.network.netdevs` has more features 532 and is better maintained. When building new things, it is advised to 533 use that instead. 534 ''; 535 default = {}; 536 example = { 537 wg0 = { 538 ips = [ "192.168.20.4/24" ]; 539 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk="; 540 peers = [ 541 { allowedIPs = [ "192.168.20.1/32" ]; 542 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; 543 endpoint = "demo.wireguard.io:12913"; } 544 ]; 545 }; 546 }; 547 type = with types; attrsOf (submodule interfaceOpts); 548 }; 549 550 }; 551 552 }; 553 554 555 ###### implementation 556 557 config = mkIf cfg.enable (let 558 all_peers = flatten 559 (mapAttrsToList (interfaceName: interfaceCfg: 560 map (peer: { inherit interfaceName interfaceCfg peer;}) interfaceCfg.peers 561 ) cfg.interfaces); 562 in { 563 564 assertions = (attrValues ( 565 mapAttrs (name: value: { 566 assertion = (value.privateKey != null) != (value.privateKeyFile != null); 567 message = "Either networking.wireguard.interfaces.${name}.privateKey or networking.wireguard.interfaces.${name}.privateKeyFile must be set."; 568 }) cfg.interfaces)) 569 ++ (attrValues ( 570 mapAttrs (name: value: { 571 assertion = value.generatePrivateKeyFile -> (value.privateKey == null); 572 message = "networking.wireguard.interfaces.${name}.generatePrivateKeyFile must not be set if networking.wireguard.interfaces.${name}.privateKey is set."; 573 }) cfg.interfaces)) 574 ++ map ({ interfaceName, peer, ... }: { 575 assertion = (peer.presharedKey == null) || (peer.presharedKeyFile == null); 576 message = "networking.wireguard.interfaces.${interfaceName} peer «${peer.publicKey}» has both presharedKey and presharedKeyFile set, but only one can be used."; 577 }) all_peers; 578 579 boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard; 580 environment.systemPackages = [ pkgs.wireguard-tools ]; 581 582 systemd.services = 583 (mapAttrs' generateInterfaceUnit cfg.interfaces) 584 // (listToAttrs (map generatePeerUnit all_peers)) 585 // (mapAttrs' generateKeyServiceUnit 586 (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces)); 587 588 systemd.targets = mapAttrs' generateInterfaceTarget cfg.interfaces; 589 } 590 ); 591 592}