main
1{
2 den,
3 lib,
4 ...
5}: {
6 den.aspects.services.headscale = {
7 includes = [
8 den.aspects.services.headscale.dns
9 den.aspects.services.headscale.policy
10 ];
11
12 settings.host = {
13 domain = lib.mkOption {
14 type = lib.types.nullOr lib.types.str;
15 default = null;
16 };
17 port = lib.mkOption {
18 type = lib.types.port;
19 default = 8080;
20 };
21 dns = {
22 enable = lib.mkEnableOption "Enable Headscale's Magic DNS";
23 domain = lib.mkOption {
24 type = lib.types.str;
25 default = "ts.net";
26 };
27 };
28 };
29
30 persist = {
31 directories = [
32 {
33 directory = "/var/lib/headscale";
34 user = "headscale";
35 group = "headscale";
36 mode = "0700";
37 }
38 ];
39 };
40
41 reverseProxy = {host, ...}: let
42 cfg = host.settings.services.headscale;
43 in {
44 ${cfg.domain} = {
45 port = cfg.port;
46 };
47 };
48
49 nixos = {
50 host,
51 pkgs,
52 config,
53 reverseProxy,
54 ...
55 }: let
56 inherit
57 (import ./_lib.nix {inherit lib;})
58 configFilePath
59 dnsRecordsFilePath
60 mkDnsRecords
61 ;
62
63 cfg = host.settings.services.headscale;
64
65 tailnetDomain = den.aspects.services.caddy.tailnetDomain;
66
67 dnsRecords = mkDnsRecords {
68 inherit reverseProxy tailnetDomain;
69 dnsDomain = cfg.dns.domain;
70 };
71
72 nixDnsRecords =
73 pkgs.writeText "headscale-dns-records.json"
74 (builtins.toJSON dnsRecords);
75
76 mergeHeadscaleState = pkgs.writeShellScript "merge-headscale-state" ''
77 set -euo pipefail
78
79 # Merge main configuration
80 if [ -f "${configFilePath}" ]; then
81 ${lib.getExe pkgs.yq-go} eval-all '. as $item ireduce ({}; . * $item)' \
82 "${configFilePath}" \
83 "${config.services.headscale.configFile}" \
84 > "${configFilePath}.tmp" \
85 && mv "${configFilePath}.tmp" "${configFilePath}"
86 else
87 cp "${config.services.headscale.configFile}" "${configFilePath}"
88 chmod 0640 "${configFilePath}"
89 fi
90
91 # Merge DNS extra records
92 if [ ! -f "${dnsRecordsFilePath}" ]; then
93 cp "${nixDnsRecords}" "${dnsRecordsFilePath}"
94 chmod 0640 "${dnsRecordsFilePath}"
95 fi
96
97 ${lib.getExe pkgs.yq-go} eval-all '. as $item ireduce ([]; . + $item) | unique_by(.name + "|" + .type)' \
98 "${nixDnsRecords}" \
99 "${config.vaultix.templates.headscale-direct-records.path}" \
100 "${dnsRecordsFilePath}" \
101 > "${dnsRecordsFilePath}.tmp" \
102 && mv "${dnsRecordsFilePath}.tmp" "${dnsRecordsFilePath}"
103 '';
104 in {
105 services.headscale = {
106 enable = true;
107 address = "127.0.0.1";
108 port = cfg.port;
109 settings = {
110 server_url = "https://${cfg.domain}";
111 database.type = "sqlite";
112 tls_cert_path = null; # Use webserver for TLS instead.
113 tls_key_path = null;
114 prefixes = {
115 v4 = "100.64.0.0/10";
116 v6 = "fd7a:115c:a1e0::/48";
117 allocation = "random";
118 };
119 };
120 };
121
122 systemd.services.headscale = let
123 hsCfg = config.services.headscale;
124 in {
125 serviceConfig = {
126 ExecStartPre = [mergeHeadscaleState];
127 };
128
129 script = lib.mkForce ''
130 ${lib.optionalString (hsCfg.settings.database.postgres.password_file != null) ''
131 export HEADSCALE_DATABASE_POSTGRES_PASS="$(head -n1 ${lib.escapeShellArg hsCfg.settings.database.postgres.password_file})"
132 ''}
133 exec ${lib.getExe hsCfg.package} serve --config ${configFilePath}
134 '';
135 };
136 };
137 };
138}