# Dynamic DNS: ddclient keeps the provider's records on the uplink's addresses. # It runs from a timer; a watcher starts it as soon as an address of the uplink # changes, so a new PPPoE session or lease is published within seconds. { config, lib, pkgs, ... }: let cfg = config.router; ddns = cfg.ddns; # Where the addresses are: IPv4 on the PPP device, IPv6 on the physical uplink # (or the PPP device when IPv6 runs over it). dev4 = cfg.wan.ipv4.interface; dev6 = if cfg.wan.ipv6.enable && cfg.wan.ipv6.transport == "ppp" then dev4 else cfg.wan.interface; devices = lib.unique (lib.optional ddns.ipv4 dev4 ++ lib.optional ddns.ipv6 dev6); watch = pkgs.writeShellApplication { name = "router-ddns-watch"; runtimeInputs = [ pkgs.iproute2 pkgs.gnugrep pkgs.systemd ]; text = '' # `ip monitor` also reports the lines of devices that do not exist yet. ip -o monitor address \ | grep --line-buffered ${lib.concatMapStringsSep " " (d: "-e ' ${d} '") devices} \ | while read -r _; do sleep 5 # an address change comes as several events systemctl start --no-block ddclient.service done ''; }; in { config = lib.mkIf (cfg.enable && ddns.enable) { assertions = [ { assertion = ddns.domains != [ ]; message = "router.ddns: no domains to update."; } { assertion = ddns.ipv4 || ddns.ipv6; message = "router.ddns: neither IPv4 nor IPv6 is enabled."; } { assertion = ddns.passwordFile != null && !lib.hasPrefix "/nix/store" ddns.passwordFile; message = "router.ddns.passwordFile must name a file outside the Nix store (the secret must not be copied into it)."; } ]; services.ddclient = { enable = true; inherit (ddns) protocol username passwordFile domains ssl interval ; server = lib.optionalString (ddns.server != null) ddns.server; zone = lib.optionalString (ddns.zone != null) ddns.zone; usev4 = lib.optionalString ddns.ipv4 "ifv4, ifv4=${dev4}"; usev6 = lib.optionalString ddns.ipv6 "ifv6, ifv6=${dev6}"; # ddclient only sends a changed address; its default of 30 s between # updates would hold back a new one that follows closely on another # (a PPP session that drops and comes back). extraConfig = "min-interval=1s"; }; systemd.services.router-ddns-watch = { description = "Update dynamic DNS when the uplink's address changes"; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = lib.getExe watch; Restart = "always"; RestartSec = 5; }; }; }; }