1 { config, lib, options, pkgs, ... }:
7 cfg = config.networking.wireguard;
8 opt = options.networking.wireguard;
10 kernel = config.boot.kernelPackages;
14 interfaceOpts = { ... }: {
19 example = [ "192.168.2.1/24" ];
21 type = with types; listOf str;
22 description = lib.mdDoc "The IP addresses of the interface.";
25 privateKey = mkOption {
26 example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
27 type = with types; nullOr str;
29 description = lib.mdDoc ''
30 Base64 private key generated by {command}`wg genkey`.
32 Warning: Consider using privateKeyFile instead if you do not
33 want to store the key in the world-readable Nix store.
37 generatePrivateKeyFile = mkOption {
40 description = lib.mdDoc ''
41 Automatically generate a private key with
42 {command}`wg genkey`, at the privateKeyFile location.
46 privateKeyFile = mkOption {
47 example = "/private/wireguard_key";
48 type = with types; nullOr str;
50 description = lib.mdDoc ''
51 Private key file as generated by {command}`wg genkey`.
55 listenPort = mkOption {
57 type = with types; nullOr int;
59 description = lib.mdDoc ''
60 16-bit port for listening. Optional; if not specified,
61 automatically generated based on interface name.
66 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"'';
68 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
69 description = lib.mdDoc ''
70 Commands called at the start of the interface setup.
74 postSetup = mkOption {
75 example = literalExpression ''
76 '''printf "nameserver 10.200.100.1" | ''${pkgs.openresolv}/bin/resolvconf -a wg0 -m 0'''
79 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
80 description = lib.mdDoc "Commands called at the end of the interface setup.";
83 postShutdown = mkOption {
84 example = literalExpression ''"''${pkgs.openresolv}/bin/resolvconf -d wg0"'';
86 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
87 description = lib.mdDoc "Commands called after shutting down the interface.";
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.
104 description = lib.mdDoc "Peers linked to the interface.";
105 type = with types; listOf (submodule peerOpts);
108 allowedIPsAsRoutes = mkOption {
112 description = lib.mdDoc ''
113 Determines whether to add allowed IPs as routes or not.
117 socketNamespace = mkOption {
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/).
129 interfaceNamespace = mkOption {
131 type = with types; nullOr str;
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
137 See [documentation](https://www.wireguard.com/netns/).
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
157 type = with types; nullOr int;
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.
177 publicKey = mkOption {
178 example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
180 description = lib.mdDoc "The base64 public key of the peer.";
183 presharedKey = mkOption {
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.
193 Warning: Consider using presharedKeyFile instead if you do not
194 want to store the key in the world-readable Nix store.
198 presharedKeyFile = mkOption {
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.
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.'';
220 endpoint = mkOption {
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.
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.
241 dynamicEndpointRefreshSeconds = mkOption {
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
250 Setting this to `0` disables periodic reexecution.
254 persistentKeepalive = mkOption {
256 type = with types; nullOr int;
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.'';
272 generateKeyServiceUnit = name: values:
273 assert values.generatePrivateKeyFile;
274 nameValuePair "wireguard-${name}-key"
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 ];
284 RemainAfterExit = true;
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}"
294 if [ ! -f "${values.privateKeyFile}" ]; then
295 # Write private key file with atomically-correct permissions.
296 (set -e; umask 077; wg genkey > "${values.privateKeyFile}")
301 peerUnitServiceName = interfaceName: publicKey: dynamicRefreshEnabled:
303 keyToUnitName = replaceChars
304 [ "/" "-" " " "+" "=" ]
305 [ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ];
306 unitName = keyToUnitName publicKey;
307 refreshSuffix = optionalString dynamicRefreshEnabled "-refresh";
309 "wireguard-${interfaceName}-peer-${unitName}${refreshSuffix}";
311 generatePeerUnit = { interfaceName, interfaceCfg, peer }:
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
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 ];
338 if !dynamicRefreshEnabled
342 RemainAfterExit = true;
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.
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}"''
362 optionalString interfaceCfg.allowedIPsAsRoutes
363 (concatMapStringsSep "\n"
365 ''${ip} route replace "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"''
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}";
383 route_destroy = optionalString interfaceCfg.allowedIPsAsRoutes
384 (concatMapStringsSep "\n"
386 ''${ip} route delete "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"''
389 ${wg} set "${interfaceName}" peer "${peer.publicKey}" remove
394 # the target is required to start new peer units when they are added
395 generateInterfaceTarget = name: values:
397 mkPeerUnit = peer: (peerUnitServiceName name peer.publicKey (peer.dynamicEndpointRefreshSeconds != 0)) + ".service";
399 nameValuePair "wireguard-${name}"
401 description = "WireGuard Tunnel - ${name}";
402 wantedBy = [ "multi-user.target" ];
403 wants = [ "wireguard-${name}.service" ] ++ map mkPeerUnit values.peers;
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;
419 nameValuePair "wireguard-${name}"
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 ];
430 RemainAfterExit = true;
434 ${optionalString (!config.boot.isContainer) "modprobe wireguard || true"}
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}''}
442 ${concatMapStringsSep "\n" (ip:
443 ''${ipPostMove} address add "${ip}" dev "${name}"''
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}"''
452 ${ipPostMove} link set up dev "${name}"
458 ${ipPostMove} link del dev "${name}"
459 ${values.postShutdown}
463 nsWrap = cmd: src: dst:
465 nsList = filter (ns: ns != null) [ src dst ];
468 if (length nsList > 0 && ns != "init") then ''ip netns exec "${ns}" "${cmd}"'' else cmd;
477 networking.wireguard = {
480 description = lib.mdDoc ''
481 Whether to enable WireGuard.
483 Please note that {option}`systemd.network.netdevs` has more features
484 and is better maintained. When building new things, it is advised to
488 # 2019-05-25: Backwards compatibility.
489 default = cfg.interfaces != {};
490 defaultText = literalExpression "config.${opt.interfaces} != { }";
494 interfaces = mkOption {
495 description = lib.mdDoc ''
496 WireGuard interfaces.
498 Please note that {option}`systemd.network.netdevs` has more features
499 and is better maintained. When building new things, it is advised to
505 ips = [ "192.168.20.4/24" ];
506 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
508 { allowedIPs = [ "192.168.20.1/32" ];
509 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
510 endpoint = "demo.wireguard.io:12913"; }
514 type = with types; attrsOf (submodule interfaceOpts);
522 ###### implementation
524 config = mkIf cfg.enable (let
526 (mapAttrsToList (interfaceName: interfaceCfg:
527 map (peer: { inherit interfaceName interfaceCfg peer;}) interfaceCfg.peers
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.";
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.";
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.";
546 boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard;
547 environment.systemPackages = [ pkgs.wireguard-tools ];
550 (mapAttrs' generateInterfaceUnit cfg.interfaces)
551 // (listToAttrs (map generatePeerUnit all_peers))
552 // (mapAttrs' generateKeyServiceUnit
553 (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces));
555 systemd.targets = mapAttrs' generateInterfaceTarget cfg.interfaces;