{ config, pkgs, lib, ... }:
let
cfg = config.my.tailscale.instances;
mkInstance = suffix: instance:
let
stateDir = "${config.home.homeDirectory}/.local/share/tailscale-${suffix}";
relayServerPort = toString instance.relayServerPort;
tailscale-wrapped = pkgs.writeShellScriptBin "tailscale-${suffix}" ''
${pkgs.tailscale}/bin/tailscale --socket /tmp/tailscale-${suffix}.sock $@
'';
tailscaled-wrapped = pkgs.writeShellScriptBin "tailscaled-${suffix}" ''
TS_LOGS_DIR="${stateDir}" \
${pkgs.tailscale}/bin/tailscaled \
--port=41641 \
--tun userspace-networking \
--outbound-http-proxy-listen=localhost:${toString instance.httpPort} \
--socks5-server=localhost:${toString instance.socks5Port} \
--socket=/tmp/tailscale-${suffix}.sock \
--state=${stateDir}/tailscaled.state \
--statedir=${stateDir} \
$@
'';
setRelayServerPort = pkgs.writeShellScript "tailscale-${suffix}-set-relay-server-port" ''
i=0
while [ "$i" -lt 20 ]; do
if ${tailscale-wrapped}/bin/tailscale-${suffix} set --relay-server-port=${relayServerPort}; then
exit 0
fi
i=$((i + 1))
${pkgs.coreutils}/bin/sleep 0.5
done
exit 1
'';
in {
packages = [
tailscale-wrapped
tailscaled-wrapped
];
service = {
Unit = {
Description = "Auto start tailscaled-${suffix} userspace network";
After = ["clash.service"];
};
Install = {
WantedBy = ["default.target"];
};
Service = {
Environment = [
"HTTPS_PROXY=http://127.0.0.1:${toString config.proxyPort}"
"HTTP_PROXY=http://127.0.0.1:${toString config.proxyPort}"
"https_proxy=http://127.0.0.1:${toString config.proxyPort}"
"http_proxy=http://127.0.0.1:${toString config.proxyPort}"
];
ExecStart = "${tailscaled-wrapped}/bin/tailscaled-${suffix}";
# The relay server port is a tailscale preference, not a tailscaled flag.
# ExecStartPost can race the daemon socket on startup, so retry briefly.
ExecStartPost = "${setRelayServerPort}";
};
};
zshInit = ''
compdef _tailscale tailscale-${suffix}
'' + lib.optionalString config.isNixOnDroid ''
# start tailscale-${suffix}
if [[ -z "$(pidof tailscaled-${suffix})" ]]; then
tmux new -d -s tailscaled-${suffix} tailscaled-${suffix}
fi
'';
};
instanceNames = lib.attrNames cfg;
instances = map (name: {
inherit name;
value = mkInstance name cfg.${name};
}) instanceNames;
in {
options.my.tailscale.instances = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
httpPort = lib.mkOption {
type = lib.types.port;
default = 1056;
};
socks5Port = lib.mkOption {
type = lib.types.port;
default = 1066;
};
relayServerPort = lib.mkOption {
type = lib.types.port;
default = 40000;
};
};
});
default = {};
};
config = {
home.packages = lib.mkIf (instanceNames != []) (
[ pkgs.tailscale ] ++ lib.concatMap (instance: instance.value.packages) instances
);
systemd.user.services = lib.mkMerge (map (instance: {
"tailscaled-${instance.name}" = instance.value.service;
}) instances);
programs.zsh.initContent = lib.concatStringsSep "" (map (instance: instance.value.zshInit) instances);
};
}