]> Git — Sourcephile - julm/julm-nix.git/blob - nixos/modules/services/networking/wireguard.nix
nm: restart online check
[julm/julm-nix.git] / nixos / modules / services / networking / wireguard.nix
1 { config, lib, options, pkgs, ... }:
2 with lib;
3 let
4 cfg = config.networking.wireguard;
5 opt = options.networking.wireguard;
6 interfaceOpts = { config, ... }: {
7 options = {
8 peers = mkOption {
9 type = with types; listOf (submodule peerOpts);
10 };
11 peersAnnouncing = {
12 enable = mkEnableOption ''
13 announcing of peers' endpoints.
14 '';
15 listenPort = mkOption {
16 type = types.port;
17 defaultText = "<option>networking.wireguard.listenPort</option> or 51820 if null";
18 description = ''
19 TCP port on which to listen for peers queries for other peers' endpoints.
20
21 <warning><para>
22 This exposes the public key and current (or last known) endpoint address of all the peers
23 configured in the WireGuard interface, to all the peers,
24 and to any host connected to the peers or announcing peer
25 able to query the WireGuard interface
26 by spoofing the allowedIPs of any peer.
27 </para></warning>
28 '';
29 };
30 };
31 };
32 config = {
33 peersAnnouncing = {
34 listenPort = mkDefault (if config.listenPort == null then 51820 else config.listenPort);
35 };
36 };
37 };
38 peerOpts = { config, ... }: {
39 options = {
40 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 = {
85 endpointsUpdater = {
86 addr = mkDefault (head (builtins.match "^\([^/]*\).*$" (head (config.allowedIPs))));
87 port = mkDefault (toInt (head (builtins.match "^.*:\([0-9]*\)$" config.endpoint)));
88 };
89 };
90 };
91
92 keyToUnitName = replaceChars
93 [ "/" "-" " " "+" "=" ]
94 [ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ];
95
96 peerUnitServiceName = interfaceName: publicKey: dynamicRefreshEnabled:
97 let
98 unitName = keyToUnitName publicKey;
99 refreshSuffix = optionalString dynamicRefreshEnabled "-refresh";
100 in
101 "wireguard-${interfaceName}-peer-${unitName}${refreshSuffix}";
102
103 # See:
104 # - systemd-analyze security wireguard-${iface}-peers-announcing@
105 # - systemd-analyze security wireguard-${iface}-endpoints-updater-${public_key}.service
106 # Note that PrivateUsers=true would be too restrictive wrt. capabilities.
107 peerUpdateSecurity = {
108 IPAddressDeny = "any";
109 # For running wg(1)
110 AmbientCapabilities = [ "CAP_NET_ADMIN" ];
111 CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
112 RestrictNamespaces = true;
113 DynamicUser = true;
114 NoNewPrivileges = true;
115 PrivateDevices = true;
116 ProtectClock = true;
117 ProtectControlGroups = true;
118 ProtectHome = true;
119 ProtectKernelLogs = true;
120 ProtectKernelModules = true;
121 ProtectKernelTunables = true;
122 ProtectProc = "invisible";
123 SystemCallArchitectures = "native";
124 # Remove (likely) unused groups from the basic @system-service group
125 SystemCallFilter = [
126 "@system-service"
127 "~@aio"
128 "~@chown"
129 "~@keyring"
130 "~@privileged"
131 "~@memlock"
132 "~@resources"
133 "~@setuid"
134 ];
135 RestrictRealtime = true;
136 LockPersonality = true;
137 MemoryDenyWriteExecute = true;
138 UMask = 0077;
139 ProtectHostname = true;
140 ProcSubset = "pid";
141 };
142
143 generatePeerUnit = { interfaceName, interfaceCfg, peer }:
144 let
145 dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0;
146 serviceName = peerUnitServiceName interfaceName peer.publicKey dynamicRefreshEnabled;
147 in
148 nameValuePair serviceName
149 {
150 serviceConfig =
151 {
152 # Overcomes failure to resolve the endpoint's host
153 # (eg, when the resolver is not yet reachable).
154 Restart = "on-failure";
155 };
156 };
157
158 generatePeersAnnouncingSocket = name: values:
159 nameValuePair "wireguard-${name}-peers-announcing"
160 {
161 enable = values.peersAnnouncing.enable;
162 listenStreams = [ (toString values.peersAnnouncing.listenPort) ];
163 socketConfig.Accept = true;
164 # Basic firewalling restricting answers to peers
165 # querying an internal IP address of the announcing peer.
166 # Note that IPv4 addresses can be spoofed using other interfaces unless
167 # sysctl net.ipv4.conf.${name}.rp_filter=1
168 socketConfig.BindToDevice = name;
169 socketConfig.IPAddressAllow = map (peer: peer.allowedIPs) values.peers;
170 socketConfig.IPAddressDeny = "any";
171 socketConfig.MaxConnectionsPerSource = 1;
172 socketConfig.ReusePort = true;
173 wantedBy = [ "sockets.target" ];
174 };
175
176 generatePeersAnnouncingUnit = name: values:
177 nameValuePair "wireguard-${name}-peers-announcing@"
178 {
179 description = "WireGuard Peers Announcing - ${name}";
180 requires = [ "wireguard-${name}.service" ];
181 after = [ "wireguard-${name}.service" ];
182
183 serviceConfig = mkMerge [
184 peerUpdateSecurity
185 {
186 Type = "simple";
187 ExecStart = "${pkgs.wireguard-tools}/bin/wg show '${name}' endpoints";
188 StandardInput = "null";
189 StandardOutput = "socket";
190 RestrictAddressFamilies = "";
191 }
192 (mkIf (values.interfaceNamespace != null)
193 { NetworkNamespacePath = "/var/run/netns/${values.interfaceNamespace}"; })
194 ];
195 };
196
197 generateEndpointsUpdaterUnit = { interfaceName, interfaceCfg, peer }:
198 let
199 dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0;
200 peerService = peerUnitServiceName interfaceName peer.publicKey dynamicRefreshEnabled;
201 in
202 nameValuePair "wireguard-${interfaceName}-endpoints-updater-${keyToUnitName peer.publicKey}"
203 {
204 description = "WireGuard ${interfaceName} Endpoints Updater - ${peer.publicKey}";
205 requires = [ "${peerService}.service" ];
206 after = [ "${peerService}.service" ];
207 wantedBy = [ "${peerService}.service" ];
208 path = with pkgs; [ wireguard-tools ];
209
210 unitConfig = {
211 StartLimitIntervalSec = 0;
212 };
213 serviceConfig = mkMerge [
214 peerUpdateSecurity
215 {
216 Type = "simple";
217 IPAddressAllow = [ peer.endpointsUpdater.addr ];
218 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" ];
219 Restart = "on-failure";
220 }
221 (mkIf (interfaceCfg.interfaceNamespace != null)
222 { NetworkNamespacePath = "/var/run/netns/${interfaceCfg.interfaceNamespace}"; })
223 ];
224
225 # Query the peer announcing other peers
226 # for setting the current endpoint of all configured peers
227 # (but the announcing peer).
228 # Note that socat is used instead of libressl's netcat
229 # (which would require MemoryDenyWriteExecute=true to load libtls.so)
230 # or netcat-gnu (which does not work on ARM and has last been released in 2004).
231 script = ''
232 wait="${toString peer.endpointsUpdater.refreshSeconds}"
233 declare -A configured_keys
234 configured_keys=(${concatMapStringsSep " " (p:
235 optionalString (p.publicKey != peer.publicKey) "[${p.publicKey}]=set")
236 interfaceCfg.peers})
237
238 while true; do
239 # Set stdin to socat's stdout
240 exec < <(exec ${pkgs.socat}/bin/socat STDOUT \
241 "TCP:${with peer.endpointsUpdater; addr+":"+toString port}")
242
243 # Update the endpoint of each configured peer
244 while read -t "$wait" -n 128 -r public_key endpoint x; do
245 if [ "$endpoint" != "(none)" -a "''${configured_keys[$public_key]}" ]; then
246 wg set "${interfaceName}" peer "$public_key" endpoint "$endpoint"
247 fi;
248 done
249
250 sleep "$wait"
251 done
252 '';
253 };
254
255 all_peers = flatten
256 (mapAttrsToList
257 (interfaceName: interfaceCfg:
258 map (peer: { inherit interfaceName interfaceCfg peer; }) interfaceCfg.peers
259 )
260 cfg.interfaces);
261 in
262 {
263 options.networking.wireguard = {
264 interfaces = mkOption {
265 type = with types; attrsOf (submodule interfaceOpts);
266 };
267 };
268 config = {
269 systemd.sockets =
270 mapAttrs' generatePeersAnnouncingSocket cfg.interfaces;
271
272 systemd.services =
273 (mapAttrs' generatePeersAnnouncingUnit cfg.interfaces)
274 // (listToAttrs (map generatePeerUnit all_peers))
275 // (listToAttrs (map generateEndpointsUpdaterUnit
276 (filter ({ peer, ... }: peer.endpointsUpdater.enable) all_peers)));
277 };
278 }