From a01214707a875f390077687f0908423df7f5dcd2 Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Fri, 30 Oct 2026 00:30:11 +0100 Subject: [PATCH] Test dynamic DHCP names in the DNS VM test A client with a hostname is published with its reverse record, a client claiming a configured name does not take it over, and a name outside the domain is cut to its first label. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F --- tests/dns.nix | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/dns.nix b/tests/dns.nix index d1021bb..998a153 100644 --- a/tests/dns.nix +++ b/tests/dns.nix @@ -9,6 +9,18 @@ let common = import ./common.nix; clientMac = "02:00:00:00:00:51"; + + # Only the settings module's services (router-leases-dns) are used here; the + # router itself is configured below. + settings = pkgs.writeText "settings.json" ( + builtins.toJSON { + system = { + hostName = "router"; + stateVersion = "26.11"; + }; + router = { }; + } + ); in pkgs.testers.runNixOSTest { name = "router-dns"; @@ -55,7 +67,11 @@ pkgs.testers.runNixOSTest { router = { lib, ... }: { - imports = [ self.nixosModules.router ]; + imports = [ + self.nixosModules.router + self.nixosModules.settings + ]; + routerSettings.file = settings; virtualisation.vlans = [ 1 2 @@ -84,6 +100,7 @@ pkgs.testers.runNixOSTest { dns = { forwarders = [ "203.0.113.1" ]; domain = "lan.test"; + dynamicLeases = true; hosts = { nas = [ "192.168.10.20" @@ -116,6 +133,23 @@ pkgs.testers.runNixOSTest { matchConfig.Name = "eth1"; linkConfig.MACAddress = "${clientMac}"; networkConfig.DHCP = "ipv4"; + # Claims a name the configuration publishes for someone else. + dhcpV4Config.Hostname = "nas"; + }; + }; + + laptop = + { lib, ... }: + { + virtualisation.vlans = [ 2 ]; + networking.useNetworkd = true; + networking.useDHCP = false; + networking.firewall.enable = false; + networking.interfaces.eth1.ipv4.addresses = lib.mkForce [ ]; + systemd.network.networks."10-lan" = { + matchConfig.Name = "eth1"; + networkConfig.DHCP = "ipv4"; + dhcpV4Config.Hostname = "Laptop.evil.example"; }; }; }; @@ -148,6 +182,23 @@ pkgs.testers.runNixOSTest { assert q("router") == "192.168.10.1", q("router") assert q("1.10.168.192.in-addr.arpa", "PTR") == "router.lan.test." + with subtest("DHCP clients are published under the name they gave"): + laptop.wait_until_succeeds("ip -4 addr show dev eth1 | grep -q 192.168.10.1[0-9][0-9]") + addr = laptop.succeed("ip -4 -o addr show dev eth1").split()[3].split("/")[0] + router.wait_until_succeeds(f"dig +short @192.168.10.1 laptop.lan.test | grep -qx {addr}", timeout=60) + assert q("laptop.lan.test") == addr + rev = ".".join(reversed(addr.split("."))) + ".in-addr.arpa" + assert q(rev, "PTR") == "laptop.lan.test." + + with subtest("a client cannot take over a name the configuration publishes"): + assert q("nas.lan.test") == "192.168.10.20", q("nas.lan.test") + assert q("20.10.168.192.in-addr.arpa", "PTR") == "nas.lan.test." + + with subtest("the status shows what was published and skipped"): + import json + st = json.loads(router.succeed("cat /run/router-leases-dns/status.json")) + router.log(str(st)) + with subtest("the local domain is authoritative: unknown names are not forwarded"): # Positive control: resolving through the router works at all. assert q("other.example.test") == "203.0.113.8"