From 6269a4f5a5d08a3ad4a0de701386d5028dd90046 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 13 Jul 2026 03:25:53 -0700 Subject: [PATCH] Merge nucleic/dusky-willow-weasel-znsn into dev --- .../xcshareddata/swiftpm/Package.resolved | 6 +-- .../NucleicRemote/Models/HostConnection.swift | 38 ++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/NucleicRemote/NucleicRemote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NucleicRemote/NucleicRemote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1d23168..05af4d3 100644 --- a/NucleicRemote/NucleicRemote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NucleicRemote/NucleicRemote.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "fa70224ebd92e3bfa55fbdf5bbea5d2a51a1f6402e805a443a323509fbc905ed", + "originHash" : "6a454662a64d0761bc14f81f12302560e4ac3eba029e64f1e0b0b7f004590631", "pins" : [ { "identity" : "async-http-client", @@ -267,8 +267,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/migueldeicaza/SwiftTerm.git", "state" : { - "revision" : "8e7a1e154f470e19c709a00a8768df348ba5fc43", - "version" : "1.13.0" + "revision" : "849e8a4f3d6f79ddee07152400137f1370c32621", + "version" : "1.14.0" } }, { diff --git a/NucleicRemote/NucleicRemote/Models/HostConnection.swift b/NucleicRemote/NucleicRemote/Models/HostConnection.swift index 832314c..82d6daf 100644 --- a/NucleicRemote/NucleicRemote/Models/HostConnection.swift +++ b/NucleicRemote/NucleicRemote/Models/HostConnection.swift @@ -150,6 +150,16 @@ final class HostConnection { private var reconnectAttempts = 0 private var seenSeq: Set = [] + /// The relay's short-lived connection token, cached for reuse across reconnects. The token is a + /// stateless HMAC bearer that isn't enforced single-use (`handleRelay` re-verifies it every + /// upgrade with no per-token state), so within its TTL a reconnect reuses it and skips the + /// `/v1/relay/connect` POST — a cold TLS round-trip to Cloudflare that dominates the reconnect + /// after an iOS lock (the app was suspended and the socket silently died). Invalidated when the + /// host reissues the relay membership (the room may move) or when a reused token is refused at + /// the upgrade. Survives `teardown()` on purpose — reuse across the very reconnect it triggers + /// is the point. + private var relayConnectionToken: RelayAPI.MintedConnectionToken? + private enum TransportAttempt { case lan(NWEndpoint) case tailnet(host: String, port: UInt16) @@ -354,7 +364,7 @@ final class HostConnection { connectTask = Task { [weak self] in guard let self else { return } do { - let channel = try await RelayFrameChannel.dial( + let channel = try await self.dialRelay( base: base, membershipToken: membershipToken) guard !Task.isCancelled else { channel.close(); return } // A room with no live host swallows frames silently until presence says @@ -376,6 +386,29 @@ final class HostConnection { } } + /// Open the relay socket, reusing a cached connection token whenever one is still comfortably + /// valid so the common reconnect (back from a lock, on the same relay room) skips the + /// `/v1/relay/connect` POST. On a cold cache it mints — and caches — a fresh token. If a reused + /// token is refused at the upgrade (expired under our margin, or the membership rotated), it + /// drops the cache and mints once before propagating the failure to the candidate/retry loop. + private func dialRelay(base: URL, membershipToken: String) async throws -> RelayFrameChannel { + // Reuse only with enough slack that the token outlives the handshake it's about to gate; + // a token about to expire mid-handshake would just force the re-mint below anyway. + if let cached = relayConnectionToken, cached.expiresAt.timeIntervalSinceNow > 15 { + do { + return try await RelayFrameChannel.dial(base: base, connectionToken: cached.token) + } catch { + // Teardown cancelled us — don't burn a mint on the way out. + if Task.isCancelled { throw error } + // The reused token was refused or its socket failed; drop it and try a fresh one. + relayConnectionToken = nil + } + } + let minted = try await RelayAPI.mintConnectionToken(base: base, membershipToken: membershipToken) + relayConnectionToken = minted + return try await RelayFrameChannel.dial(base: base, connectionToken: minted.token) + } + /// A candidate that dials asynchronously (tailnet, relay) failed before producing a /// channel — fall through to the next candidate or schedule a retry. private func asyncAttemptFailed(_ error: Error, isPairing: Bool) { @@ -667,6 +700,9 @@ final class HostConnection { pinnedHost?.relayMembershipToken = membership.token pinnedHost?.relayURL = membership.url } + // A fresh membership can name a new room, which the cached connection token (minted + // against the old one) wouldn't admit us to — drop it so the next dial re-mints. + relayConnectionToken = nil case .pairingCode(let qr): // This Mac minted a join code we asked for ("add a device to this mesh") — hand it up // to RemoteStore for the QR/copy sheet.