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 [interface]
335 ${concatMapStringsSep "\n" (address: "Address = ${address}") values.address}
336 ${concatMapStringsSep "\n" (dns: "DNS = ${dns}") values.dns}
337 ''
338 + optionalString (values.table != null) "Table = ${values.table}\n"
339 + optionalString (values.mtu != null) "MTU = ${toString values.mtu}\n"
340 + optionalString (values.privateKey != null) "PrivateKey = ${values.privateKey}\n"
341 + optionalString (values.listenPort != null) "ListenPort = ${toString values.listenPort}\n"
342 + optionalString (generateKeyScriptFile != null) "PreUp = ${generateKeyScriptFile}\n"
343 + optionalString (preUpFile != null) "PreUp = ${preUpFile}\n"
344 + optionalString (postUpFile != null) "PostUp = ${postUpFile}\n"
345 + optionalString (preDownFile != null) "PreDown = ${preDownFile}\n"
346 + optionalString (postDownFile != null) "PostDown = ${postDownFile}\n"
347 + concatLines (mapAttrsToList (n: v: "${n} = ${toString v}") values.extraOptions)
348 + concatMapStringsSep "\n" (
349 peer:
350 assert assertMsg (
351 !((peer.presharedKeyFile != null) && (peer.presharedKey != null))
352 ) "Only one of presharedKey or presharedKeyFile may be set";
353 "[Peer]\n"
354 + "PublicKey = ${peer.publicKey}\n"
355 + optionalString (peer.presharedKey != null) "PresharedKey = ${peer.presharedKey}\n"
356 + optionalString (peer.endpoint != null) "Endpoint = ${peer.endpoint}\n"
357 + optionalString (
358 peer.persistentKeepalive != null
359 ) "PersistentKeepalive = ${toString peer.persistentKeepalive}\n"
360 + optionalString (peer.allowedIPs != [ ]) "AllowedIPs = ${concatStringsSep "," peer.allowedIPs}\n"
361 ) values.peers;
362 };
363 configPath =
364 if values.configFile != null then
365 # This uses bind-mounted private tmp folder (/tmp/systemd-private-***)
366 "/tmp/${name}.conf"
367 else
368 "${configDir}/${name}.conf";
369 in
370 nameValuePair "wg-quick-${name}" {
371 description = "wg-quick WireGuard Tunnel - ${name}";
372 requires = [ "network-online.target" ];
373 after = [
374 "network.target"
375 "network-online.target"
376 ];
377 wantedBy = optional values.autostart "multi-user.target";
378 environment.DEVICE = name;
379 path = [
380 {
381 wireguard = pkgs.wireguard-tools;
382 amneziawg = pkgs.amneziawg-tools;
383 }
384 .${values.type}
385 config.networking.firewall.package # iptables or nftables
386 config.networking.resolvconf.package # openresolv or systemd
387 ];
388
389 serviceConfig = {
390 Type = "oneshot";
391 RemainAfterExit = true;
392 };
393
394 script = ''
395 ${optionalString (!config.boot.isContainer) "${pkgs.kmod}/bin/modprobe ${values.type}"}
396 ${optionalString (values.configFile != null) ''
397 cp ${values.configFile} ${configPath}
398 ''}
399 ${wgBin}-quick up ${configPath}
400 '';
401
402 serviceConfig = {
403 # Used to privately store renamed copies of external config files during activation
404 PrivateTmp = true;
405 };
406
407 preStop = ''
408 ${wgBin}-quick down ${configPath}
409 '';
410 };
411in
412{
413
414 ###### interface
415
416 options = {
417 networking.wg-quick = {
418 interfaces = mkOption {
419 description = "Wireguard interfaces.";
420 default = { };
421 example = {
422 wg0 = {
423 address = [ "192.168.20.4/24" ];
424 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
425 peers = [
426 {
427 allowedIPs = [ "192.168.20.1/32" ];
428 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
429 endpoint = "demo.wireguard.io:12913";
430 }
431 ];
432 };
433 };
434 type = with types; attrsOf (submodule interfaceOpts);
435 };
436 };
437 };
438
439 ###### implementation
440
441 config = mkIf (cfg.interfaces != { }) {
442 boot.extraModulePackages =
443 optional (
444 any (x: x.type == "wireguard") (attrValues cfg.interfaces)
445 && (versionOlder kernel.kernel.version "5.6")
446 ) kernel.wireguard
447 ++ optional (any (x: x.type == "amneziawg") (attrValues cfg.interfaces)) kernel.amneziawg;
448 environment.systemPackages =
449 optional (any (x: x.type == "wireguard") (attrValues cfg.interfaces)) pkgs.wireguard-tools
450 ++ optional (any (x: x.type == "amneziawg") (attrValues cfg.interfaces)) pkgs.amneziawg-tools;
451 systemd.services = mapAttrs' generateUnit cfg.interfaces;
452
453 # Prevent networkd from clearing the rules set by wg-quick when restarted (e.g. when waking up from suspend).
454 systemd.network.config.networkConfig.ManageForeignRoutingPolicyRules = mkDefault false;
455
456 # WireGuard interfaces should be ignored in determining whether the network is online.
457 systemd.network.wait-online.ignoredInterfaces = builtins.attrNames cfg.interfaces;
458 };
459}