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