]> Git — Sourcephile - sourcephile-nix.git/blob - nixos/modules/services/networking/wireguard.nix
nix: update to nixos-24.11
[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"
125 "~@chown"
126 "~@keyring"
127 "~@privileged"
128 "~@memlock"
129 "~@resources"
130 "~@setuid"
131 ];
132 RestrictRealtime = true;
133 LockPersonality = true;
134 MemoryDenyWriteExecute = true;
135 UMask = 0077;
136 ProtectHostname = true;
137 ProcSubset = "pid";
138 };
139
140 generatePeersAnnouncingSocket = name: values:
141 nameValuePair "wireguard-${name}-peers-announcing"
142 {
143 enable = values.peersAnnouncing.enable;
144 listenStreams = [ (toString values.peersAnnouncing.listenPort) ];
145 socketConfig.Accept = true;
146 # Basic firewalling restricting answers to peers
147 # querying an internal IP address of the announcing peer.
148 # Note that IPv4 addresses can be spoofed using other interfaces unless
149 # sysctl net.ipv4.conf.${name}.rp_filter=1
150 socketConfig.BindToDevice = name;
151 socketConfig.IPAddressAllow = map (peer: peer.allowedIPs) values.peers;
152 socketConfig.IPAddressDeny = "any";
153 socketConfig.MaxConnectionsPerSource = 1;
154 socketConfig.ReusePort = true;
155 wantedBy = [ "sockets.target" ];
156 };
157
158 generatePeersAnnouncingUnit = name: values:
159 nameValuePair "wireguard-${name}-peers-announcing@"
160 {
161 description = "WireGuard Peers Announcing - ${name}";
162 requires = [ "wireguard-${name}.service" ];
163 after = [ "wireguard-${name}.service" ];
164
165 serviceConfig = mkMerge [
166 peerUpdateSecurity
167 {
168 Type = "simple";
169 ExecStart = "${pkgs.wireguard-tools}/bin/wg show '${name}' endpoints";
170 StandardInput = "null";
171 StandardOutput = "socket";
172 RestrictAddressFamilies = "";
173 }
174 (mkIf (values.interfaceNamespace != null)
175 { NetworkNamespacePath = "/var/run/netns/${values.interfaceNamespace}"; })
176 ];
177 };
178
179 generateEndpointsUpdaterUnit = { interfaceName, interfaceCfg, peer }:
180 let
181 dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0;
182 peerService = peerUnitServiceName interfaceName peer.publicKey dynamicRefreshEnabled;
183 in
184 nameValuePair "wireguard-${interfaceName}-endpoints-updater-${keyToUnitName peer.publicKey}"
185 {
186 description = "WireGuard ${interfaceName} Endpoints Updater - ${peer.publicKey}";
187 requires = [ "${peerService}.service" ];
188 after = [ "${peerService}.service" ];
189 wantedBy = [ "${peerService}.service" ];
190 path = with pkgs; [ wireguard-tools ];
191
192 unitConfig = {
193 StartLimitIntervalSec = 0;
194 };
195 serviceConfig = mkMerge [
196 peerUpdateSecurity
197 {
198 Type = "simple";
199 IPAddressAllow = [ peer.endpointsUpdater.addr ];
200 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" ];
201 Restart = "on-failure";
202 }
203 (mkIf (interfaceCfg.interfaceNamespace != null)
204 { NetworkNamespacePath = "/var/run/netns/${interfaceCfg.interfaceNamespace}"; })
205 ];
206
207 # Query the peer announcing other peers
208 # for setting the current endpoint of all configured peers
209 # (but the announcing peer).
210 # Note that socat is used instead of libressl's netcat
211 # (which would require MemoryDenyWriteExecute=true to load libtls.so)
212 # or netcat-gnu (which does not work on ARM and has last been released in 2004).
213 script = ''
214 wait="${toString peer.endpointsUpdater.refreshSeconds}"
215 declare -A configured_keys
216 configured_keys=(${concatMapStringsSep " " (p:
217 optionalString (p.publicKey != peer.publicKey) "[${p.publicKey}]=set")
218 interfaceCfg.peers})
219
220 while true; do
221 # Set stdin to socat's stdout
222 exec < <(exec ${pkgs.socat}/bin/socat STDOUT \
223 "TCP:${with peer.endpointsUpdater; addr+":"+toString port}")
224
225 # Update the endpoint of each configured peer
226 while read -t "$wait" -n 128 -r public_key endpoint x; do
227 if [ "$endpoint" != "(none)" -a "''${configured_keys[$public_key]}" ]; then
228 wg set "${interfaceName}" peer "$public_key" endpoint "$endpoint"
229 fi;
230 done
231
232 sleep "$wait"
233 done
234 '';
235 };
236
237 in
238
239 {
240
241 options.networking.wireguard.interfaces = mkOption {
242 type = with types; attrsOf (submodule interfaceOpts);
243 };
244
245 config = mkIf cfg.enable (
246 let
247 all_peers = flatten
248 (mapAttrsToList
249 (interfaceName: interfaceCfg:
250 map (peer: { inherit interfaceName interfaceCfg peer; }) interfaceCfg.peers
251 )
252 cfg.interfaces);
253 in
254 {
255
256 systemd.sockets =
257 mapAttrs' generatePeersAnnouncingSocket cfg.interfaces;
258
259 systemd.services =
260 mapAttrs' generatePeersAnnouncingUnit cfg.interfaces //
261 (listToAttrs (map generateEndpointsUpdaterUnit
262 (filter ({ peer, ... }: peer.endpointsUpdater.enable) all_peers)));
263
264 }
265 );
266
267 }