main
1{
2 den,
3 lib,
4 ...
5}: {
6 den.aspects.services.headscale = {
7 settings = {
8 domain = lib.mkOption {
9 type = lib.types.nullOr lib.types.str;
10 default = null;
11 };
12 port = lib.mkOption {
13 type = lib.types.port;
14 default = 8080;
15 };
16 derp = {
17 enable = lib.mkEnableOption "Enable Headscale's builtin DERP server";
18 port = lib.mkOption {
19 type = lib.types.port;
20 default = 3478;
21 };
22 };
23 dns = {
24 enable = lib.mkEnableOption "Enable Headscale's Magic DNS";
25 domain = lib.mkOption {
26 type = lib.types.str;
27 default = "ts.net";
28 };
29 };
30 };
31
32 persist = {
33 directories = [
34 {
35 directory = "/var/lib/headscale";
36 user = "headscale";
37 group = "headscale";
38 mode = "0700";
39 }
40 ];
41 };
42
43 reverseProxy = {host, ...}: let
44 cfg = host.settings.services.headscale;
45 in {
46 ${cfg.domain} = {
47 port = cfg.port;
48 };
49 };
50
51 nixos = {
52 host,
53 pkgs,
54 config,
55 reverseProxyTailscale,
56 ...
57 }: let
58 cfg = host.settings.services.headscale;
59 hostAddr = host.address;
60
61 # Publish a Magic DNS record for every host's Tailscale-only reverse proxy,
62 # pointing each domain at the owning host's Tailscale IP.
63 tailscaleRecords =
64 lib.concatMap (
65 r: let
66 srcAddr = r.source.host.address;
67 in
68 lib.concatMap (
69 domain:
70 (lib.optional (srcAddr.ipv4.tailscale != null) {
71 name = domain;
72 type = "A";
73 value = srcAddr.ipv4.tailscale;
74 })
75 ++ (lib.optional (srcAddr.ipv6.tailscale != null) {
76 name = domain;
77 type = "AAAA";
78 value = srcAddr.ipv6.tailscale;
79 })
80 )
81 (builtins.attrNames r.value)
82 )
83 reverseProxyTailscale;
84
85 headscale-wrapped =
86 pkgs.runCommandLocal "headscale-wrapped" {
87 nativeBuildInputs = [pkgs.makeWrapper];
88 } ''
89 mkdir -p $out/bin
90 makeWrapper ${lib.getExe config.services.headscale.package} $out/bin/headscale \
91 --add-flags "--config ${config.services.headscale.configFile}"
92 '';
93 in {
94 environment.systemPackages = [headscale-wrapped];
95 services.headscale = {
96 enable = true;
97 address =
98 if (host.hasAspect den.aspects.services.webserver)
99 then "127.0.0.1"
100 else abort "Not implemented for outside host webserver";
101 port = cfg.port;
102 settings = {
103 server_url = "https://${cfg.domain}";
104 database.type = "sqlite";
105 tls_cert_path = null; # Use webserver for TLS instead.
106 tls_key_path = null;
107 prefixes = {
108 v4 = "100.64.0.0/10";
109 v6 = "fd7a:115c:a1e0::/48";
110 allocation = "random";
111 };
112 derp.server = lib.optionalAttrs (cfg.derp.enable) {
113 enabled = true;
114 stun_listen_addr = "0.0.0.0:${toString cfg.derp.port}";
115 verify_clients = true;
116 region_id = 999;
117 region_code = "headscale";
118 region_name = "Headscale Embedded DERP";
119 ipv4 = lib.mkIf (hostAddr.ipv4.clearText != null) hostAddr.ipv4.clearText;
120 ipv6 = lib.mkIf (hostAddr.ipv6.clearText != null) hostAddr.ipv6.clearText;
121 };
122 dns = {
123 magic_dns = cfg.dns.enable;
124 override_local_dns = cfg.dns.enable;
125 base_domain = cfg.dns.domain;
126 nameservers.global = [
127 # IPv4
128 "119.29.29.29" # DNSPod
129 "223.5.5.5" # AliDNS
130 # IPv6
131 "2400:3200::1" # AliDNS
132 "2606:4700:4700::1111" # Cloudflare
133 ];
134 extra_records = tailscaleRecords;
135 };
136 };
137 };
138
139 networking.firewall.allowedUDPPorts = lib.optional (cfg.derp.enable) 3478;
140
141 systemd.services.headscale = {
142 serviceConfig.EnvironmentFile =
143 lib.mkIf (hostAddr.ipv4.secret.name != null || hostAddr.ipv6.secret.name != null)
144 config.vaultix.templates.headscale-env.path;
145 };
146 vaultix.templates.headscale-env =
147 lib.mkIf (hostAddr.ipv4.secret.name != null || hostAddr.ipv6.secret.name != null)
148 {
149 content = lib.concatLines (
150 (
151 lib.optional
152 (hostAddr.ipv4.secret.name != null)
153 "HEADSCALE_DERP_SERVER_IPV4=${config.vaultix.placeholder.${hostAddr.ipv4.secret.name}}"
154 )
155 ++ (
156 lib.optional
157 (hostAddr.ipv6.secret.name != null)
158 "HEADSCALE_DERP_SERVER_IPV4=${config.vaultix.placeholder.${hostAddr.ipv6.secret.name}}"
159 )
160 );
161 owner = config.services.headscale.user;
162 group = config.services.headscale.group;
163 mode = "0400";
164 };
165 };
166 };
167}