]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/networking/wireguard.nix
nix: reuse julm-nix nixos/default.nix
[sourcephile-nix.git] / nixos / modules / services / networking / wireguard.nix
1 { config, lib, pkgs, ... }:
2
3 with lib;
4
5 let
6
7 cfg = config.networking.wireguard;
8
9 interfaceOpts = { config, ... }: {
10 options.peers = mkOption {
11 type = with types; listOf (submodule peerOpts);
12 };
13 options.peersAnnouncing = {
14 enable = mkEnableOption ''
15 announcing of peers' endpoints.
16 '';
17 listenPort = mkOption {
18 type = types.port;
19 defaultText = "<option>networking.wireguard.listenPort</option> or 51820 if null";
20 description = ''
21 TCP port on which to listen for peers queries for other peers' endpoints.
22
23 <warning><para>
24 This exposes the public key and current (or last known) endpoint address of all the peers
25 configured in the WireGuard interface, to all the peers,
26 and to any host connected to the peers or announcing peer
27 able to query the WireGuard interface
28 by spoofing the allowedIPs of any peer.
29 </para></warning>
30 '';
31 };
32 };
33
34 config.peersAnnouncing = {
35 listenPort = mkDefault (if config.listenPort == null then 51820 else config.listenPort);
36 };
37 };
38
39 peerOpts = { config, ... }: {
40 options.endpointsUpdater = {
41 enable = mkEnableOption ''
42 receiving the enpoint of other peers from a periodic TCP connection.
43 That connection is meant to be to a peer using <option>networking.wireguard.peersAnnouncing.enable</option>.
44
45 This is especially useful to punch-through non-symmetric NATs
46 effectively establishing peer-to-peer connectivity:
47 the peers initiate a Wireguard connection to the announcing peer,
48 then use that Wireguard tunnel to query the endpoints of the other peers
49 to establish Wireguard tunnels to them,
50 using UDP hole punching when both peers are behind non-symmetric NATs.
51
52 Note that it can also work for two peers behind the same NAT
53 if that NAT can reflect (to its internal network) connections
54 from its internal network to its external IP address,
55 which may require to set persistentKeepalive as low as 1
56 and/or redirecting at least one host's WireGuard port, eg. with UPnP.
57 '';
58 addr = mkOption {
59 type = types.str;
60 defaultText = "address of the first item of the peer's <option>allowedIPs</option>";
61 description = ''
62 Address at which to contact the announcing peer,
63 configured on the announcing peer's <option>networking.wireguard.ips</option>.
64 '';
65 };
66 port = mkOption {
67 type = types.port;
68 defaultText = "port of the peer's endpoint";
69 description = ''
70 TCP port on which to contact the peer announcing other peers.
71 This is configured on the announcing peer's <option>networking.wireguard.peersAnnouncing.listenPort</option>.
72 '';
73 };
74 refreshSeconds = mkOption {
75 type = with types; int;
76 example = 120;
77 default = 60;
78 description = ''
79 Time to wait between two successive queries of the endpoints known by the peer.
80 '';
81 };
82 };
83
84 config.endpointsUpdater = {
85 addr = mkDefault (head (builtins.match "^\([^/]*\).*$" (head (config.allowedIPs))));
86 port = mkDefault (toInt (head (builtins.match "^.*:\([0-9]*\)$" config.endpoint)));
87 };
88 };
89
90 keyToUnitName = replaceChars
91 [ "/" "-" " " "+" "=" ]
92 [ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ];
93
94 peerUnitServiceName = interfaceName: publicKey: dynamicRefreshEnabled:
95 let
96 unitName = keyToUnitName publicKey;
97 refreshSuffix = optionalString dynamicRefreshEnabled "-refresh";
98 in
99 "wireguard-${interfaceName}-peer-${unitName}${refreshSuffix}";
100
101 # See:
102 # - systemd-analyze security wireguard-${iface}-peers-announcing@
103 # - systemd-analyze security wireguard-${iface}-endpoints-updater-${public_key}.service
104 # Note that PrivateUsers=true would be too restrictive wrt. capabilities.
105 peerUpdateSecurity = {
106 IPAddressDeny = "any";
107 # For running wg(1)
108 AmbientCapabilities = [ "CAP_NET_ADMIN" ];
109 CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
110 RestrictNamespaces = true;
111 DynamicUser = true;
112 PrivateDevices = true;
113 ProtectClock = true;
114 ProtectControlGroups = true;
115 ProtectHome = true;
116 ProtectKernelLogs = true;
117 ProtectKernelModules = true;
118 ProtectKernelTunables = true;
119 ProtectProc = "invisible";
120 SystemCallArchitectures = "native";
121 # Remove (likely) unused groups from the basic @system-service group
122 SystemCallFilter = [
123 "@system-service"
124 "~@aio" "~@chown" "~@keyring" "~@privileged"
125 "~@memlock" "~@resources" "~@setuid"
126 ];
127 RestrictRealtime = true;
128 LockPersonality = true;
129 MemoryDenyWriteExecute = true;
130 UMask = 0077;
131 ProtectHostname = true;
132 ProcSubset = "pid";
133 };
134
135 generatePeersAnnouncingSocket = name: values:
136 nameValuePair "wireguard-${name}-peers-announcing"
137 {
138 enable = values.peersAnnouncing.enable;
139 listenStreams = [(toString values.peersAnnouncing.listenPort)];
140 socketConfig.Accept = true;
141 # Basic firewalling restricting answers to peers
142 # querying an internal IP address of the announcing peer.
143 # Note that IPv4 addresses can be spoofed using other interfaces unless
144 # sysctl net.ipv4.conf.${name}.rp_filter=1
145 socketConfig.BindToDevice = name;
146 socketConfig.IPAddressAllow = map (peer: peer.allowedIPs) values.peers;
147 socketConfig.IPAddressDeny = "any";
148 socketConfig.MaxConnectionsPerSource = 1;
149 socketConfig.ReusePort = true;
150 wantedBy = [ "sockets.target" ];
151 };
152
153 generatePeersAnnouncingUnit = name: values:
154 nameValuePair "wireguard-${name}-peers-announcing@"
155 {
156 description = "WireGuard Peers Announcing - ${name}";
157 requires = [ "wireguard-${name}.service" ];
158 after = [ "wireguard-${name}.service" ];
159
160 serviceConfig = mkMerge [
161 peerUpdateSecurity
162 {
163 Type = "simple";
164 ExecStart = "${pkgs.wireguard-tools}/bin/wg show '${name}' endpoints";
165 StandardInput = "null";
166 StandardOutput = "socket";
167 RestrictAddressFamilies = "";
168 }
169 (mkIf (values.interfaceNamespace != null)
170 { NetworkNamespacePath = "/var/run/netns/${values.interfaceNamespace}"; })
171 ];
172 };
173
174 generateEndpointsUpdaterUnit = { interfaceName, interfaceCfg, peer }: let
175 dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0;
176 peerService = peerUnitServiceName interfaceName peer.publicKey dynamicRefreshEnabled;
177 in
178 nameValuePair "wireguard-${interfaceName}-endpoints-updater-${keyToUnitName peer.publicKey}"
179 {
180 description = "WireGuard ${interfaceName} Endpoints Updater - ${peer.publicKey}";
181 requires = [ "${peerService}.service" ];
182 after = [ "${peerService}.service" ];
183 wantedBy = [ "${peerService}.service" ];
184 path = with pkgs; [ wireguard-tools ];
185
186 unitConfig = {
187 StartLimitIntervalSec = 0;
188 };
189 serviceConfig = mkMerge [
190 peerUpdateSecurity
191 {
192 Type = "simple";
193 IPAddressAllow = [ peer.endpointsUpdater.addr ];
194 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" ];
195 Restart = "on-failure";
196 }
197 (mkIf (interfaceCfg.interfaceNamespace != null)
198 { NetworkNamespacePath = "/var/run/netns/${interfaceCfg.interfaceNamespace}"; })
199 ];
200
201 # Query the peer announcing other peers
202 # for setting the current endpoint of all configured peers
203 # (but the announcing peer).
204 # Note that socat is used instead of libressl's netcat
205 # (which would require MemoryDenyWriteExecute=true to load libtls.so)
206 # or netcat-gnu (which does not work on ARM and has last been released in 2004).
207 script = ''
208 wait="${toString peer.endpointsUpdater.refreshSeconds}"
209 declare -A configured_keys
210 configured_keys=(${concatMapStringsSep " " (p:
211 optionalString (p.publicKey != peer.publicKey) "[${p.publicKey}]=set")
212 interfaceCfg.peers})
213
214 while true; do
215 # Set stdin to socat's stdout
216 exec < <(exec ${pkgs.socat}/bin/socat STDOUT \
217 "TCP:${with peer.endpointsUpdater; addr+":"+toString port}")
218
219 # Update the endpoint of each configured peer
220 while read -t "$wait" -n 128 -r public_key endpoint x; do
221 if [ "$endpoint" != "(none)" -a "''${configured_keys[$public_key]}" ]; then
222 wg set "${interfaceName}" peer "$public_key" endpoint "$endpoint"
223 fi;
224 done
225
226 sleep "$wait"
227 done
228 '';
229 };
230
231 in
232
233 {
234
235 options.networking.wireguard.interfaces = mkOption {
236 type = with types; attrsOf (submodule interfaceOpts);
237 };
238
239 config = mkIf cfg.enable (let
240 all_peers = flatten
241 (mapAttrsToList (interfaceName: interfaceCfg:
242 map (peer: { inherit interfaceName interfaceCfg peer;}) interfaceCfg.peers
243 ) cfg.interfaces);
244 in {
245
246 systemd.sockets =
247 mapAttrs' generatePeersAnnouncingSocket cfg.interfaces;
248
249 systemd.services =
250 mapAttrs' generatePeersAnnouncingUnit cfg.interfaces //
251 (listToAttrs (map generateEndpointsUpdaterUnit
252 (filter ({peer, ...}: peer.endpointsUpdater.enable) all_peers)));
253
254 });
255
256 }