141 lines
4 KiB
Nix
141 lines
4 KiB
Nix
{
|
|
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 <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 = [
|
|
"--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 ];
|
|
};
|
|
}
|