{ config, lib, pkgs, ... }: let cfg = config.services.tailscale; inherit (lib) concatStrings filterAttrs mapAttrsToList mkIf mkOption optional types ; in { options.services.tailscale = { tailnets = mkOption { default = { }; type = types.attrsOf ( types.submodule { options = { loginServer = mkOption { type = types.str; description = "Login server for this tailnet."; }; authKeyFile = mkOption { type = types.nullOr types.str; default = null; description = "Path to auth key secret."; }; enableSSH = mkOption { type = types.bool; default = false; }; acceptDNS = mkOption { type = types.bool; default = true; }; default = mkOption { type = types.bool; default = false; description = "Connect to this tailnet on boot."; }; }; } ); }; }; config = let defaultTailnets = filterAttrs (_: t: t.default) cfg.tailnets; defaultTailnet = if defaultTailnets == { } then null else builtins.head (builtins.attrValues defaultTailnets); entries = mapAttrsToList (name: tcfg: '' TAILNETS["${name}"]="${tcfg.loginServer}|${if tcfg.enableSSH then "true" else "false"}|${ if tcfg.acceptDNS then "true" else "false" }|${if tcfg.authKeyFile != null then tcfg.authKeyFile else ""}" '') cfg.tailnets; tailnetSwitchCli = pkgs.writeShellScriptBin "tailnet-switch" '' set -euo pipefail TAILSCALE="${cfg.package}/bin/tailscale" declare -A TAILNETS ${concatStrings entries} CHOICE="''${1:-}" if [[ -z "$CHOICE" ]]; then if [[ -t 0 ]]; then CHOICE=$(printf '%s\n' "''${!TAILNETS[@]}" | sort | ${pkgs.fzf}/bin/fzf --prompt="Switch tailnet: ") else echo "Usage: tailnet-switch " >&2 printf 'Available tailnets: %s\n' "''${!TAILNETS[@]}" >&2 exit 1 fi fi if [[ -z "''${TAILNETS[$CHOICE]+x}" ]]; then echo "Unknown tailnet: $CHOICE" >&2 printf 'Available tailnets: %s\n' "''${!TAILNETS[@]}" >&2 exit 1 fi IFS='|' read -r LOGIN_SERVER ENABLE_SSH ACCEPT_DNS AUTH_KEY_FILE <<< "''${TAILNETS[$CHOICE]}" echo "Switching to: $CHOICE ($LOGIN_SERVER)" "$TAILSCALE" down --accept-risk=lose-ssh 2>/dev/null || true "$TAILSCALE" logout 2>/dev/null || true UP_FLAGS=("--login-server=$LOGIN_SERVER") [[ "$ENABLE_SSH" == "true" ]] && UP_FLAGS+=("--ssh") [[ "$ACCEPT_DNS" == "true" ]] && UP_FLAGS+=("--accept-dns") [[ -n "$AUTH_KEY_FILE" ]] && UP_FLAGS+=("--auth-key=$(cat "$AUTH_KEY_FILE")") "$TAILSCALE" up "''${UP_FLAGS[@]}" echo "Switched to tailnet: $CHOICE" ''; in mkIf cfg.enable { assertions = [ { assertion = (builtins.length (builtins.attrValues (filterAttrs (_: t: t.default) cfg.tailnets))) <= 1; message = "services.tailscale.tailnets: Only one tailnet can be set as default."; } { assertion = cfg.tailnets != { }; message = "services.tailscale.tailnets: At least one tailnet must be defined."; } ]; services.tailscale = mkIf (defaultTailnet != null) ( with defaultTailnet; { inherit authKeyFile; extraSetFlags = optional enableSSH "--ssh" ++ optional acceptDNS "--accept-dns"; extraUpFlags = [ "--login-server=${loginServer}" ] ++ optional enableSSH "--ssh" ++ optional acceptDNS "--accept-dns"; } ); environment.systemPackages = [ tailnetSwitchCli ]; environment.shellAliases.ts = "${cfg.package}/bin/tailscale"; networking.firewall.trustedInterfaces = [ cfg.interfaceName ]; }; }