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}