main
1{lib}: let
2 inherit (import ../direct-dns/_lib.nix {inherit lib;}) mkDirectEntries;
3
4 mkTailnetEntries = reverseProxy: let
5 uniqueByDomain = lib.foldl' (acc: e:
6 if builtins.any (x: x.domain == e.domain) acc
7 then acc
8 else acc ++ [e]) [];
9 in
10 uniqueByDomain (
11 lib.concatMap (
12 r:
13 lib.mapAttrsToList (domain: conf: {
14 inherit domain;
15 source = r.source.host;
16 }) (
17 lib.filterAttrs (_: conf: conf.tailscale or false) r.value
18 )
19 )
20 reverseProxy
21 );
22
23 # Dedupe grants; dst defaults to the emitting host's tailscale addresses.
24 mkPolicyGrants = grants:
25 lib.unique (
26 map (grant: let
27 value = grant.value;
28 host = grant.source.host;
29 ip = value.ip or [];
30 app = value.app or [];
31 src = value.src or [];
32 in
33 assert lib.assertMsg (src != [])
34 "Host '${host.name}' emits a tailscale grant with no sources (src)";
35 assert lib.assertMsg (ip != [] || app != [])
36 "Host '${host.name}' emits a tailscale grant with no capabilities (ip or app)"; let
37 dst =
38 if value.dst or null != null
39 then [value.dst]
40 else let
41 v4 = host.address.ipv4.tailscale;
42 v6 = host.address.ipv6.tailscale;
43 in
44 assert lib.assertMsg (v4 != null || v6 != null)
45 "Host '${host.name}' emits a tailscale grant without a dst, but has no tailscale IPv4 or IPv6 address (see `address` in modules/hosts/schema.nix).";
46 lib.optionals (v4 != null) [v4] ++ lib.optionals (v6 != null) [v6];
47 in
48 {
49 inherit src dst;
50 }
51 // lib.optionalAttrs (ip != []) {inherit ip;}
52 // lib.optionalAttrs (app != []) {inherit app;})
53 grants
54 );
55
56 # Hosts serving tailnet reverse-proxy entries get a grant for caddy's ports.
57 mkCaddyGrants = reverseProxy:
58 lib.unique (
59 map (r: let
60 v4 = r.source.host.address.ipv4.tailscale;
61 v6 = r.source.host.address.ipv6.tailscale;
62 in
63 assert lib.assertMsg (v4 != null || v6 != null)
64 "Host '${r.source.host.name}' serves tailnet domains but has no tailscale IPv4 or IPv6 address (see `address` in modules/hosts/schema.nix)."; {
65 src = ["*"];
66 ip = ["tcp:80" "tcp:443"];
67 dst = lib.optionals (v4 != null) [v4] ++ lib.optionals (v6 != null) [v6];
68 })
69 (lib.filter
70 (r: builtins.any (conf: conf.tailscale or false) (builtins.attrValues r.value))
71 reverseProxy)
72 );
73
74 # Direct-dns "tailscale" hosts are reached through the tailnet like
75 # tailnet-domain sites, so they need the same web-port grants.
76 mkDirectDnsGrants = reverseProxy: let
77 tailscaleHosts = lib.filter (h: h.v4 != null || h.v6 != null) (
78 map (e: {inherit (e) v4 v6;})
79 (lib.filter (e: e.mode == "tailscale") (mkDirectEntries reverseProxy))
80 );
81 in
82 lib.unique (
83 map (h: {
84 src = ["*"];
85 ip = ["tcp:80" "tcp:443"];
86 dst = lib.optionals (h.v4 != null) [h.v4.value] ++ lib.optionals (h.v6 != null) [h.v6.value];
87 })
88 tailscaleHosts
89 );
90
91 mkDnsRecords = {
92 reverseProxy,
93 tailnetDomain,
94 dnsDomain,
95 }: let
96 tailnetEntries = mkTailnetEntries reverseProxy;
97
98 duplicateDomains =
99 lib.filter (domain: builtins.length (lib.filter (e: e.domain == domain) tailnetEntries) > 1)
100 (lib.unique (map (e: e.domain) tailnetEntries));
101
102 assertNoDuplicates =
103 lib.assertMsg (duplicateDomains == [])
104 "Multiple hosts declare the same tailnet domain: ${lib.concatStringsSep ", " duplicateDomains}";
105
106 assertDomainMatches =
107 lib.assertMsg (dnsDomain == tailnetDomain)
108 ("Headscale MagicDNS base domain '${dnsDomain}' does not match the tailnet domain"
109 + "'${tailnetDomain}' used by the caddy tailnet module (hardcoded in modules/services/caddy/quirks.nix).");
110 in
111 assert assertNoDuplicates;
112 assert assertDomainMatches;
113 lib.concatMap (e: let
114 v4 = e.source.address.ipv4.tailscale;
115 v6 = e.source.address.ipv6.tailscale;
116 in
117 assert lib.assertMsg (v4 != null || v6 != null)
118 ("Tailnet domain '${e.domain}' is declared by host '${e.source.name}' which has no tailscale address configured"
119 + "(see `address` in modules/hosts/schema.nix).");
120 lib.optionals (v4 != null) [
121 {
122 name = e.domain;
123 type = "A";
124 value = v4;
125 }
126 ]
127 ++ lib.optionals (v6 != null) [
128 {
129 name = e.domain;
130 type = "AAAA";
131 value = v6;
132 }
133 ])
134 tailnetEntries;
135in {
136 inherit
137 mkCaddyGrants
138 mkDirectDnsGrants
139 mkDirectEntries
140 mkDnsRecords
141 mkPolicyGrants
142 ;
143
144 configFilePath = "/var/lib/headscale/config.yaml";
145 dnsRecordsFilePath = "/var/lib/headscale/dns-records.json";
146}