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