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}