1 { config, lib, pkgs, ... }:
5 cfg = config.networking.wg-quick;
7 kernel = config.boot.kernelPackages;
11 interfaceOpts = { ... }: {
14 configFile = mkOption {
15 example = "/secret/wg0.conf";
17 type = with types; nullOr str;
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.
28 example = [ "192.168.2.1/24" ];
30 type = with types; listOf str;
31 description = "The IP addresses of the interface.";
34 autostart = mkOption {
35 description = "Whether to bring up this interface automatically during boot.";
42 example = [ "192.168.2.2" ];
44 type = with types; listOf str;
45 description = "The IP addresses of DNS servers to configure.";
48 privateKey = mkOption {
49 example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
50 type = with types; nullOr str;
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.
60 generatePrivateKeyFile = mkOption {
64 Automatically generate a private key with
65 {command}`wg genkey`, at the privateKeyFile location.
69 privateKeyFile = mkOption {
70 example = "/private/wireguard_key";
71 type = with types; nullOr str;
74 Private key file as generated by {command}`wg genkey`.
78 listenPort = mkOption {
80 type = with types; nullOr int;
83 16-bit port for listening. Optional; if not specified,
84 automatically generated based on interface name.
89 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"'';
91 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
93 Commands called at the start of the interface setup.
98 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"'';
100 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
102 Command called before the interface is taken down.
107 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"'';
109 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
111 Commands called after the interface setup.
115 postDown = mkOption {
116 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"'';
118 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
120 Command called after the interface is taken down.
127 type = with types; nullOr str;
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.
140 type = with types; nullOr int;
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.
151 description = "Peers linked to the interface.";
152 type = with types; listOf (submodule peerOpts);
161 publicKey = mkOption {
162 example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
164 description = "The base64 public key to the peer.";
167 presharedKey = mkOption {
169 example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I=";
170 type = with types; nullOr str;
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.
182 presharedKeyFile = mkOption {
184 example = "/private/wireguard_psk";
185 type = with types; nullOr str;
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.
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.'';
204 endpoint = mkOption {
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.'';
212 persistentKeepalive = mkOption {
214 type = with types; nullOr int;
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.'';
228 writeScriptFile = name: text: ((pkgs.writeShellScriptBin name text) + "/bin/${name}");
230 generatePrivateKeyScript = privateKeyFile: ''
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}")
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";
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;
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}";
259 destination = "/${name}.conf";
263 ${concatMapStringsSep "\n" (address:
264 "Address = ${address}"
266 ${concatMapStringsSep "\n" (dns:
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";
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"
290 if values.configFile != null then
291 # This uses bind-mounted private tmp folder (/tmp/systemd-private-***)
294 "${configDir}/${name}.conf";
296 nameValuePair "wg-quick-${name}"
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;
305 config.networking.firewall.package # iptables or nftables
306 config.networking.resolvconf.package # openresolv or systemd
311 RemainAfterExit = true;
315 ${optionalString (!config.boot.isContainer) "${pkgs.kmod}/bin/modprobe wireguard"}
316 ${optionalString (values.configFile != null) ''
317 cp ${values.configFile} ${configPath}
319 wg-quick up ${configPath}
323 # Used to privately store renamed copies of external config files during activation
328 wg-quick down ${configPath}
336 networking.wg-quick = {
337 interfaces = mkOption {
338 description = "Wireguard interfaces.";
342 address = [ "192.168.20.4/24" ];
343 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
345 { allowedIPs = [ "192.168.20.1/32" ];
346 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
347 endpoint = "demo.wireguard.io:12913"; }
351 type = with types; attrsOf (submodule interfaceOpts);
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;