1{ pkgs, lib, ... }:
2let
3 tls-cert = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
4 openssl req \
5 -x509 -newkey rsa:4096 -sha256 -days 365 \
6 -nodes -out cert.pem -keyout key.pem \
7 -subj '/CN=headscale' -addext "subjectAltName=DNS:headscale"
8
9 mkdir -p $out
10 cp key.pem cert.pem $out
11 '';
12in
13{
14 name = "headscale";
15 meta.maintainers = with lib.maintainers; [
16 kradalby
17 misterio77
18 ];
19
20 nodes =
21 let
22 headscalePort = 8080;
23 stunPort = 3478;
24 peer = {
25 services.tailscale.enable = true;
26 security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
27 };
28 in
29 {
30 peer1 = peer;
31 peer2 = peer;
32
33 headscale = {
34 services = {
35 headscale = {
36 enable = true;
37 port = headscalePort;
38 settings = {
39 server_url = "https://headscale";
40 ip_prefixes = [ "100.64.0.0/10" ];
41 derp.server = {
42 enabled = true;
43 region_id = 999;
44 stun_listen_addr = "0.0.0.0:${toString stunPort}";
45 };
46 dns = {
47 base_domain = "tailnet";
48 extra_records = [
49 {
50 name = "foo.bar";
51 type = "A";
52 value = "100.64.0.2";
53 }
54 ];
55 override_local_dns = false;
56 };
57 };
58 };
59 nginx = {
60 enable = true;
61 virtualHosts.headscale = {
62 addSSL = true;
63 sslCertificate = "${tls-cert}/cert.pem";
64 sslCertificateKey = "${tls-cert}/key.pem";
65 locations."/" = {
66 proxyPass = "http://127.0.0.1:${toString headscalePort}";
67 proxyWebsockets = true;
68 };
69 };
70 };
71 };
72 networking.firewall = {
73 allowedTCPPorts = [
74 80
75 443
76 ];
77 allowedUDPPorts = [ stunPort ];
78 };
79 environment.systemPackages = [ pkgs.headscale ];
80 };
81 };
82
83 testScript = ''
84 start_all()
85 headscale.wait_for_unit("headscale")
86 headscale.wait_for_open_port(443)
87
88 # Create headscale user and preauth-key
89 headscale.succeed("headscale users create test")
90 authkey = headscale.succeed("headscale preauthkeys -u 1 create --reusable")
91
92 # Connect peers
93 up_cmd = f"tailscale up --login-server 'https://headscale' --auth-key {authkey}"
94 peer1.execute(up_cmd)
95 peer2.execute(up_cmd)
96
97 # Check that they are reachable from the tailnet
98 peer1.wait_until_succeeds("tailscale ping peer2")
99 peer2.wait_until_succeeds("tailscale ping peer1.tailnet")
100 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"
101 '';
102}