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
@@ -1,5 +1,5 @@
{ {
"originHash" : "fa70224ebd92e3bfa55fbdf5bbea5d2a51a1f6402e805a443a323509fbc905ed", "originHash" : "6a454662a64d0761bc14f81f12302560e4ac3eba029e64f1e0b0b7f004590631",
"pins" : [ "pins" : [
{ {
"identity" : "async-http-client", "identity" : "async-http-client",
@@ -267,8 +267,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/migueldeicaza/SwiftTerm.git", "location" : "https://github.com/migueldeicaza/SwiftTerm.git",
"state" : { "state" : {
"revision" : "8e7a1e154f470e19c709a00a8768df348ba5fc43", "revision" : "849e8a4f3d6f79ddee07152400137f1370c32621",
"version" : "1.13.0" "version" : "1.14.0"
} }
}, },
{ {
@@ -150,6 +150,16 @@ final class HostConnection {
private var reconnectAttempts = 0 private var reconnectAttempts = 0
private var seenSeq: Set<UInt64> = [] 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 { private enum TransportAttempt {
case lan(NWEndpoint) case lan(NWEndpoint)
case tailnet(host: String, port: UInt16) case tailnet(host: String, port: UInt16)
@@ -354,7 +364,7 @@ final class HostConnection {
connectTask = Task { [weak self] in connectTask = Task { [weak self] in
guard let self else { return } guard let self else { return }
do { do {
let channel = try await RelayFrameChannel.dial( let channel = try await self.dialRelay(
base: base, membershipToken: membershipToken) base: base, membershipToken: membershipToken)
guard !Task.isCancelled else { channel.close(); return } guard !Task.isCancelled else { channel.close(); return }
// A room with no live host swallows frames silently until presence says // 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 /// A candidate that dials asynchronously (tailnet, relay) failed before producing a
/// channel — fall through to the next candidate or schedule a retry. /// channel — fall through to the next candidate or schedule a retry.
private func asyncAttemptFailed(_ error: Error, isPairing: Bool) { private func asyncAttemptFailed(_ error: Error, isPairing: Bool) {
@@ -667,6 +700,9 @@ final class HostConnection {
pinnedHost?.relayMembershipToken = membership.token pinnedHost?.relayMembershipToken = membership.token
pinnedHost?.relayURL = membership.url 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): case .pairingCode(let qr):
// This Mac minted a join code we asked for ("add a device to this mesh") — hand it up // 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. // to RemoteStore for the QR/copy sheet.