tailscale: add support for multiple tailnets #26

Merged
sid merged 1 commit from develop into release-25.11 2026-05-02 19:01:43 +02:00

View file

@ -1,9 +1,17 @@
{ config, lib, ... }: {
config,
lib,
pkgs,
...
}:
let let
cfg = config.services.tailscale; cfg = config.services.tailscale;
inherit (lib) inherit (lib)
concatStrings
filterAttrs
mapAttrsToList
mkIf mkIf
mkOption mkOption
optional optional
@ -12,39 +20,124 @@ let
in in
{ {
options.services.tailscale = { options.services.tailscale = {
tailnets = mkOption {
default = { };
type = types.attrsOf (
types.submodule {
options = {
loginServer = mkOption { loginServer = mkOption {
type = types.str; type = types.str;
description = "The Tailscale login server to use."; description = "Login server for this tailnet.";
};
authKeyFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to auth key secret.";
}; };
enableSSH = mkOption { enableSSH = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = "Enable Tailscale SSH functionality.";
}; };
acceptDNS = mkOption { acceptDNS = mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
description = "Enable Tailscale's MagicDNS and custom DNS configuration."; };
default = mkOption {
type = types.bool;
default = false;
description = "Connect to this tailnet on boot.";
};
};
}
);
}; };
}; };
config = mkIf cfg.enable { config =
services.tailscale = { let
authKeyFile = config.sops.secrets."tailscale/auth-key".path; defaultTailnets = filterAttrs (_: t: t.default) cfg.tailnets;
extraSetFlags = optional cfg.enableSSH "--ssh" ++ optional cfg.acceptDNS "--accept-dns"; 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 <tailnet-name>" >&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 = [ extraUpFlags = [
"--login-server=${cfg.loginServer}" "--login-server=${loginServer}"
] ]
++ optional cfg.enableSSH "--ssh" ++ optional enableSSH "--ssh"
++ optional cfg.acceptDNS "--accept-dns"; ++ optional acceptDNS "--accept-dns";
}; }
);
environment.shellAliases = { environment.systemPackages = [ tailnetSwitchCli ];
ts = "${cfg.package}/bin/tailscale";
}; environment.shellAliases.ts = "${cfg.package}/bin/tailscale";
networking.firewall.trustedInterfaces = [ cfg.interfaceName ]; networking.firewall.trustedInterfaces = [ cfg.interfaceName ];
sops.secrets."tailscale/auth-key" = { };
}; };
} }