at 24.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 = "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 = '' 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 = '' 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 = '' 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 = '' 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 = '' 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 = "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 = "Commands called after shutting down the interface."; 88 }; 89 90 table = mkOption { 91 default = "main"; 92 type = types.str; 93 description = '' 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 = "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 = '' 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 = ''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 = ''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 = '' 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 = '' 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 metric = mkOption { 169 default = null; 170 type = with types; nullOr int; 171 example = 700; 172 description = '' 173 Set the metric of routes related to this Wireguard interface. 174 ''; 175 }; 176 }; 177 178 }; 179 180 # peer options 181 182 peerOpts = self: { 183 184 options = { 185 186 name = mkOption { 187 default = 188 replaceStrings 189 [ "/" "-" " " "+" "=" ] 190 [ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ] 191 self.config.publicKey; 192 defaultText = literalExpression "publicKey"; 193 example = "bernd"; 194 type = types.str; 195 description = "Name used to derive peer unit name."; 196 }; 197 198 publicKey = mkOption { 199 example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; 200 type = types.singleLineStr; 201 description = "The base64 public key of the peer."; 202 }; 203 204 presharedKey = mkOption { 205 default = null; 206 example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I="; 207 type = with types; nullOr str; 208 description = '' 209 Base64 preshared key generated by {command}`wg genpsk`. 210 Optional, and may be omitted. This option adds an additional layer of 211 symmetric-key cryptography to be mixed into the already existing 212 public-key cryptography, for post-quantum resistance. 213 214 Warning: Consider using presharedKeyFile instead if you do not 215 want to store the key in the world-readable Nix store. 216 ''; 217 }; 218 219 presharedKeyFile = mkOption { 220 default = null; 221 example = "/private/wireguard_psk"; 222 type = with types; nullOr str; 223 description = '' 224 File pointing to preshared key as generated by {command}`wg genpsk`. 225 Optional, and may be omitted. This option adds an additional layer of 226 symmetric-key cryptography to be mixed into the already existing 227 public-key cryptography, for post-quantum resistance. 228 ''; 229 }; 230 231 allowedIPs = mkOption { 232 example = [ "10.192.122.3/32" "10.192.124.1/24" ]; 233 type = with types; listOf str; 234 description = ''List of IP (v4 or v6) addresses with CIDR masks from 235 which this peer is allowed to send incoming traffic and to which 236 outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may 237 be specified for matching all IPv4 addresses, and ::/0 may be specified 238 for matching all IPv6 addresses.''; 239 }; 240 241 endpoint = mkOption { 242 default = null; 243 example = "demo.wireguard.io:12913"; 244 type = with types; nullOr str; 245 description = '' 246 Endpoint IP or hostname of the peer, followed by a colon, 247 and then a port number of the peer. 248 249 Warning for endpoints with changing IPs: 250 The WireGuard kernel side cannot perform DNS resolution. 251 Thus DNS resolution is done once by the `wg` userspace 252 utility, when setting up WireGuard. Consequently, if the IP address 253 behind the name changes, WireGuard will not notice. 254 This is especially common for dynamic-DNS setups, but also applies to 255 any other DNS-based setup. 256 If you do not use IP endpoints, you likely want to set 257 {option}`networking.wireguard.dynamicEndpointRefreshSeconds` 258 to refresh the IPs periodically. 259 ''; 260 }; 261 262 dynamicEndpointRefreshSeconds = mkOption { 263 default = 0; 264 example = 5; 265 type = with types; int; 266 description = '' 267 Periodically re-execute the `wg` utility every 268 this many seconds in order to let WireGuard notice DNS / hostname 269 changes. 270 271 Setting this to `0` disables periodic reexecution. 272 ''; 273 }; 274 275 dynamicEndpointRefreshRestartSeconds = mkOption { 276 default = null; 277 example = 5; 278 type = with types; nullOr ints.unsigned; 279 description = '' 280 When the dynamic endpoint refresh that is configured via 281 dynamicEndpointRefreshSeconds exits (likely due to a failure), 282 restart that service after this many seconds. 283 284 If set to `null` the value of 285 {option}`networking.wireguard.dynamicEndpointRefreshSeconds` 286 will be used as the default. 287 ''; 288 }; 289 290 persistentKeepalive = mkOption { 291 default = null; 292 type = with types; nullOr int; 293 example = 25; 294 description = ''This is optional and is by default off, because most 295 users will not need it. It represents, in seconds, between 1 and 65535 296 inclusive, how often to send an authenticated empty packet to the peer, 297 for the purpose of keeping a stateful firewall or NAT mapping valid 298 persistently. For example, if the interface very rarely sends traffic, 299 but it might at anytime receive traffic from a peer, and it is behind 300 NAT, the interface might benefit from having a persistent keepalive 301 interval of 25 seconds; however, most users will not need this.''; 302 }; 303 304 }; 305 306 }; 307 308 generateKeyServiceUnit = name: values: 309 assert values.generatePrivateKeyFile; 310 nameValuePair "wireguard-${name}-key" 311 { 312 description = "WireGuard Tunnel - ${name} - Key Generator"; 313 wantedBy = [ "wireguard-${name}.service" ]; 314 requiredBy = [ "wireguard-${name}.service" ]; 315 before = [ "wireguard-${name}.service" ]; 316 path = with pkgs; [ wireguard-tools ]; 317 318 serviceConfig = { 319 Type = "oneshot"; 320 RemainAfterExit = true; 321 }; 322 323 script = '' 324 set -e 325 326 # If the parent dir does not already exist, create it. 327 # Otherwise, does nothing, keeping existing permissions intact. 328 mkdir -p --mode 0755 "${dirOf values.privateKeyFile}" 329 330 if [ ! -f "${values.privateKeyFile}" ]; then 331 # Write private key file with atomically-correct permissions. 332 (set -e; umask 077; wg genkey > "${values.privateKeyFile}") 333 fi 334 ''; 335 }; 336 337 peerUnitServiceName = interfaceName: peerName: dynamicRefreshEnabled: 338 let 339 refreshSuffix = optionalString dynamicRefreshEnabled "-refresh"; 340 in 341 "wireguard-${interfaceName}-peer-${peerName}${refreshSuffix}"; 342 343 generatePeerUnit = { interfaceName, interfaceCfg, peer }: 344 let 345 psk = 346 if peer.presharedKey != null 347 then pkgs.writeText "wg-psk" peer.presharedKey 348 else peer.presharedKeyFile; 349 src = interfaceCfg.socketNamespace; 350 dst = interfaceCfg.interfaceNamespace; 351 ip = nsWrap "ip" src dst; 352 wg = nsWrap "wg" src dst; 353 dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0; 354 # We generate a different name (a `-refresh` suffix) when `dynamicEndpointRefreshSeconds` 355 # to avoid that the same service switches `Type` (`oneshot` vs `simple`), 356 # with the intent to make scripting more obvious. 357 serviceName = peerUnitServiceName interfaceName peer.name dynamicRefreshEnabled; 358 in nameValuePair serviceName 359 { 360 description = "WireGuard Peer - ${interfaceName} - ${peer.name}" 361 + optionalString (peer.name != peer.publicKey) " (${peer.publicKey})"; 362 requires = [ "wireguard-${interfaceName}.service" ]; 363 wants = [ "network-online.target" ]; 364 after = [ "wireguard-${interfaceName}.service" "network-online.target" ]; 365 wantedBy = [ "wireguard-${interfaceName}.service" ]; 366 environment.DEVICE = interfaceName; 367 environment.WG_ENDPOINT_RESOLUTION_RETRIES = "infinity"; 368 path = with pkgs; [ iproute2 wireguard-tools ]; 369 370 serviceConfig = 371 if !dynamicRefreshEnabled 372 then 373 { 374 Type = "oneshot"; 375 RemainAfterExit = true; 376 } 377 else 378 { 379 Type = "simple"; # re-executes 'wg' indefinitely 380 # Note that `Type = "oneshot"` services with `RemainAfterExit = true` 381 # cannot be used with systemd timers (see `man systemd.timer`), 382 # which is why `simple` with a loop is the best choice here. 383 # It also makes starting and stopping easiest. 384 # 385 # Restart if the service exits (e.g. when wireguard gives up after "Name or service not known" dns failures): 386 Restart = "always"; 387 RestartSec = if null != peer.dynamicEndpointRefreshRestartSeconds 388 then peer.dynamicEndpointRefreshRestartSeconds 389 else peer.dynamicEndpointRefreshSeconds; 390 }; 391 unitConfig = lib.optionalAttrs dynamicRefreshEnabled { 392 StartLimitIntervalSec = 0; 393 }; 394 395 script = let 396 wg_setup = concatStringsSep " " ( 397 [ ''${wg} set ${interfaceName} peer "${peer.publicKey}"'' ] 398 ++ optional (psk != null) ''preshared-key "${psk}"'' 399 ++ optional (peer.endpoint != null) ''endpoint "${peer.endpoint}"'' 400 ++ optional (peer.persistentKeepalive != null) ''persistent-keepalive "${toString peer.persistentKeepalive}"'' 401 ++ optional (peer.allowedIPs != []) ''allowed-ips "${concatStringsSep "," peer.allowedIPs}"'' 402 ); 403 route_setup = 404 optionalString interfaceCfg.allowedIPsAsRoutes 405 (concatMapStringsSep "\n" 406 (allowedIP: 407 ''${ip} route replace "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}" ${optionalString (interfaceCfg.metric != null) "metric ${toString interfaceCfg.metric}"}'' 408 ) peer.allowedIPs); 409 in '' 410 ${wg_setup} 411 ${route_setup} 412 413 ${optionalString (peer.dynamicEndpointRefreshSeconds != 0) '' 414 # Re-execute 'wg' periodically to notice DNS / hostname changes. 415 # Note this will not time out on transient DNS failures such as DNS names 416 # because we have set 'WG_ENDPOINT_RESOLUTION_RETRIES=infinity'. 417 # Also note that 'wg' limits its maximum retry delay to 20 seconds as of writing. 418 while ${wg_setup}; do 419 sleep "${toString peer.dynamicEndpointRefreshSeconds}"; 420 done 421 ''} 422 ''; 423 424 postStop = let 425 route_destroy = optionalString interfaceCfg.allowedIPsAsRoutes 426 (concatMapStringsSep "\n" 427 (allowedIP: 428 ''${ip} route delete "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"'' 429 ) peer.allowedIPs); 430 in '' 431 ${wg} set "${interfaceName}" peer "${peer.publicKey}" remove 432 ${route_destroy} 433 ''; 434 }; 435 436 # the target is required to start new peer units when they are added 437 generateInterfaceTarget = name: values: 438 let 439 mkPeerUnit = peer: (peerUnitServiceName name peer.name (peer.dynamicEndpointRefreshSeconds != 0)) + ".service"; 440 in 441 nameValuePair "wireguard-${name}" 442 rec { 443 description = "WireGuard Tunnel - ${name}"; 444 wantedBy = [ "multi-user.target" ]; 445 wants = [ "wireguard-${name}.service" ] ++ map mkPeerUnit values.peers; 446 after = wants; 447 }; 448 449 generateInterfaceUnit = name: values: 450 # exactly one way to specify the private key must be set 451 #assert (values.privateKey != null) != (values.privateKeyFile != null); 452 let privKey = if values.privateKeyFile != null then values.privateKeyFile else pkgs.writeText "wg-key" values.privateKey; 453 src = values.socketNamespace; 454 dst = values.interfaceNamespace; 455 ipPreMove = nsWrap "ip" src null; 456 ipPostMove = nsWrap "ip" src dst; 457 wg = nsWrap "wg" src dst; 458 ns = if dst == "init" then "1" else dst; 459 460 in 461 nameValuePair "wireguard-${name}" 462 { 463 description = "WireGuard Tunnel - ${name}"; 464 after = [ "network-pre.target" ]; 465 wants = [ "network.target" ]; 466 before = [ "network.target" ]; 467 environment.DEVICE = name; 468 path = with pkgs; [ kmod iproute2 wireguard-tools ]; 469 470 serviceConfig = { 471 Type = "oneshot"; 472 RemainAfterExit = true; 473 }; 474 475 script = '' 476 ${optionalString (!config.boot.isContainer) "modprobe wireguard || true"} 477 478 ${values.preSetup} 479 480 ${ipPreMove} link add dev "${name}" type wireguard 481 ${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) ''${ipPreMove} link set "${name}" netns "${ns}"''} 482 ${optionalString (values.mtu != null) ''${ipPostMove} link set "${name}" mtu ${toString values.mtu}''} 483 484 ${concatMapStringsSep "\n" (ip: 485 ''${ipPostMove} address add "${ip}" dev "${name}"'' 486 ) values.ips} 487 488 ${concatStringsSep " " ( 489 [ ''${wg} set "${name}" private-key "${privKey}"'' ] 490 ++ optional (values.listenPort != null) ''listen-port "${toString values.listenPort}"'' 491 ++ optional (values.fwMark != null) ''fwmark "${values.fwMark}"'' 492 )} 493 494 ${ipPostMove} link set up dev "${name}" 495 496 ${values.postSetup} 497 ''; 498 499 postStop = '' 500 ${ipPostMove} link del dev "${name}" 501 ${values.postShutdown} 502 ''; 503 }; 504 505 nsWrap = cmd: src: dst: 506 let 507 nsList = filter (ns: ns != null) [ src dst ]; 508 ns = last nsList; 509 in 510 if (length nsList > 0 && ns != "init") then ''ip netns exec "${ns}" "${cmd}"'' else cmd; 511in 512 513{ 514 515 ###### interface 516 517 options = { 518 519 networking.wireguard = { 520 521 enable = mkOption { 522 description = '' 523 Whether to enable WireGuard. 524 525 Please note that {option}`systemd.network.netdevs` has more features 526 and is better maintained. When building new things, it is advised to 527 use that instead. 528 ''; 529 type = types.bool; 530 # 2019-05-25: Backwards compatibility. 531 default = cfg.interfaces != {}; 532 defaultText = literalExpression "config.${opt.interfaces} != { }"; 533 example = true; 534 }; 535 536 interfaces = mkOption { 537 description = '' 538 WireGuard interfaces. 539 540 Please note that {option}`systemd.network.netdevs` has more features 541 and is better maintained. When building new things, it is advised to 542 use that instead. 543 ''; 544 default = {}; 545 example = { 546 wg0 = { 547 ips = [ "192.168.20.4/24" ]; 548 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk="; 549 peers = [ 550 { allowedIPs = [ "192.168.20.1/32" ]; 551 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; 552 endpoint = "demo.wireguard.io:12913"; } 553 ]; 554 }; 555 }; 556 type = with types; attrsOf (submodule interfaceOpts); 557 }; 558 559 }; 560 561 }; 562 563 564 ###### implementation 565 566 config = mkIf cfg.enable (let 567 all_peers = flatten 568 (mapAttrsToList (interfaceName: interfaceCfg: 569 map (peer: { inherit interfaceName interfaceCfg peer;}) interfaceCfg.peers 570 ) cfg.interfaces); 571 in { 572 573 assertions = (attrValues ( 574 mapAttrs (name: value: { 575 assertion = (value.privateKey != null) != (value.privateKeyFile != null); 576 message = "Either networking.wireguard.interfaces.${name}.privateKey or networking.wireguard.interfaces.${name}.privateKeyFile must be set."; 577 }) cfg.interfaces)) 578 ++ (attrValues ( 579 mapAttrs (name: value: { 580 assertion = value.generatePrivateKeyFile -> (value.privateKey == null); 581 message = "networking.wireguard.interfaces.${name}.generatePrivateKeyFile must not be set if networking.wireguard.interfaces.${name}.privateKey is set."; 582 }) cfg.interfaces)) 583 ++ map ({ interfaceName, peer, ... }: { 584 assertion = (peer.presharedKey == null) || (peer.presharedKeyFile == null); 585 message = "networking.wireguard.interfaces.${interfaceName} peer «${peer.publicKey}» has both presharedKey and presharedKeyFile set, but only one can be used."; 586 }) all_peers; 587 588 boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard; 589 boot.kernelModules = [ "wireguard" ]; 590 environment.systemPackages = [ pkgs.wireguard-tools ]; 591 592 systemd.services = 593 (mapAttrs' generateInterfaceUnit cfg.interfaces) 594 // (listToAttrs (map generatePeerUnit all_peers)) 595 // (mapAttrs' generateKeyServiceUnit 596 (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces)); 597 598 systemd.targets = mapAttrs' generateInterfaceTarget cfg.interfaces; 599 } 600 ); 601 602}