diff --git a/flake.nix b/flake.nix index f048cc84..4735abd1 100644 --- a/flake.nix +++ b/flake.nix @@ -296,12 +296,14 @@ } ); - checks = { - headscale = pkgs.testers.nixosTest (import ./nix/tests/headscale.nix); - } - # The Go build/test checks are gated to Linux: parts of the tree are - # Linux-specific and the pure unit subset is validated by CI. - // pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux goChecks; + # Gated to Linux: parts of the tree are Linux-specific, and the VM + # test needs KVM. + checks = pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux ( + goChecks + // { + headscale = pkgs.testers.runNixOSTest (import ./nix/tests/headscale.nix self); + } + ); } ); } diff --git a/nix/tests/headscale.nix b/nix/tests/headscale.nix index 819626b7..cc42ba1d 100644 --- a/nix/tests/headscale.nix +++ b/nix/tests/headscale.nix @@ -1,14 +1,18 @@ -{ pkgs, lib, ... }: +# The test kit's contract test, and CI's coverage of nix/module.nix. +self: +{ lib, pkgs, ... }: let - tls-cert = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } '' - openssl req \ - -x509 -newkey rsa:4096 -sha256 -days 365 \ - -nodes -out cert.pem -keyout key.pem \ - -subj '/CN=headscale' -addext "subjectAltName=DNS:headscale" - - mkdir -p $out - cp key.pem cert.pem $out - ''; + policy = pkgs.writeText "policy.json" ( + builtins.toJSON { + acls = [ + { + action = "accept"; + src = [ "*" ]; + dst = [ "*:*" ]; + } + ]; + } + ); in { name = "headscale"; @@ -17,89 +21,50 @@ in misterio77 ]; - nodes = - let - headscalePort = 8080; - stunPort = 3478; - peer = { - services.tailscale.enable = true; - security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ]; - }; - in - { - peer1 = peer; - peer2 = peer; - - headscale = { - services = { - headscale = { - enable = true; - port = headscalePort; - settings = { - server_url = "https://headscale"; - ip_prefixes = [ "100.64.0.0/10" ]; - derp = { - server = { - enabled = true; - region_id = 999; - stun_listen_addr = "0.0.0.0:${toString stunPort}"; - }; - urls = [ ]; - }; - dns = { - base_domain = "tailnet"; - extra_records = [ - { - name = "foo.bar"; - type = "A"; - value = "100.64.0.2"; - } - ]; - override_local_dns = false; - }; - }; - }; - nginx = { - enable = true; - virtualHosts.headscale = { - addSSL = true; - sslCertificate = "${tls-cert}/cert.pem"; - sslCertificateKey = "${tls-cert}/key.pem"; - locations."/" = { - proxyPass = "http://127.0.0.1:${toString headscalePort}"; - proxyWebsockets = true; - }; - }; - }; - }; - networking.firewall = { - allowedTCPPorts = [ - 80 - 443 - ]; - allowedUDPPorts = [ stunPort ]; - }; - environment.systemPackages = [ pkgs.headscale ]; - }; + nodes = { + headscale = { + imports = [ self.nixosModules.testkit ]; + services.headscale.settings.dns.extra_records = [ + { + name = "foo.bar"; + type = "A"; + value = "100.64.0.2"; + } + ]; }; + # All of peer1's noise goes over the kit's TLS listener, not plain :80. + peer1 = { + imports = [ self.nixosModules.testkit-peer ]; + systemd.services.tailscaled.environment.TS_FORCE_NOISE_443 = "1"; + }; + # No direct UDP for peer2: its traffic has to cross the kit's DERP. + peer2 = { + imports = [ self.nixosModules.testkit-peer ]; + systemd.services.tailscaled.environment.TS_DEBUG_ALWAYS_USE_DERP = "1"; + }; + }; testScript = '' + from datetime import timedelta + start_all() - headscale.wait_for_unit("headscale") - headscale.wait_for_open_port(443) + key = headscale.succeed("hs-authkey test").strip() + for peer in [peer1, peer2]: + peer.succeed(f"hs-join {key}") - # Create headscale user and preauth-key - headscale.succeed("headscale users create test") - authkey = headscale.succeed("headscale preauthkeys -u 1 create --reusable") + peer1.wait_until_succeeds("tailscale ping --until-direct=false -c 1 peer2 | grep -F 'via DERP(headscale)'") + peer2.wait_until_succeeds("tailscale ping --until-direct=false -c 1 peer1.tailnet") + res = peer1.wait_until_succeeds("${lib.getExe pkgs.dig} +short foo.bar").strip() + assert res == "100.64.0.2", res - # Connect peers - up_cmd = f"tailscale up --login-server 'https://headscale' --auth-key {authkey}" - peer1.execute(up_cmd) - peer2.execute(up_cmd) + headscale.succeed("headscale policy set -f ${policy}") - # Check that they are reachable from the tailnet - peer1.wait_until_succeeds("tailscale ping peer2") - peer2.wait_until_succeeds("tailscale ping peer1.tailnet") - assert (res := peer1.wait_until_succeeds("${lib.getExe pkgs.dig} +short foo.bar").strip()) == "100.64.0.2", f"Domain {res} did not match 100.64.0.2" + # Clients reconnect after a control restart, peer1 over the TLS listener. + headscale.systemctl("restart headscale.service") + headscale.wait_until_succeeds( + "headscale nodes list -o json | ${lib.getExe pkgs.jq} -e 'length == 2 and all(.[]; .online)'", + timeout=timedelta(minutes=2), + ) + peer1.wait_until_succeeds("tailscale ping --until-direct=false -c 1 peer2") ''; }