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}