From 6420dd0af7677033dccbd4eff8faf630c836abc0 Mon Sep 17 00:00:00 2001 From: sid Date: Sat, 2 May 2026 18:55:56 +0200 Subject: [PATCH] tailscale: add support for multiple tailnets --- modules/nixos/tailscale/default.nix | 157 ++++++++++++++++++++++------ 1 file changed, 125 insertions(+), 32 deletions(-) diff --git a/modules/nixos/tailscale/default.nix b/modules/nixos/tailscale/default.nix index 3e43963..b437849 100644 --- a/modules/nixos/tailscale/default.nix +++ b/modules/nixos/tailscale/default.nix @@ -1,9 +1,17 @@ -{ config, lib, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.services.tailscale; inherit (lib) + concatStrings + filterAttrs + mapAttrsToList mkIf mkOption optional @@ -12,39 +20,124 @@ let in { options.services.tailscale = { - loginServer = mkOption { - type = types.str; - description = "The Tailscale login server to use."; - }; - enableSSH = mkOption { - type = types.bool; - default = false; - description = "Enable Tailscale SSH functionality."; - }; - acceptDNS = mkOption { - type = types.bool; - default = true; - description = "Enable Tailscale's MagicDNS and custom DNS configuration."; + 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 = mkIf cfg.enable { - services.tailscale = { - authKeyFile = config.sops.secrets."tailscale/auth-key".path; - extraSetFlags = optional cfg.enableSSH "--ssh" ++ optional cfg.acceptDNS "--accept-dns"; - extraUpFlags = [ - "--login-server=${cfg.loginServer}" - ] - ++ optional cfg.enableSSH "--ssh" - ++ optional cfg.acceptDNS "--accept-dns"; + 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 ]; }; - - environment.shellAliases = { - ts = "${cfg.package}/bin/tailscale"; - }; - - networking.firewall.trustedInterfaces = [ cfg.interfaceName ]; - - sops.secrets."tailscale/auth-key" = { }; - }; }