From 357cf46c8d753d08b5e9c837640aa43557b3675f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20Hamb=C3=BCchen?= Date: Fri, 30 Apr 2021 23:00:29 +0200 Subject: [PATCH] wireguard module: Add `dynamicEndpointRefreshSeconds` option. See for an intro: https://wiki.archlinux.org/index.php/WireGuard#Endpoint_with_changing_IP --- .../modules/services/networking/wireguard.nix | 73 +++++++++++++++++-- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix index 3e097063ae28..6d4d8d617d25 100644 --- a/nixos/modules/services/networking/wireguard.nix +++ b/nixos/modules/services/networking/wireguard.nix @@ -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 wg 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 + + to refresh the IPs periodically. + ''; + }; + + dynamicEndpointRefreshSeconds = mkOption { + default = 0; + example = 5; + type = with types; int; + description = '' + Periodically re-execute the wg utility every + this many seconds in order to let WireGuard notice DNS / hostname + changes. + + Setting this to 0 disables periodic reexecution. + ''; }; persistentKeepalive = mkOption { @@ -256,12 +281,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 @@ -270,7 +301,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" ]; @@ -280,10 +316,21 @@ 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 = concatStringsSep " " ( @@ -302,6 +349,16 @@ let 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