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;
18 description = lib.mdDoc ''
19 wg-quick .conf file, describing the interface.
20 This overrides any other configuration interface configuration options.
21 See wg-quick manpage for more details.
26 example = [ "192.168.2.1/24" ];
28 type = with types; listOf str;
29 description = lib.mdDoc "The IP addresses of the interface.";
32 autostart = mkOption {
33 description = lib.mdDoc "Whether to bring up this interface automatically during boot.";
40 example = [ "192.168.2.2" ];
42 type = with types; listOf str;
43 description = lib.mdDoc "The IP addresses of DNS servers to configure.";
46 privateKey = mkOption {
47 example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
48 type = with types; nullOr str;
50 description = lib.mdDoc ''
51 Base64 private key generated by {command}`wg genkey`.
53 Warning: Consider using privateKeyFile instead if you do not
54 want to store the key in the world-readable Nix store.
58 privateKeyFile = mkOption {
59 example = "/private/wireguard_key";
60 type = with types; nullOr str;
62 description = lib.mdDoc ''
63 Private key file as generated by {command}`wg genkey`.
67 listenPort = mkOption {
69 type = with types; nullOr int;
71 description = lib.mdDoc ''
72 16-bit port for listening. Optional; if not specified,
73 automatically generated based on interface name.
78 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"'';
80 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
81 description = lib.mdDoc ''
82 Commands called at the start of the interface setup.
87 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"'';
89 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
90 description = lib.mdDoc ''
91 Command called before the interface is taken down.
96 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"'';
98 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
99 description = lib.mdDoc ''
100 Commands called after the interface setup.
104 postDown = mkOption {
105 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"'';
107 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
108 description = lib.mdDoc ''
109 Command called after the interface is taken down.
116 type = with types; nullOr str;
117 description = lib.mdDoc ''
118 The kernel routing table to add this interface's
119 associated routes to. Setting this is useful for e.g. policy routing
120 ("ip rule") or virtual routing and forwarding ("ip vrf"). Both
121 numeric table IDs and table names (/etc/rt_tables) can be used.
129 type = with types; nullOr int;
130 description = lib.mdDoc ''
131 If not specified, the MTU is automatically determined
132 from the endpoint addresses or the system default route, which is usually
133 a sane choice. However, to manually specify an MTU to override this
134 automatic discovery, this value may be specified explicitly.
140 description = lib.mdDoc "Peers linked to the interface.";
141 type = with types; listOf (submodule peerOpts);
150 publicKey = mkOption {
151 example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
153 description = lib.mdDoc "The base64 public key to the peer.";
156 presharedKey = mkOption {
158 example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I=";
159 type = with types; nullOr str;
160 description = lib.mdDoc ''
161 Base64 preshared key generated by {command}`wg genpsk`.
162 Optional, and may be omitted. This option adds an additional layer of
163 symmetric-key cryptography to be mixed into the already existing
164 public-key cryptography, for post-quantum resistance.
166 Warning: Consider using presharedKeyFile instead if you do not
167 want to store the key in the world-readable Nix store.
171 presharedKeyFile = mkOption {
173 example = "/private/wireguard_psk";
174 type = with types; nullOr str;
175 description = lib.mdDoc ''
176 File pointing to preshared key as generated by {command}`wg genpsk`.
177 Optional, and may be omitted. This option adds an additional layer of
178 symmetric-key cryptography to be mixed into the already existing
179 public-key cryptography, for post-quantum resistance.
183 allowedIPs = mkOption {
184 example = [ "10.192.122.3/32" "10.192.124.1/24" ];
185 type = with types; listOf str;
186 description = lib.mdDoc ''List of IP (v4 or v6) addresses with CIDR masks from
187 which this peer is allowed to send incoming traffic and to which
188 outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may
189 be specified for matching all IPv4 addresses, and ::/0 may be specified
190 for matching all IPv6 addresses.'';
193 endpoint = mkOption {
195 example = "demo.wireguard.io:12913";
196 type = with types; nullOr str;
197 description = lib.mdDoc ''Endpoint IP or hostname of the peer, followed by a colon,
198 and then a port number of the peer.'';
201 persistentKeepalive = mkOption {
203 type = with types; nullOr int;
205 description = lib.mdDoc ''This is optional and is by default off, because most
206 users will not need it. It represents, in seconds, between 1 and 65535
207 inclusive, how often to send an authenticated empty packet to the peer,
208 for the purpose of keeping a stateful firewall or NAT mapping valid
209 persistently. For example, if the interface very rarely sends traffic,
210 but it might at anytime receive traffic from a peer, and it is behind
211 NAT, the interface might benefit from having a persistent keepalive
212 interval of 25 seconds; however, most users will not need this.'';
217 writeScriptFile = name: text: ((pkgs.writeShellScriptBin name text) + "/bin/${name}");
219 generateUnit = name: values:
220 assert assertMsg (values.configFile != null || ((values.privateKey != null) != (values.privateKeyFile != null))) "Only one of privateKey, configFile or privateKeyFile may be set";
222 preUpFile = if values.preUp != "" then writeScriptFile "preUp.sh" values.preUp else null;
224 optional (values.privateKeyFile != null) "wg set ${name} private-key <(cat ${values.privateKeyFile})" ++
225 (concatMap (peer: optional (peer.presharedKeyFile != null) "wg set ${name} peer ${peer.publicKey} preshared-key <(cat ${peer.presharedKeyFile})") values.peers) ++
226 optional (values.postUp != "") values.postUp;
227 postUpFile = if postUp != [] then writeScriptFile "postUp.sh" (concatMapStringsSep "\n" (line: line) postUp) else null;
228 preDownFile = if values.preDown != "" then writeScriptFile "preDown.sh" values.preDown else null;
229 postDownFile = if values.postDown != "" then writeScriptFile "postDown.sh" values.postDown else null;
230 configDir = pkgs.writeTextFile {
231 name = "config-${name}";
233 destination = "/${name}.conf";
237 ${concatMapStringsSep "\n" (address:
238 "Address = ${address}"
240 ${concatMapStringsSep "\n" (dns:
244 optionalString (values.table != null) "Table = ${values.table}\n" +
245 optionalString (values.mtu != null) "MTU = ${toString values.mtu}\n" +
246 optionalString (values.privateKey != null) "PrivateKey = ${values.privateKey}\n" +
247 optionalString (values.listenPort != null) "ListenPort = ${toString values.listenPort}\n" +
248 optionalString (preUpFile != null) "PreUp = ${preUpFile}\n" +
249 optionalString (postUpFile != null) "PostUp = ${postUpFile}\n" +
250 optionalString (preDownFile != null) "PreDown = ${preDownFile}\n" +
251 optionalString (postDownFile != null) "PostDown = ${postDownFile}\n" +
252 concatMapStringsSep "\n" (peer:
253 assert assertMsg (!((peer.presharedKeyFile != null) && (peer.presharedKey != null))) "Only one of presharedKey or presharedKeyFile may be set";
255 "PublicKey = ${peer.publicKey}\n" +
256 optionalString (peer.presharedKey != null) "PresharedKey = ${peer.presharedKey}\n" +
257 optionalString (peer.endpoint != null) "Endpoint = ${peer.endpoint}\n" +
258 optionalString (peer.persistentKeepalive != null) "PersistentKeepalive = ${toString peer.persistentKeepalive}\n" +
259 optionalString (peer.allowedIPs != []) "AllowedIPs = ${concatStringsSep "," peer.allowedIPs}\n"
263 if values.configFile != null then
264 # This uses bind-mounted private tmp folder (/tmp/systemd-private-***)
267 "${configDir}/${name}.conf";
269 nameValuePair "wg-quick-${name}"
271 description = "wg-quick WireGuard Tunnel - ${name}";
272 requires = [ "network-online.target" ];
273 after = [ "network.target" "network-online.target" ];
274 wantedBy = optional values.autostart "multi-user.target";
275 environment.DEVICE = name;
276 path = [ pkgs.kmod pkgs.wireguard-tools config.networking.resolvconf.package ];
280 RemainAfterExit = true;
284 ${optionalString (!config.boot.isContainer) "modprobe wireguard"}
285 ${optionalString (values.configFile != null) ''
286 cp ${values.configFile} ${configPath}
288 wg-quick up ${configPath}
292 # Used to privately store renamed copies of external config files during activation
297 wg-quick down ${configPath}
305 networking.wg-quick = {
306 interfaces = mkOption {
307 description = lib.mdDoc "Wireguard interfaces.";
311 address = [ "192.168.20.4/24" ];
312 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
314 { allowedIPs = [ "192.168.20.1/32" ];
315 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
316 endpoint = "demo.wireguard.io:12913"; }
320 type = with types; attrsOf (submodule interfaceOpts);
326 ###### implementation
328 config = mkIf (cfg.interfaces != {}) {
329 boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard;
330 environment.systemPackages = [ pkgs.wireguard-tools ];
331 systemd.services = mapAttrs' generateUnit cfg.interfaces;
333 # Prevent networkd from clearing the rules set by wg-quick when restarted (e.g. when waking up from suspend).
334 systemd.network.config.networkConfig.ManageForeignRoutingPolicyRules = mkDefault false;
336 # WireGuard interfaces should be ignored in determining whether the network is online.
337 systemd.network.wait-online.ignoredInterfaces = builtins.attrNames cfg.interfaces;