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 = { ... }: {
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; coercedTo (listOf str) (concatStringsSep "\n") lines;
61 description = ''
62 Commands called at the start of the interface setup.
63 '';
64 };
65
66 postSetup = mkOption {
67 example = literalExample ''
68 printf "nameserver 10.200.100.1" | ${pkgs.openresolv}/bin/resolvconf -a wg0 -m 0
69 '';
70 default = "";
71 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines;
72 description = "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; coercedTo (listOf str) (concatStringsSep "\n") lines;
79 description = "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 generateUnit = name: values:
186 # exactly one way to specify the private key must be set
187 assert (values.privateKey != null) != (values.privateKeyFile != null);
188 let privKey = if values.privateKeyFile != null then values.privateKeyFile else pkgs.writeText "wg-key" values.privateKey;
189 in
190 nameValuePair "wireguard-${name}"
191 {
192 description = "WireGuard Tunnel - ${name}";
193 requires = [ "network-online.target" ];
194 after = [ "network.target" "network-online.target" ];
195 wantedBy = [ "multi-user.target" ];
196 environment.DEVICE = name;
197 path = with pkgs; [ kmod iproute wireguard-tools ];
198
199 serviceConfig = {
200 Type = "oneshot";
201 RemainAfterExit = true;
202 };
203
204 script = ''
205 modprobe wireguard
206
207 ${values.preSetup}
208
209 ip link add dev ${name} type wireguard
210
211 ${concatMapStringsSep "\n" (ip:
212 "ip address add ${ip} dev ${name}"
213 ) values.ips}
214
215 wg set ${name} private-key ${privKey} ${
216 optionalString (values.listenPort != null) " listen-port ${toString values.listenPort}"}
217
218 ${concatMapStringsSep "\n" (peer:
219 assert (peer.presharedKeyFile == null) || (peer.presharedKey == null); # at most one of the two must be set
220 let psk = if peer.presharedKey != null then pkgs.writeText "wg-psk" peer.presharedKey else peer.presharedKeyFile;
221 in
222 "wg set ${name} peer ${peer.publicKey}" +
223 optionalString (psk != null) " preshared-key ${psk}" +
224 optionalString (peer.endpoint != null) " endpoint ${peer.endpoint}" +
225 optionalString (peer.persistentKeepalive != null) " persistent-keepalive ${toString peer.persistentKeepalive}" +
226 optionalString (peer.allowedIPs != []) " allowed-ips ${concatStringsSep "," peer.allowedIPs}"
227 ) values.peers}
228
229 ip link set up dev ${name}
230
231 ${optionalString (values.allowedIPsAsRoutes != false) (concatStringsSep "\n" (concatMap (peer:
232 (map (allowedIP:
233 "ip route replace ${allowedIP} dev ${name} table ${values.table}"
234 ) peer.allowedIPs)
235 ) values.peers))}
236
237 ${values.postSetup}
238 '';
239
240 postStop = ''
241 ip link del dev ${name}
242 ${values.postShutdown}
243 '';
244 };
245
246in
247
248{
249
250 ###### interface
251
252 options = {
253
254 networking.wireguard = {
255
256 interfaces = mkOption {
257 description = "Wireguard interfaces.";
258 default = {};
259 example = {
260 wg0 = {
261 ips = [ "192.168.20.4/24" ];
262 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=";
263 peers = [
264 { allowedIPs = [ "192.168.20.1/32" ];
265 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
266 endpoint = "demo.wireguard.io:12913"; }
267 ];
268 };
269 };
270 type = with types; attrsOf (submodule interfaceOpts);
271 };
272
273 };
274
275 };
276
277
278 ###### implementation
279
280 config = mkIf (cfg.interfaces != {}) {
281
282 boot.extraModulePackages = [ kernel.wireguard ];
283 environment.systemPackages = [ pkgs.wireguard-tools ];
284
285 systemd.services = mapAttrs' generateUnit cfg.interfaces;
286
287 };
288
289}