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