Merge nucleic/dusky-willow-weasel-znsn into dev

This commit is contained in:
2026-07-13 03:25:53 -07:00
parent 5903f8ed14
commit 6269a4f5a5
2 changed files with 40 additions and 4 deletions
@@ -150,6 +150,16 @@ final class HostConnection {
private var reconnectAttempts = 0
private var seenSeq: Set<UInt64> = []
/// 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.