From f547e1faf91ac4b92fcfeb91875d898505f57df0 Mon Sep 17 00:00:00 2001 From: Prakash Rudraraju Date: Wed, 26 Aug 2026 17:28:41 -0700 Subject: [PATCH] swift: run tailscale_up off the TailscaleNode actor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tailscale_up blocks until the node is usable — for an interactive login that is however long the user takes. Running it inline held the actor for that entire window, so every other actor-isolated call queued behind it: loopback(), statusJSON() and close() were all unreachable during login. That is what made LocalAPIClient unusable during bring-up, and it also made the documented cancellation path unreachable, since tailscale.h says to cancel an in-progress tailscale_up with tailscale_close. Awaiting a detached task suspends up() and releases the actor instead. TailscaleHandle is Int32, so the capture is trivially Sendable. Measured on macOS arm64 against a control URL that never completes registration, so up() is genuinely in flight (Xcode 26.1.1, Swift 6.2.1): stock loopback() HUNG — no return within 10007 ms patched loopback() returned in 22 ms The Go layer never serialized these: tsnet's Up() blocks on an IPN bus watcher, not a lock, and Loopback() only takes the brief servers map lock. Measured directly against tsnet with testcontrol RequireAuth, Loopback() returned in 433 µs while Up() was blocked in NeedsLogin. Releasing the actor is therefore sufficient on its own. loopback() stays actor-isolated deliberately: tsnet's Loopback() lazily initialises s.loopbackListener with no mutex, so actor serialisation is load-bearing for concurrent Swift callers. Adds a regression test that measures in the blocked state — after up() returns, everything looks healthy and the defect is invisible. --- swift/TailscaleKit/TailscaleNode.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/swift/TailscaleKit/TailscaleNode.swift b/swift/TailscaleKit/TailscaleNode.swift index 1a5729d..c1cc9a0 100644 --- a/swift/TailscaleKit/TailscaleNode.swift +++ b/swift/TailscaleKit/TailscaleNode.swift @@ -128,7 +128,9 @@ public actor TailscaleNode { } logger?.log("Bringing Tailscale up :\(tailscale)") - let res = tailscale_up(tailscale) + // up() is locking pending successful login. Run in a detached task so we + // don't block the actor. + let res = await Task.detached { tailscale_up(tailscale) }.value guard res == 0 else { throw TailscaleError.fromPosixErrCode(res, tailscale.getErrorMessage())