# Dynamic DNS: the router tells its provider about the uplink's address, at # boot and as soon as the address changes (the timer is set to an hour, so # only the watcher can explain the second update). # # upstream (ISP: DHCP, a dyndns2 update server) -- router { self, pkgs }: let common = import ./common.nix; in pkgs.testers.runNixOSTest { name = "router-ddns"; defaults.imports = [ common.tools ]; interactive.defaults = common.interactiveDefaults; nodes = { upstream = { lib, ... }: { virtualisation.vlans = [ 1 ]; networking.useNetworkd = true; networking.useDHCP = false; networking.firewall.enable = false; networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ]; systemd.network.networks."10-wan" = { matchConfig.Name = "eth1"; address = [ "203.0.113.1/24" ]; }; services.dnsmasq = { enable = true; settings = { interface = "eth1"; bind-interfaces = true; dhcp-range = "203.0.113.100,203.0.113.100,255.255.255.0,1h"; dhcp-option = [ "option:router,203.0.113.1" ]; }; }; # What a dyndns2 provider answers; the access log is the test's record. services.nginx = { enable = true; virtualHosts.default = { default = true; locations."/nic/update".return = ''200 "good $arg_myip"''; }; }; }; router = { lib, ... }: { imports = [ self.nixosModules.router ]; virtualisation.vlans = [ 1 2 ]; networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ]; networking.interfaces.eth2.ipv4.addresses = lib.mkForce [ ]; router = { enable = true; wan.interface = "eth1"; lans.main = { interface = "eth2"; ipv4.address = "192.168.10.1"; dhcp.pool = { start = "192.168.10.100"; end = "192.168.10.199"; }; }; dnsProviders.provider = { type = "dyndns2"; server = "203.0.113.1"; username = "router"; secretFile = "/run/ddns-secret"; }; ddns = { enable = true; provider = "provider"; ssl = false; domains = [ "home.example.test" "vpn.example.test" ]; interval = "1h"; }; }; # The secret is a file on the router, as the web UI leaves it. systemd.tmpfiles.rules = [ "f+ /run/ddns-secret 0600 root root - hunter2" ]; }; }; testScript = '' start_all() upstream.wait_for_unit("nginx.service") router.wait_for_unit("network-online.target") router.wait_until_succeeds("ip -4 addr show dev eth1 | grep -q 203.0.113.100") router.wait_for_unit("router-ddns-watch.service") def updated(addr): return f"grep -q 'myip={addr}' /var/log/nginx/access.log" with subtest("the first address is published, for every name"): upstream.wait_until_succeeds(updated("203.0.113.100"), timeout=90) # One request for all names or one each, depending on the provider's answer. for name in ("home.example.test", "vpn.example.test"): upstream.wait_until_succeeds(f"grep -q '{name}.*myip=203.0.113.100' /var/log/nginx/access.log", timeout=30) # The login arrives as basic auth, not in the URL, and the secret stays off the command line. router.fail("grep -r hunter2 /etc/ddclient* /nix/store/*ddclient.conf 2>/dev/null") with subtest("a new address is published at once, not at the next timer"): router.succeed("ip addr add 203.0.113.101/24 dev eth1") router.succeed("ip addr del 203.0.113.100/24 dev eth1") upstream.wait_until_succeeds(updated("203.0.113.101"), timeout=60) ''; }