From 26d362f306f2c2a5dd17e37f85685ba0342fc75a Mon Sep 17 00:00:00 2001 From: Justin Garrison Date: Thu, 13 Aug 2026 14:45:22 -0700 Subject: [PATCH] cmd/ts-multi-plug: log out on shutdown When ts-plug receives SIGINT/SIGTERM/SIGHUP, call tsnet.LocalClient.Logout() before cancelling the main context. This tells the Tailscale control plane to expire the node key immediately. Signed-off-by: Justin Garrison --- cmd/ts-multi-plug/ts-multi-plug.go | 36 ++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/cmd/ts-multi-plug/ts-multi-plug.go b/cmd/ts-multi-plug/ts-multi-plug.go index c3b6ca0..be107ce 100644 --- a/cmd/ts-multi-plug/ts-multi-plug.go +++ b/cmd/ts-multi-plug/ts-multi-plug.go @@ -18,6 +18,7 @@ import ( "os/signal" "strconv" "strings" + "sync/atomic" "syscall" "time" @@ -25,6 +26,9 @@ import ( "tailscale.com/tsnet" ) +// lcPtr is set after LocalClient() so the signal handler can call Logout. +var lcPtr atomic.Pointer[local.Client] + var ( flagHostname string flagDir string @@ -125,21 +129,28 @@ func main() { slog.Info("command started") } - // handle the exit cases either from signal or the upstream command exiting + // cmd.Wait() runs in its own goroutine so it does not block the signal + // handler below. Evaluated inline in a select send case it stalls until + // the child exits, which would prevent the Logout call from ever firing. go func() { - for { - select { - case cmdExitChan <- cmd.Wait(): - // the upstream command has exited - return - case sig := <-signalChan: - slog.Info("signal received, shutting down...", "sig", sig.String()) - - // this will cause the case above with cmd.Wait() to return - // as well ts.Up() to exit early if it hasn't been fully initialized yet - cancelCtx() + cmdExitChan <- cmd.Wait() + }() + + go func() { + sig := <-signalChan + slog.Info("signal received, shutting down...", "sig", sig.String()) + + if lc := lcPtr.Load(); lc != nil { + logoutCtx, cancelLogout := context.WithTimeout(context.Background(), 10*time.Second) + if err := lc.Logout(logoutCtx); err != nil { + slog.Warn("tsnet logout failed", "error", err) + } else { + slog.Info("tsnet logout complete") } + cancelLogout() } + + cancelCtx() }() ts := &tsnet.Server{ @@ -174,6 +185,7 @@ func main() { cancelCtx() os.Exit(1) } + lcPtr.Store(lc) hostname := strings.TrimSuffix(st.Self.DNSName, ".")