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}