Merge pull request #121331 from nh2/wireguard-dynamicEndpointRefreshSeconds

nixos/wireguard: Add `dynamicEndpointRefreshSeconds` option
master
Niklas Hambüchen 2021-05-24 21:49:05 +02:00 committed by GitHub
commit 83a8acc392
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 94 additions and 24 deletions

View File

@ -395,6 +395,15 @@
which is the new stable release. OpenAFS 1.6 was removed.
</para>
</listitem>
<listitem>
<para>
The WireGuard module gained a new option
<option>networking.wireguard.interfaces.&lt;name&gt;.peers.*.dynamicEndpointRefreshSeconds</option>
that implements refreshing the IP of DNS-based endpoints periodically
(which WireGuard itself
<link xlink:href="https://lists.zx2c4.com/pipermail/wireguard/2017-November/002028.html">cannot do</link>).
</para>
</listitem>
<listitem>
<para>
MariaDB has been updated to 10.5.

View File

@ -198,7 +198,32 @@ let
example = "demo.wireguard.io:12913";
type = with types; nullOr str;
description = ''Endpoint IP or hostname of the peer, followed by a colon,
and then a port number of the peer.'';
and then a port number of the peer.
Warning for endpoints with changing IPs:
The WireGuard kernel side cannot perform DNS resolution.
Thus DNS resolution is done once by the <literal>wg</literal> userspace
utility, when setting up WireGuard. Consequently, if the IP address
behind the name changes, WireGuard will not notice.
This is especially common for dynamic-DNS setups, but also applies to
any other DNS-based setup.
If you do not use IP endpoints, you likely want to set
<option>networking.wireguard.dynamicEndpointRefreshSeconds</option>
to refresh the IPs periodically.
'';
};
dynamicEndpointRefreshSeconds = mkOption {
default = 0;
example = 5;
type = with types; int;
description = ''
Periodically re-execute the <literal>wg</literal> utility every
this many seconds in order to let WireGuard notice DNS / hostname
changes.
Setting this to <literal>0</literal> disables periodic reexecution.
'';
};
persistentKeepalive = mkOption {
@ -259,12 +284,18 @@ let
'';
};
generatePeerUnit = { interfaceName, interfaceCfg, peer }:
peerUnitServiceName = interfaceName: publicKey: dynamicRefreshEnabled:
let
keyToUnitName = replaceChars
[ "/" "-" " " "+" "=" ]
[ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ];
unitName = keyToUnitName peer.publicKey;
unitName = keyToUnitName publicKey;
refreshSuffix = optionalString dynamicRefreshEnabled "-refresh";
in
"wireguard-${interfaceName}-peer-${unitName}${refreshSuffix}";
generatePeerUnit = { interfaceName, interfaceCfg, peer }:
let
psk =
if peer.presharedKey != null
then pkgs.writeText "wg-psk" peer.presharedKey
@ -273,7 +304,12 @@ let
dst = interfaceCfg.interfaceNamespace;
ip = nsWrap "ip" src dst;
wg = nsWrap "wg" src dst;
in nameValuePair "wireguard-${interfaceName}-peer-${unitName}"
dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0;
# We generate a different name (a `-refresh` suffix) when `dynamicEndpointRefreshSeconds`
# to avoid that the same service switches `Type` (`oneshot` vs `simple`),
# with the intent to make scripting more obvious.
serviceName = peerUnitServiceName interfaceName peer.publicKey dynamicRefreshEnabled;
in nameValuePair serviceName
{
description = "WireGuard Peer - ${interfaceName} - ${peer.publicKey}";
requires = [ "wireguard-${interfaceName}.service" ];
@ -283,36 +319,59 @@ let
environment.WG_ENDPOINT_RESOLUTION_RETRIES = "infinity";
path = with pkgs; [ iproute2 wireguard-tools ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
serviceConfig =
if !dynamicRefreshEnabled
then
{
Type = "oneshot";
RemainAfterExit = true;
}
else
{
Type = "simple"; # re-executes 'wg' indefinitely
# Note that `Type = "oneshot"` services with `RemainAfterExit = true`
# cannot be used with systemd timers (see `man systemd.timer`),
# which is why `simple` with a loop is the best choice here.
# It also makes starting and stopping easiest.
};
script = let
wg_setup = "${wg} set ${interfaceName} peer ${peer.publicKey}" +
optionalString (psk != null) " preshared-key ${psk}" +
optionalString (peer.endpoint != null) " endpoint ${peer.endpoint}" +
optionalString (peer.persistentKeepalive != null) " persistent-keepalive ${toString peer.persistentKeepalive}" +
optionalString (peer.allowedIPs != []) " allowed-ips ${concatStringsSep "," peer.allowedIPs}";
wg_setup = concatStringsSep " " (
[ ''${wg} set ${interfaceName} peer "${peer.publicKey}"'' ]
++ optional (psk != null) ''preshared-key "${psk}"''
++ optional (peer.endpoint != null) ''endpoint "${peer.endpoint}"''
++ optional (peer.persistentKeepalive != null) ''persistent-keepalive "${toString peer.persistentKeepalive}"''
++ optional (peer.allowedIPs != []) ''allowed-ips "${concatStringsSep "," peer.allowedIPs}"''
);
route_setup =
optionalString interfaceCfg.allowedIPsAsRoutes
(concatMapStringsSep "\n"
(allowedIP:
"${ip} route replace ${allowedIP} dev ${interfaceName} table ${interfaceCfg.table}"
''${ip} route replace "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"''
) peer.allowedIPs);
in ''
${wg_setup}
${route_setup}
${optionalString (peer.dynamicEndpointRefreshSeconds != 0) ''
# Re-execute 'wg' periodically to notice DNS / hostname changes.
# Note this will not time out on transient DNS failures such as DNS names
# because we have set 'WG_ENDPOINT_RESOLUTION_RETRIES=infinity'.
# Also note that 'wg' limits its maximum retry delay to 20 seconds as of writing.
while ${wg_setup}; do
sleep "${toString peer.dynamicEndpointRefreshSeconds}";
done
''}
'';
postStop = let
route_destroy = optionalString interfaceCfg.allowedIPsAsRoutes
(concatMapStringsSep "\n"
(allowedIP:
"${ip} route delete ${allowedIP} dev ${interfaceName} table ${interfaceCfg.table}"
''${ip} route delete "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"''
) peer.allowedIPs);
in ''
${wg} set ${interfaceName} peer ${peer.publicKey} remove
${wg} set "${interfaceName}" peer "${peer.publicKey}" remove
${route_destroy}
'';
};
@ -348,23 +407,25 @@ let
${values.preSetup}
${ipPreMove} link add dev ${name} type wireguard
${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) "${ipPreMove} link set ${name} netns ${ns}"}
${ipPreMove} link add dev "${name}" type wireguard
${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) ''${ipPreMove} link set "${name}" netns "${ns}"''}
${concatMapStringsSep "\n" (ip:
"${ipPostMove} address add ${ip} dev ${name}"
''${ipPostMove} address add "${ip}" dev "${name}"''
) values.ips}
${wg} set ${name} private-key ${privKey} ${
optionalString (values.listenPort != null) " listen-port ${toString values.listenPort}"}
${concatStringsSep " " (
[ ''${wg} set "${name}" private-key "${privKey}"'' ]
++ optional (values.listenPort != null) ''listen-port "${toString values.listenPort}"''
)}
${ipPostMove} link set up dev ${name}
${ipPostMove} link set up dev "${name}"
${values.postSetup}
'';
postStop = ''
${ipPostMove} link del dev ${name}
${ipPostMove} link del dev "${name}"
${values.postShutdown}
'';
};
@ -374,7 +435,7 @@ let
nsList = filter (ns: ns != null) [ src dst ];
ns = last nsList;
in
if (length nsList > 0 && ns != "init") then "ip netns exec ${ns} ${cmd}" else cmd;
if (length nsList > 0 && ns != "init") then ''ip netns exec "${ns}" "${cmd}"'' else cmd;
in
{