{ ... }: { _class = "clan.service"; manifest.name = "headscale"; manifest.description = "An open source, self-hosted implementation of the Tailscale control server"; manifest.readme = "An open source, self-hosted implementation of the Tailscale control server"; manifest.categories = [ "System" ]; roles.server = { description = "A server role"; interface = { lib, config, ... }: { options = { public_url = lib.mkOption { type = with lib.types; nullOr str; default = config.services.headscale.settings.server_url; description = "Public URL for accessing the instance"; }; base_domain = lib.mkOption { type = with lib.types; str; default = ""; description = "Defines the base domain to create the hostnames for MagicDNS in Headscale. `base_domain` must be a FQDN, without the trailing dot. The FQDN of the hosts will be `hostname.base_domain (e.g. myhost.tailnet.example.com)"; }; advertise_routes = lib.mkOption { type = with lib.types; listOf str; default = [ ]; description = "Expose physical subnet routes to your entire Tailscale network."; example = [ "192.168.1.0/24" ]; }; nameservers = lib.mkOption { type = with lib.types; listOf str; default = [ "1.1.1.1" "8.8.8.8" ]; description = "List of nameservers to pass to Tailscale clients"; example = [ "10.0.10.1" ]; }; }; }; perInstance = { settings, ... }: { nixosModule = { config, pkgs, lib, ... }: let preAuthKeyFile = "/var/lib/headscale/preauth.key"; routes = lib.concatStringsSep "," settings.advertise_routes; in { systemd.services.headscale-auto-enroll = let serverUser = "hcserver"; in { description = "Enroll this machine into headscale automatically"; after = [ "headscale.service" "tailscaled.service" ]; requires = [ "headscale.service" "tailscaled.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; User = "root"; }; path = [ pkgs.jq ]; script = '' set -euo pipefail if ${pkgs.tailscale}/bin/tailscale status &>/dev/null; then echo "Already enrolled, skipping." exit 0 fi for i in $(seq 1 30); do ${pkgs.headscale}/bin/headscale users list &>/dev/null && break sleep 1 done ${pkgs.headscale}/bin/headscale users create ${serverUser} 2>/dev/null || true USER_ID=$(${pkgs.headscale}/bin/headscale users list --name ${serverUser} -o json | jq '.[0].id') KEY=$(${pkgs.headscale}/bin/headscale preauthkeys create \ --user $USER_ID \ --reusable \ --expiration 30m \ --output json | ${pkgs.jq}/bin/jq -r '.key') echo "$KEY" > ${preAuthKeyFile} chmod 600 ${preAuthKeyFile} ${pkgs.tailscale}/bin/tailscale up \ --login-server=https://${settings.public_url} \ --authkey="$KEY" \ --accept-routes \ --advertise-routes=${routes} ''; }; systemd.services.headscale-approve-routes = { description = "Auto approve routes"; after = [ "headscale.service" "tailscaled.service" "headscale-auto-enroll.service" ]; requires = [ "headscale.service" "tailscaled.service" "headscale-auto-enroll.service" ]; path = [ pkgs.jq ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; User = "root"; }; script = '' set -euo pipefail NODE_ID=$(${pkgs.tailscale}/bin/tailscale status --json | jq '.Self.ID' | tr -d '"') ${pkgs.headscale}/bin/headscale node approve-routes --identifier $NODE_ID --routes ${routes} ''; }; systemd.services.tailscaled-autoconnect.after = [ "tailscaled.service" "headscale-auto-enroll.service" ]; services.tailscale = { enable = true; useRoutingFeatures = "server"; openFirewall = true; }; networking.firewall.allowedTCPPorts = [ config.services.headscale.port ]; services.headscale = { enable = true; address = "0.0.0.0"; settings.server_url = "https://${settings.public_url}"; settings.dns = { base_domain = settings.base_domain; override_local_dns = true; nameservers.global = settings.nameservers; magic_dns = false; }; }; }; }; }; }