name the control server explicitly, and log out before moving between them

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) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 20:29:26 +00:00
co-authored by Claude Opus 5
parent 22a661131d
commit 06492c3297
2 changed files with 46 additions and 5 deletions
+30 -5
View File
@@ -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}")