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 };
99
100 };
101
102 # peer options
103
104 peerOpts = {
105
106 options = {
107
108 publicKey = mkOption {
109 example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
110 type = types.str;
111 description = "The base64 public key the peer.";
112 };
113
114 presharedKey = mkOption {
115 default = null;
116 example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I=";
117 type = with types; nullOr str;
118 description = ''
119 Base64 preshared key generated by wg genpsk. Optional,
120 and may be omitted. This option adds an additional layer of
121 symmetric-key cryptography to be mixed into the already existing
122 public-key cryptography, for post-quantum resistance.
123
124 Warning: Consider using presharedKeyFile instead if you do not
125 want to store the key in the world-readable Nix store.
126 '';
127 };
128
129 presharedKeyFile = mkOption {
130 default = null;
131 example = "/private/wireguard_psk";
132 type = with types; nullOr str;
133 description = ''
134 File pointing to preshared key as generated by wg pensk. Optional,
135 and may be omitted. This option adds an additional layer of
136 symmetric-key cryptography to be mixed into the already existing
137 public-key cryptography, for post-quantum resistance.
138 '';
139 };
140
141 allowedIPs = mkOption {
142 example = [ "10.192.122.3/32" "10.192.124.1/24" ];
143 type = with types; listOf str;
144 description = ''List of IP (v4 or v6) addresses with CIDR masks from
145 which this peer is allowed to send incoming traffic and to which
146 outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may
147 be specified for matching all IPv4 addresses, and ::/0 may be specified
148 for matching all IPv6 addresses.'';
149 };
150
151 endpoint = mkOption {
152 default = null;
153 example = "demo.wireguard.io:12913";
154 type = with types; nullOr str;
155 description = ''Endpoint IP or hostname of the peer, followed by a colon,
156 and then a port number of the peer.'';
157 };
158
159 persistentKeepalive = mkOption {
160 default = null;
161 type = with types; nullOr int;
162 example = 25;
163 description = ''This is optional and is by default off, because most
164 users will not need it. It represents, in seconds, between 1 and 65535
165 inclusive, how often to send an authenticated empty packet to the peer,
166 for the purpose of keeping a stateful firewall or NAT mapping valid
167 persistently. For example, if the interface very rarely sends traffic,
168 but it might at anytime receive traffic from a peer, and it is behind
169 NAT, the interface might benefit from having a persistent keepalive
170 interval of 25 seconds; however, most users will not need this.'';
171 };
172
173 };
174
175 };
176
177 ipCommand = "${pkgs.iproute}/bin/ip";
178 wgCommand = "${pkgs.wireguard}/bin/wg";
179
180 generateUnit = name: values:
181 # exactly one way to specify the private key must be set
182 assert (values.privateKey != null) != (values.privateKeyFile != null);
183 let privKey = if values.privateKeyFile != null then values.privateKeyFile else pkgs.writeText "wg-key" values.privateKey;
184 in
185 nameValuePair "wireguard-${name}"
186 {
187 description = "WireGuard Tunnel - ${name}";
188 after = [ "network.target" ];
189 wantedBy = [ "multi-user.target" ];
190
191 serviceConfig = {
192 Type = "oneshot";
193 RemainAfterExit = true;
194 ExecStart = flatten([
195 values.preSetup
196
197 "-${ipCommand} link del dev ${name}"
198 "${ipCommand} link add dev ${name} type wireguard"
199
200 (map (ip:
201 "${ipCommand} address add ${ip} dev ${name}"
202 ) values.ips)
203
204 ("${wgCommand} set ${name} private-key ${privKey}" +
205 optionalString (values.listenPort != null) " listen-port ${toString values.listenPort}")
206
207 (map (peer:
208 assert (peer.presharedKeyFile == null) || (peer.presharedKey == null); # at most one of the two must be set
209 let psk = if peer.presharedKey != null then pkgs.writeText "wg-psk" peer.presharedKey else peer.presharedKeyFile;
210 in
211 "${wgCommand} set ${name} peer ${peer.publicKey}" +
212 optionalString (psk != null) " preshared-key ${psk}" +
213 optionalString (peer.endpoint != null) " endpoint ${peer.endpoint}" +
214 optionalString (peer.persistentKeepalive != null) " persistent-keepalive ${toString peer.persistentKeepalive}" +
215 optionalString (peer.allowedIPs != []) " allowed-ips ${concatStringsSep "," peer.allowedIPs}"
216 ) values.peers)
217
218 "${ipCommand} link set up dev ${name}"
219
220 (map (peer:
221 (map (allowedIP:
222 "${ipCommand} route replace ${allowedIP} dev ${name} table ${values.table}"
223 ) peer.allowedIPs)
224 ) values.peers)
225
226 values.postSetup
227 ]);
228 ExecStop = flatten([
229 "${ipCommand} link del dev ${name}"
230 values.postShutdown
231 ]);
232 };
233 };
234
235in
236
237{
238
239 ###### interface
240
241 options = {
242
243 networking.wireguard = {
244
245 interfaces = mkOption {
246 description = "Wireguard interfaces.";
247 default = {};
248 example = {
249 wg0 = {
250 ips = [ "192.168.20.4/24" ];
251 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
252 peers = [
253 { allowedIPs = [ "192.168.20.1/32" ];
254 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
255 endpoint = "demo.wireguard.io:12913"; }
256 ];
257 };
258 };
259 type = with types; attrsOf (submodule interfaceOpts);
260 };
261
262 };
263
264 };
265
266
267 ###### implementation
268
269 config = mkIf (cfg.interfaces != {}) {
270
271 boot.extraModulePackages = [ kernel.wireguard ];
272 environment.systemPackages = [ pkgs.wireguard ];
273
274 systemd.services = mapAttrs' generateUnit cfg.interfaces;
275
276 };
277
278}