1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.networking.wireguard;
8
9 kernel = config.boot.kernelPackages;
10
11 # interface options
12
13 interfaceOpts = { name, ... }: {
14
15 options = {
16
17 ips = mkOption {
18 example = [ "192.168.2.1/24" ];
19 default = [];
20 type = with types; listOf str;
21 description = "The IP addresses of the interface.";
22 };
23
24 privateKey = mkOption {
25 example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
26 type = with types; nullOr str;
27 default = null;
28 description = ''
29 Base64 private key generated by wg genkey.
30
31 Warning: Consider using privateKeyFile instead if you do not
32 want to store the key in the world-readable Nix store.
33 '';
34 };
35
36 privateKeyFile = mkOption {
37 example = "/private/wireguard_key";
38 type = with types; nullOr str;
39 default = null;
40 description = ''
41 Private key file as generated by wg genkey.
42 '';
43 };
44
45 listenPort = mkOption {
46 default = null;
47 type = with types; nullOr int;
48 example = 51820;
49 description = ''
50 16-bit port for listening. Optional; if not specified,
51 automatically generated based on interface name.
52 '';
53 };
54
55 preSetup = mkOption {
56 example = literalExample [''
57 ${pkgs.iproute}/bin/ip netns add foo
58 ''];
59 default = [];
60 type = with types; listOf str;
61 description = ''
62 A list of commands called at the start of the interface setup.
63 '';
64 };
65
66 postSetup = mkOption {
67 example = literalExample [''
68 ${pkgs.bash} -c 'printf "nameserver 10.200.100.1" | ${pkgs.openresolv}/bin/resolvconf -a wg0 -m 0'
69 ''];
70 default = [];
71 type = with types; listOf str;
72 description = "A list of commands called at the end of the interface setup.";
73 };
74
75 postShutdown = mkOption {
76 example = literalExample ["${pkgs.openresolv}/bin/resolvconf -d wg0"];
77 default = [];
78 type = with types; listOf str;
79 description = "A list of commands called after shutting down the interface.";
80 };
81
82 table = mkOption {
83 default = "main";
84 type = types.str;
85 description = ''The kernel routing table to add this interface's
86 associated routes to. Setting this is useful for e.g. policy routing
87 ("ip rule") or virtual routing and forwarding ("ip vrf"). Both numeric
88 table IDs and table names (/etc/rt_tables) can be used. Defaults to
89 "main".'';
90 };
91
92 peers = mkOption {
93 default = [];
94 description = "Peers linked to the interface.";
95 type = with types; listOf (submodule peerOpts);
96 };
97
98 allowedIPsAsRoutes = mkOption {
99 example = false;
100 default = true;
101 type = types.bool;
102 description = ''
103 Determines whether to add allowed IPs as routes or not.
104 '';
105 };
106 };
107
108 };
109
110 # peer options
111
112 peerOpts = {
113
114 options = {
115
116 publicKey = mkOption {
117 example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
118 type = types.str;
119 description = "The base64 public key the peer.";
120 };
121
122 presharedKey = mkOption {
123 default = null;
124 example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I=";
125 type = with types; nullOr str;
126 description = ''
127 Base64 preshared key generated by wg genpsk. Optional,
128 and may be omitted. This option adds an additional layer of
129 symmetric-key cryptography to be mixed into the already existing
130 public-key cryptography, for post-quantum resistance.
131
132 Warning: Consider using presharedKeyFile instead if you do not
133 want to store the key in the world-readable Nix store.
134 '';
135 };
136
137 presharedKeyFile = mkOption {
138 default = null;
139 example = "/private/wireguard_psk";
140 type = with types; nullOr str;
141 description = ''
142 File pointing to preshared key as generated by wg pensk. Optional,
143 and may be omitted. This option adds an additional layer of
144 symmetric-key cryptography to be mixed into the already existing
145 public-key cryptography, for post-quantum resistance.
146 '';
147 };
148
149 allowedIPs = mkOption {
150 example = [ "10.192.122.3/32" "10.192.124.1/24" ];
151 type = with types; listOf str;
152 description = ''List of IP (v4 or v6) addresses with CIDR masks from
153 which this peer is allowed to send incoming traffic and to which
154 outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may
155 be specified for matching all IPv4 addresses, and ::/0 may be specified
156 for matching all IPv6 addresses.'';
157 };
158
159 endpoint = mkOption {
160 default = null;
161 example = "demo.wireguard.io:12913";
162 type = with types; nullOr str;
163 description = ''Endpoint IP or hostname of the peer, followed by a colon,
164 and then a port number of the peer.'';
165 };
166
167 persistentKeepalive = mkOption {
168 default = null;
169 type = with types; nullOr int;
170 example = 25;
171 description = ''This is optional and is by default off, because most
172 users will not need it. It represents, in seconds, between 1 and 65535
173 inclusive, how often to send an authenticated empty packet to the peer,
174 for the purpose of keeping a stateful firewall or NAT mapping valid
175 persistently. For example, if the interface very rarely sends traffic,
176 but it might at anytime receive traffic from a peer, and it is behind
177 NAT, the interface might benefit from having a persistent keepalive
178 interval of 25 seconds; however, most users will not need this.'';
179 };
180
181 };
182
183 };
184
185 ipCommand = "${pkgs.iproute}/bin/ip";
186 wgCommand = "${pkgs.wireguard}/bin/wg";
187
188 generateUnit = name: values:
189 # exactly one way to specify the private key must be set
190 assert (values.privateKey != null) != (values.privateKeyFile != null);
191 let privKey = if values.privateKeyFile != null then values.privateKeyFile else pkgs.writeText "wg-key" values.privateKey;
192 in
193 nameValuePair "wireguard-${name}"
194 {
195 description = "WireGuard Tunnel - ${name}";
196 after = [ "network.target" ];
197 wantedBy = [ "multi-user.target" ];
198 environment.DEVICE = name;
199
200 serviceConfig = {
201 Type = "oneshot";
202 RemainAfterExit = true;
203 ExecStart = flatten([
204 values.preSetup
205
206 "-${ipCommand} link del dev ${name}"
207 "${ipCommand} link add dev ${name} type wireguard"
208
209 (map (ip:
210 "${ipCommand} address add ${ip} dev ${name}"
211 ) values.ips)
212
213 ("${wgCommand} set ${name} private-key ${privKey}" +
214 optionalString (values.listenPort != null) " listen-port ${toString values.listenPort}")
215
216 (map (peer:
217 assert (peer.presharedKeyFile == null) || (peer.presharedKey == null); # at most one of the two must be set
218 let psk = if peer.presharedKey != null then pkgs.writeText "wg-psk" peer.presharedKey else peer.presharedKeyFile;
219 in
220 "${wgCommand} set ${name} peer ${peer.publicKey}" +
221 optionalString (psk != null) " preshared-key ${psk}" +
222 optionalString (peer.endpoint != null) " endpoint ${peer.endpoint}" +
223 optionalString (peer.persistentKeepalive != null) " persistent-keepalive ${toString peer.persistentKeepalive}" +
224 optionalString (peer.allowedIPs != []) " allowed-ips ${concatStringsSep "," peer.allowedIPs}"
225 ) values.peers)
226
227 "${ipCommand} link set up dev ${name}"
228
229 (optionals (values.allowedIPsAsRoutes != false) (map (peer:
230 (map (allowedIP:
231 "${ipCommand} route replace ${allowedIP} dev ${name} table ${values.table}"
232 ) peer.allowedIPs)
233 ) values.peers))
234
235 values.postSetup
236 ]);
237 ExecStop = flatten([
238 "${ipCommand} link del dev ${name}"
239 values.postShutdown
240 ]);
241 };
242 };
243
244in
245
246{
247
248 ###### interface
249
250 options = {
251
252 networking.wireguard = {
253
254 interfaces = mkOption {
255 description = "Wireguard interfaces.";
256 default = {};
257 example = {
258 wg0 = {
259 ips = [ "192.168.20.4/24" ];
260 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
261 peers = [
262 { allowedIPs = [ "192.168.20.1/32" ];
263 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
264 endpoint = "demo.wireguard.io:12913"; }
265 ];
266 };
267 };
268 type = with types; attrsOf (submodule interfaceOpts);
269 };
270
271 };
272
273 };
274
275
276 ###### implementation
277
278 config = mkIf (cfg.interfaces != {}) {
279
280 boot.extraModulePackages = [ kernel.wireguard ];
281 environment.systemPackages = [ pkgs.wireguard ];
282
283 systemd.services = mapAttrs' generateUnit cfg.interfaces;
284
285 };
286
287}