vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / networking / wg-quick.nix
blob369c5a9397659006bb3d87ff61d119e3a2070a0c
1 { config, lib, pkgs, ... }:
3 with lib;
4 let
5   cfg = config.networking.wg-quick;
7   kernel = config.boot.kernelPackages;
9   # interface options
11   interfaceOpts = { ... }: {
12     options = {
14       configFile = mkOption {
15         example = "/secret/wg0.conf";
16         default = null;
17         type = with types; nullOr str;
18         description = ''
19           wg-quick .conf file, describing the interface.
20           Using this option can be a useful means of configuring WireGuard if
21           one has an existing .conf file.
22           This overrides any other configuration interface configuration options.
23           See wg-quick manpage for more details.
24         '';
25       };
27       address = mkOption {
28         example = [ "192.168.2.1/24" ];
29         default = [];
30         type = with types; listOf str;
31         description = "The IP addresses of the interface.";
32       };
34       autostart = mkOption {
35         description = "Whether to bring up this interface automatically during boot.";
36         default = true;
37         example = false;
38         type = types.bool;
39       };
41       dns = mkOption {
42         example = [ "192.168.2.2" ];
43         default = [];
44         type = with types; listOf str;
45         description = "The IP addresses of DNS servers to configure.";
46       };
48       privateKey = mkOption {
49         example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
50         type = with types; nullOr str;
51         default = null;
52         description = ''
53           Base64 private key generated by {command}`wg genkey`.
55           Warning: Consider using privateKeyFile instead if you do not
56           want to store the key in the world-readable Nix store.
57         '';
58       };
60       generatePrivateKeyFile = mkOption {
61         default = false;
62         type = types.bool;
63         description = ''
64           Automatically generate a private key with
65           {command}`wg genkey`, at the privateKeyFile location.
66         '';
67       };
69       privateKeyFile = mkOption {
70         example = "/private/wireguard_key";
71         type = with types; nullOr str;
72         default = null;
73         description = ''
74           Private key file as generated by {command}`wg genkey`.
75         '';
76       };
78       listenPort = mkOption {
79         default = null;
80         type = with types; nullOr int;
81         example = 51820;
82         description = ''
83           16-bit port for listening. Optional; if not specified,
84           automatically generated based on interface name.
85         '';
86       };
88       preUp = mkOption {
89         example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"'';
90         default = "";
91         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
92         description = ''
93           Commands called at the start of the interface setup.
94         '';
95       };
97       preDown = mkOption {
98         example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"'';
99         default = "";
100         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
101         description = ''
102           Command called before the interface is taken down.
103         '';
104       };
106       postUp = mkOption {
107         example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"'';
108         default = "";
109         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
110         description = ''
111           Commands called after the interface setup.
112         '';
113       };
115       postDown = mkOption {
116         example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"'';
117         default = "";
118         type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
119         description = ''
120           Command called after the interface is taken down.
121         '';
122       };
124       table = mkOption {
125         example = "main";
126         default = null;
127         type = with types; nullOr str;
128         description = ''
129           The kernel routing table to add this interface's
130           associated routes to. Setting this is useful for e.g. policy routing
131           ("ip rule") or virtual routing and forwarding ("ip vrf"). Both
132           numeric table IDs and table names (/etc/rt_tables) can be used.
133           Defaults to "main".
134         '';
135       };
137       mtu = mkOption {
138         example = 1248;
139         default = null;
140         type = with types; nullOr int;
141         description = ''
142           If not specified, the MTU is automatically determined
143           from the endpoint addresses or the system default route, which is usually
144           a sane choice. However, to manually specify an MTU to override this
145           automatic discovery, this value may be specified explicitly.
146         '';
147       };
149       peers = mkOption {
150         default = [];
151         description = "Peers linked to the interface.";
152         type = with types; listOf (submodule peerOpts);
153       };
154     };
155   };
157   # peer options
159   peerOpts = {
160     options = {
161       publicKey = mkOption {
162         example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
163         type = types.str;
164         description = "The base64 public key to the peer.";
165       };
167       presharedKey = mkOption {
168         default = null;
169         example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I=";
170         type = with types; nullOr str;
171         description = ''
172           Base64 preshared key generated by {command}`wg genpsk`.
173           Optional, and may be omitted. This option adds an additional layer of
174           symmetric-key cryptography to be mixed into the already existing
175           public-key cryptography, for post-quantum resistance.
177           Warning: Consider using presharedKeyFile instead if you do not
178           want to store the key in the world-readable Nix store.
179         '';
180       };
182       presharedKeyFile = mkOption {
183         default = null;
184         example = "/private/wireguard_psk";
185         type = with types; nullOr str;
186         description = ''
187           File pointing to preshared key as generated by {command}`wg genpsk`.
188           Optional, and may be omitted. This option adds an additional layer of
189           symmetric-key cryptography to be mixed into the already existing
190           public-key cryptography, for post-quantum resistance.
191         '';
192       };
194       allowedIPs = mkOption {
195         example = [ "10.192.122.3/32" "10.192.124.1/24" ];
196         type = with types; listOf str;
197         description = ''List of IP (v4 or v6) addresses with CIDR masks from
198         which this peer is allowed to send incoming traffic and to which
199         outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may
200         be specified for matching all IPv4 addresses, and ::/0 may be specified
201         for matching all IPv6 addresses.'';
202       };
204       endpoint = mkOption {
205         default = null;
206         example = "demo.wireguard.io:12913";
207         type = with types; nullOr str;
208         description = ''Endpoint IP or hostname of the peer, followed by a colon,
209         and then a port number of the peer.'';
210       };
212       persistentKeepalive = mkOption {
213         default = null;
214         type = with types; nullOr int;
215         example = 25;
216         description = ''This is optional and is by default off, because most
217         users will not need it. It represents, in seconds, between 1 and 65535
218         inclusive, how often to send an authenticated empty packet to the peer,
219         for the purpose of keeping a stateful firewall or NAT mapping valid
220         persistently. For example, if the interface very rarely sends traffic,
221         but it might at anytime receive traffic from a peer, and it is behind
222         NAT, the interface might benefit from having a persistent keepalive
223         interval of 25 seconds; however, most users will not need this.'';
224       };
225     };
226   };
228   writeScriptFile = name: text: ((pkgs.writeShellScriptBin name text) + "/bin/${name}");
230   generatePrivateKeyScript = privateKeyFile: ''
231     set -e
233     # If the parent dir does not already exist, create it.
234     # Otherwise, does nothing, keeping existing permissions intact.
235     mkdir -p --mode 0755 "${dirOf privateKeyFile}"
237     if [ ! -f "${privateKeyFile}" ]; then
238       # Write private key file with atomically-correct permissions.
239       (set -e; umask 077; wg genkey > "${privateKeyFile}")
240     fi
241   '';
243   generateUnit = name: values:
244     assert assertMsg (values.configFile != null || ((values.privateKey != null) != (values.privateKeyFile != null))) "Only one of privateKey, configFile or privateKeyFile may be set";
245     assert assertMsg (values.generatePrivateKeyFile == false || values.privateKeyFile != null) "generatePrivateKeyFile requires privateKeyFile to be set";
246     let
247       generateKeyScriptFile = if values.generatePrivateKeyFile then writeScriptFile "generatePrivateKey.sh" (generatePrivateKeyScript values.privateKeyFile) else null;
248       preUpFile = if values.preUp != "" then writeScriptFile "preUp.sh" values.preUp else null;
249       postUp =
250             optional (values.privateKeyFile != null) "wg set ${name} private-key <(cat ${values.privateKeyFile})" ++
251             (concatMap (peer: optional (peer.presharedKeyFile != null) "wg set ${name} peer ${peer.publicKey} preshared-key <(cat ${peer.presharedKeyFile})") values.peers) ++
252             optional (values.postUp != "") values.postUp;
253       postUpFile = if postUp != [] then writeScriptFile "postUp.sh" (concatMapStringsSep "\n" (line: line) postUp) else null;
254       preDownFile = if values.preDown != "" then writeScriptFile "preDown.sh" values.preDown else null;
255       postDownFile = if values.postDown != "" then writeScriptFile "postDown.sh" values.postDown else null;
256       configDir = pkgs.writeTextFile {
257         name = "config-${name}";
258         executable = false;
259         destination = "/${name}.conf";
260         text =
261         ''
262         [interface]
263         ${concatMapStringsSep "\n" (address:
264           "Address = ${address}"
265         ) values.address}
266         ${concatMapStringsSep "\n" (dns:
267           "DNS = ${dns}"
268         ) values.dns}
269         '' +
270         optionalString (values.table != null) "Table = ${values.table}\n" +
271         optionalString (values.mtu != null) "MTU = ${toString values.mtu}\n" +
272         optionalString (values.privateKey != null) "PrivateKey = ${values.privateKey}\n" +
273         optionalString (values.listenPort != null) "ListenPort = ${toString values.listenPort}\n" +
274         optionalString (generateKeyScriptFile != null) "PreUp = ${generateKeyScriptFile}\n" +
275         optionalString (preUpFile != null) "PreUp = ${preUpFile}\n" +
276         optionalString (postUpFile != null) "PostUp = ${postUpFile}\n" +
277         optionalString (preDownFile != null) "PreDown = ${preDownFile}\n" +
278         optionalString (postDownFile != null) "PostDown = ${postDownFile}\n" +
279         concatMapStringsSep "\n" (peer:
280           assert assertMsg (!((peer.presharedKeyFile != null) && (peer.presharedKey != null))) "Only one of presharedKey or presharedKeyFile may be set";
281           "[Peer]\n" +
282           "PublicKey = ${peer.publicKey}\n" +
283           optionalString (peer.presharedKey != null) "PresharedKey = ${peer.presharedKey}\n" +
284           optionalString (peer.endpoint != null) "Endpoint = ${peer.endpoint}\n" +
285           optionalString (peer.persistentKeepalive != null) "PersistentKeepalive = ${toString peer.persistentKeepalive}\n" +
286           optionalString (peer.allowedIPs != []) "AllowedIPs = ${concatStringsSep "," peer.allowedIPs}\n"
287         ) values.peers;
288       };
289       configPath =
290         if values.configFile != null then
291           # This uses bind-mounted private tmp folder (/tmp/systemd-private-***)
292           "/tmp/${name}.conf"
293         else
294           "${configDir}/${name}.conf";
295     in
296     nameValuePair "wg-quick-${name}"
297       {
298         description = "wg-quick WireGuard Tunnel - ${name}";
299         requires = [ "network-online.target" ];
300         after = [ "network.target" "network-online.target" ];
301         wantedBy = optional values.autostart "multi-user.target";
302         environment.DEVICE = name;
303         path = [
304           pkgs.wireguard-tools
305           config.networking.firewall.package   # iptables or nftables
306           config.networking.resolvconf.package # openresolv or systemd
307         ];
309         serviceConfig = {
310           Type = "oneshot";
311           RemainAfterExit = true;
312         };
314         script = ''
315           ${optionalString (!config.boot.isContainer) "${pkgs.kmod}/bin/modprobe wireguard"}
316           ${optionalString (values.configFile != null) ''
317             cp ${values.configFile} ${configPath}
318           ''}
319           wg-quick up ${configPath}
320         '';
322         serviceConfig = {
323           # Used to privately store renamed copies of external config files during activation
324           PrivateTmp = true;
325         };
327         preStop = ''
328           wg-quick down ${configPath}
329         '';
330       };
331 in {
333   ###### interface
335   options = {
336     networking.wg-quick = {
337       interfaces = mkOption {
338         description = "Wireguard interfaces.";
339         default = {};
340         example = {
341           wg0 = {
342             address = [ "192.168.20.4/24" ];
343             privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
344             peers = [
345               { allowedIPs = [ "192.168.20.1/32" ];
346                 publicKey  = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
347                 endpoint   = "demo.wireguard.io:12913"; }
348             ];
349           };
350         };
351         type = with types; attrsOf (submodule interfaceOpts);
352       };
353     };
354   };
357   ###### implementation
359   config = mkIf (cfg.interfaces != {}) {
360     boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard;
361     environment.systemPackages = [ pkgs.wireguard-tools ];
362     systemd.services = mapAttrs' generateUnit cfg.interfaces;
364     # Prevent networkd from clearing the rules set by wg-quick when restarted (e.g. when waking up from suspend).
365     systemd.network.config.networkConfig.ManageForeignRoutingPolicyRules = mkDefault false;
367     # WireGuard interfaces should be ignored in determining whether the network is online.
368     systemd.network.wait-online.ignoredInterfaces = builtins.attrNames cfg.interfaces;
369   };