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