From f1385adc72fad9e7ea9316a7ef73a2c827fa1844 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Mon, 6 Jul 2026 13:36:28 -0700 Subject: [PATCH] Network Connection Logic Enhancement Nucleic-Session: 93A70648-2057-4092-9372-860E1BA151D8 Co-authored-by: Nucleic --- .../NucleicRemote/Models/HostConnection.swift | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/NucleicRemote/NucleicRemote/Models/HostConnection.swift b/NucleicRemote/NucleicRemote/Models/HostConnection.swift index 85d84c1..cd74552 100644 --- a/NucleicRemote/NucleicRemote/Models/HostConnection.swift +++ b/NucleicRemote/NucleicRemote/Models/HostConnection.swift @@ -689,6 +689,72 @@ final class HostConnection { if !connectivity.isLive, pathMonitor.isSatisfied { reconnectAttempts = 0 reconnect(to: host) + return + } + // Live over a fallback transport (tailnet/relay) and a LAN path just reappeared (rejoined the + // Mac's Wi-Fi) → the return leg of the switch above. The tunnel survives the interface change + // on its own, so nothing else would ever re-plan back down to the direct path — probe LAN and + // swap to it if the Mac actually answers here. + maybeUpgradeToLAN() + } + + /// Probe whether the pinned Mac is reachable over LAN right now and, if so, switch back to the + /// direct connection — the tailnet/relay → LAN return leg. Only meaningful while live over a + /// non-LAN transport with a LAN-capable path available; a bare TCP connect to the pinned LAN + /// endpoint that reaches `.ready` is the "the Mac is on this network" signal. The probe runs + /// entirely off the live connection, so joining a *foreign* Wi-Fi (no Mac here) costs one short, + /// silent connect attempt and leaves the working tunnel untouched. + private func maybeUpgradeToLAN() { + guard let host = pinnedHost, connectivity.isLive, activeTransport != .lan, + pathMonitor.canUseLAN, lanUpgradeProbe == nil, + let endpoint = discovery.endpoint( + forFingerprint: host.fingerprint, lanHost: host.lanHost, lanPort: host.lanPort) + else { return } + lanUpgradeProbe = Task { [weak self] in + let reachable = await Self.probeReachable(endpoint) + guard let self else { return } + self.lanUpgradeProbe = nil + // Re-check the world didn't move under us during the probe (still live, still not on LAN, + // LAN still available) before tearing a working connection down. + guard !Task.isCancelled, reachable, self.connectivity.isLive, + self.activeTransport != .lan, self.pathMonitor.canUseLAN, + let host = self.pinnedHost + else { return } + self.reconnectAttempts = 0 + self.reconnect(to: host) // rebuilds the candidate chain LAN-first + } + } + + /// Bare TCP reachability check for `maybeUpgradeToLAN`: does `endpoint` accept a connection within + /// `timeoutSeconds`? Tears the probe socket down regardless — it only tests reachability, never + /// carries traffic. A short timeout so a foreign Wi-Fi (Mac absent) doesn't stall the check. + private static func probeReachable(_ endpoint: NWEndpoint, timeoutSeconds: Double = 3) async -> Bool { + let params = NWParameters.tcp + params.includePeerToPeer = true + let connection = NWConnection(to: endpoint, using: params) + let queue = DispatchQueue(label: "nucleic.remote.lanprobe") + let once = ProbeOnce() + return await withTaskCancellationHandler { + await withCheckedContinuation { (cont: CheckedContinuation) in + connection.stateUpdateHandler = { state in + switch state { + case .ready: + if once.take() { cont.resume(returning: true) } + connection.cancel() + case .failed, .cancelled: + if once.take() { cont.resume(returning: false) } + default: + break + } + } + queue.asyncAfter(deadline: .now() + timeoutSeconds) { + if once.take() { cont.resume(returning: false) } + connection.cancel() + } + connection.start(queue: queue) + } + } onCancel: { + connection.cancel() } }