nix: run the headscale VM test on the test kit

Drops nginx, the cert and dead ip_prefixes. Proves DERP relay and TLS noise
after a restart, and puts nix/module.nix under CI.
This commit is contained in:
Kristoffer Dalby
2026-09-28 13:37:07 +00:00
committed by Kristoffer Dalby
parent 200c2c01e8
commit 00663fbbfe
2 changed files with 60 additions and 93 deletions
+8 -6
View File
@@ -296,12 +296,14 @@
} }
); );
checks = { # Gated to Linux: parts of the tree are Linux-specific, and the VM
headscale = pkgs.testers.nixosTest (import ./nix/tests/headscale.nix); # test needs KVM.
} checks = pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux (
# The Go build/test checks are gated to Linux: parts of the tree are goChecks
# Linux-specific and the pure unit subset is validated by CI. // {
// pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux goChecks; headscale = pkgs.testers.runNixOSTest (import ./nix/tests/headscale.nix self);
}
);
} }
); );
} }
+52 -87
View File
@@ -1,14 +1,18 @@
{ pkgs, lib, ... }: # The test kit's contract test, and CI's coverage of nix/module.nix.
self:
{ lib, pkgs, ... }:
let let
tls-cert = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } '' policy = pkgs.writeText "policy.json" (
openssl req \ builtins.toJSON {
-x509 -newkey rsa:4096 -sha256 -days 365 \ acls = [
-nodes -out cert.pem -keyout key.pem \ {
-subj '/CN=headscale' -addext "subjectAltName=DNS:headscale" action = "accept";
src = [ "*" ];
mkdir -p $out dst = [ "*:*" ];
cp key.pem cert.pem $out }
''; ];
}
);
in in
{ {
name = "headscale"; name = "headscale";
@@ -17,89 +21,50 @@ in
misterio77 misterio77
]; ];
nodes = nodes = {
let headscale = {
headscalePort = 8080; imports = [ self.nixosModules.testkit ];
stunPort = 3478; services.headscale.settings.dns.extra_records = [
peer = { {
services.tailscale.enable = true; name = "foo.bar";
security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ]; type = "A";
}; value = "100.64.0.2";
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 ];
};
}; };
# 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 = '' testScript = ''
from datetime import timedelta
start_all() start_all()
headscale.wait_for_unit("headscale") key = headscale.succeed("hs-authkey test").strip()
headscale.wait_for_open_port(443) for peer in [peer1, peer2]:
peer.succeed(f"hs-join {key}")
# Create headscale user and preauth-key peer1.wait_until_succeeds("tailscale ping --until-direct=false -c 1 peer2 | grep -F 'via DERP(headscale)'")
headscale.succeed("headscale users create test") peer2.wait_until_succeeds("tailscale ping --until-direct=false -c 1 peer1.tailnet")
authkey = headscale.succeed("headscale preauthkeys -u 1 create --reusable") res = peer1.wait_until_succeeds("${lib.getExe pkgs.dig} +short foo.bar").strip()
assert res == "100.64.0.2", res
# Connect peers headscale.succeed("headscale policy set -f ${policy}")
up_cmd = f"tailscale up --login-server 'https://headscale' --auth-key {authkey}"
peer1.execute(up_cmd)
peer2.execute(up_cmd)
# Check that they are reachable from the tailnet # Clients reconnect after a control restart, peer1 over the TLS listener.
peer1.wait_until_succeeds("tailscale ping peer2") headscale.systemctl("restart headscale.service")
peer2.wait_until_succeeds("tailscale ping peer1.tailnet") headscale.wait_until_succeeds(
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" "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")
''; '';
} }