From 06492c32970ba732627630fa538c321dc52b1d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 12 Aug 2026 20:29:26 +0000 Subject: [PATCH] name the control server explicitly, and log out before moving between them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps found by tracing the assembled command rather than assuming it, and the first meant option 3 did not work at all on a machine like this one. `tailscale up` with no --login-server keeps whatever ControlURL is already stored. So on a node already pointed at a self-hosted server — which this host is — choosing "the easy route" left it exactly where it was. No error, no message, and a summary line claiming it had connected. The URL is now passed explicitly in both cases, TS_DEFAULT_CONTROL_URL for Tailscale's own service. And a node logged in to one coordination server cannot simply be pointed at another; it has to be logged out first. That is now detected by comparing the stored URL with the target, and offered rather than done quietly — the tailnet drops while it happens, and the run says so, because on a machine reached over the tailnet that is the session you are reading this in. Declining leaves the node where it is and records that. Verified all three paths with tailscale stubbed: switching logs out then connects to controlplane.tailscale.com, declining leaves it on offscale, and reconnecting to the SAME server offers no logout at all. Also noted while tracing: lan_cidr correctly finds nothing on this host, since a /32 with host routes has no subnet to advertise. That means the homelab subnet-router prompt is the one path here that has not been exercised on real hardware. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/setup/machine-setup/lib/tailscale.sh | 16 +++++++++ scripts/setup/machine-setup/machine-setup.sh | 35 +++++++++++++++++--- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/scripts/setup/machine-setup/lib/tailscale.sh b/scripts/setup/machine-setup/lib/tailscale.sh index 0f5172b9..fdc0bbdd 100644 --- a/scripts/setup/machine-setup/lib/tailscale.sh +++ b/scripts/setup/machine-setup/lib/tailscale.sh @@ -169,6 +169,22 @@ tailscale_control_url() { tailscale_install() { curl -fsSL https://tailscale.com/install.sh | sh; } +# Tailscale's own coordination server, spelled out. +# +# Passed explicitly even when it is the default, because `tailscale up` with no +# --login-server keeps whatever ControlURL is already stored. On a node already +# pointed at a self-hosted server, choosing "the easy route" would otherwise +# leave it exactly where it was — no error, no message, wrong answer. +TS_DEFAULT_CONTROL_URL="https://controlplane.tailscale.com" + +# Moving a node between coordination servers is not something `up` will do while +# it is logged in to one. Logging out first is the documented way, and doing it +# unasked would be worse than saying so. +tailscale_needs_logout() { + local current="$1" target="$2" + [[ -n "$current" && -n "$target" && "$current" != "$target" ]] +} + # Routing has to be on before this machine can forward anyone else's packets, # whether as an exit node or as a subnet router. Written as a drop-in so it is # visible as this script's doing. diff --git a/scripts/setup/machine-setup/machine-setup.sh b/scripts/setup/machine-setup/machine-setup.sh index c79b4953..1d10494f 100755 --- a/scripts/setup/machine-setup/machine-setup.sh +++ b/scripts/setup/machine-setup/machine-setup.sh @@ -530,11 +530,34 @@ if ! skip; then install_exit_node_tuning fi + # Where this node is being pointed, named explicitly in both cases — see + # TS_DEFAULT_CONTROL_URL for why the default is not left implicit. + if [[ "$TS_PLANE" == "self-hosted" ]]; then + TS_TARGET_URL="$TS_LOGIN_SERVER" + else + TS_TARGET_URL="$TS_DEFAULT_CONTROL_URL" + fi + + # A node logged in to one coordination server cannot simply be pointed at + # another; it has to be logged out first. Said out loud rather than done + # quietly, because it drops the tailnet for a moment. + TS_URL_NOW="$(tailscale_control_url)" + if [[ "$TS_STATE" == "Running" ]] && tailscale_needs_logout "$TS_URL_NOW" "$TS_TARGET_URL"; then + echo "" + warn "this node is on ${TS_URL_NOW}, and moving it to ${TS_TARGET_URL} means logging out first" + echo " The tailnet drops while that happens. If you are connected over" + echo " it right now, this session goes with it." + if confirm "Log out and move it?" n; then + tailscale logout || true + else + warn "left where it is" + SUMMARY+=("Tailscale: left on ${TS_URL_NOW}") + TS_CONNECT=false + fi + fi + echo "" - TS_ARGS=(up --timeout=60s) - # Only a self-hosted server needs pointing at. Option 3 is the default - # coordination server, which means passing no --login-server at all. - [[ "$TS_PLANE" == "self-hosted" ]] && TS_ARGS+=(--login-server "$TS_LOGIN_SERVER") + TS_ARGS=(up --timeout=60s --login-server "$TS_TARGET_URL") # Never passed empty. `--authkey ""` silently falls back to the interactive # flow and blocks forever, which is exactly how the original hung. [[ -n "$TS_AUTHKEY" ]] && TS_ARGS+=(--authkey "$TS_AUTHKEY") @@ -547,7 +570,9 @@ if ! skip; then fi echo "" - if tailscale "${TS_ARGS[@]}"; then + if [[ "$TS_CONNECT" == false ]]; then + : + elif tailscale "${TS_ARGS[@]}"; then TS_IP="$(tailscale_ip)" ok "connected as ${TS_IP} on $(hostname)" SUMMARY+=("Tailscale: ${TS_IP}${TS_SSH:+, Tailscale SSH}${TS_ROUTES:+, subnet router}${TS_EXIT:+, exit node}")