{ ... }: { flake.nixosModules.systemTailscale = { config, lib, ... }: let cfg = config.chiasson.system.networking.tailscale; tsCfg = config.services.tailscale; in { options.chiasson.system.networking.tailscale = { enable = lib.mkEnableOption '' Tailscale mesh VPN. Joins this host to your tailnet for encrypted access from anywhere without opening inbound ports on your router. ''; authKeyFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; example = "/run/secrets/tailscale/auth-key"; description = '' Reusable Tailscale auth key (sops path). Create one at https://login.tailscale.com/admin/settings/keys — enable Reusable and Pre-approved. Store under `tailscale/auth-key` in secrets/secrets.yaml. ''; }; openFirewall = lib.mkOption { type = lib.types.bool; default = true; description = "Allow Tailscale UDP 41641 through the host firewall."; }; acceptRoutes = lib.mkOption { type = lib.types.bool; default = false; description = '' Accept subnet routes advertised by other tailnet nodes (e.g. home LAN via nix-server). Enable on portable clients like t2mbp. ''; }; subnetRouter = { enable = lib.mkEnableOption '' Advertise the home LAN through this node so other tailnet devices can reach local IPs (SSH, Attic, Gitea, Jellyfin, etc.). Enable on an always-on home host (nix-server). Approve the route in the Tailscale admin console after first connect. ''; routes = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "192.168.2.0/24" ]; example = [ "192.168.2.0/24" ]; description = "Subnets to advertise to the tailnet."; }; }; }; config = lib.mkIf cfg.enable (lib.mkMerge [ { services.tailscale = { enable = true; openFirewall = cfg.openFirewall; authKeyFile = cfg.authKeyFile; }; networking.firewall.trustedInterfaces = lib.mkAfter [ "tailscale0" ]; } (lib.mkIf cfg.acceptRoutes { services.tailscale.useRoutingFeatures = "client"; services.tailscale.extraUpFlags = [ "--accept-routes" ]; }) (lib.mkIf cfg.subnetRouter.enable { services.tailscale.useRoutingFeatures = "both"; services.tailscale.extraUpFlags = map (route: "--advertise-routes=${route}") cfg.subnetRouter.routes; }) (lib.mkIf (cfg.authKeyFile != null) { # Upstream uses `$(cat $authKeyFile)` which breaks when sops leaves a trailing newline. systemd.services.tailscaled-autoconnect.script = lib.mkOverride 50 '' getState() { tailscale status --json --peers=false | jq -r '.BackendState' } lastState="" while state="$(getState)"; do if [[ "$state" != "$lastState" ]]; then case "$state" in NeedsLogin|NeedsMachineAuth|Stopped) echo "Server needs authentication, sending auth key" tailscale up --auth-key "$(tr -d '\n\r' < ${cfg.authKeyFile})" ${lib.escapeShellArgs tsCfg.extraUpFlags} ;; Running) echo "Tailscale is running" systemd-notify --ready exit 0 ;; *) echo "Waiting for Tailscale State = Running or systemd timeout" ;; esac echo "State = $state" fi lastState="$state" sleep .5 done ''; }) ]); }; }