Merge nucleic/mellow-dewy-falcon-rjhr into main

This commit is contained in:
2026-08-07 03:46:54 -07:00
parent b84835649c
commit 042bd813a8
4 changed files with 199 additions and 24 deletions
+48 -2
View File
@@ -313,11 +313,48 @@ enum SSHTransportError: Error {
var asCoreError: CoreError {
switch self {
case .connectFailed(let host, let port, let underlying):
return .sshFailed("cannot connect to \(host):\(port): \(underlying)")
return .sshFailed(
"cannot connect to \(host):\(port): \(underlying)"
+ Self.localNetworkHint(for: underlying)
)
case .authenticationFailed(let host, let username):
return .sshFailed("authentication failed for \(username)@\(host)")
}
}
/// Extra guidance for the one connect failure that is usually not a network
/// problem at all.
///
/// macOS 15 and newer filter local-network traffic per app, and a blocked
/// flow is not reported as "denied": the filter answers `EHOSTUNREACH`
/// (errno 65, "No route to host"), which is exactly what a guest that is
/// genuinely off the network looks like. Guests here sit on a host-private
/// NAT link that is reachable whenever the VM is up, so on this code path
/// that errno is more often the privacy filter than a routing failure —
/// worth naming rather than leaving the operator to guess.
///
/// It matters most right after a rebuild. Per
/// [TN3179](https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy)
/// the grant "uses your main executable UUID", and the linker mints a fresh
/// `LC_UUID` on essentially every build — so `make install` can present a
/// program macOS has never seen, whose permission is undetermined again,
/// even though the previous binary worked minutes earlier.
static func localNetworkHint(for underlying: any Error) -> String {
let text = "\(underlying)".lowercased()
guard text.contains("errno: 65") || text.contains("no route to host")
|| text.contains("host is unreachable")
else { return "" }
return """
(on macOS 15+ this is also what Local Network privacy returns when \
it blocks an app — and the grant is keyed on the executable's UUID, \
so every rebuild withdraws it. Pre-authorize the guest subnet \
instead: sudo defaults write com.apple.network.local-network \
AllowedEthernetLocalNetworkAddresses -array "192.168.64.0/18" — same \
for AllowedWiFiLocalNetworkAddresses — then reboot. \
See docs/troubleshooting.md)
"""
}
}
/// Shared, thread-safe record of whether the server rejected our password.
@@ -584,6 +621,15 @@ public func waitForSSH(
guard ContinuousClock.now - started < timeout else { break }
}
let detail = lastError.map { "; last error: \($0)" } ?? ""
// Rendered through `asCoreError` rather than interpolated raw: a connect
// failure is where the Local Network privacy hint lives, and the timeout
// message is the *only* place most operators will ever see the last error.
let detail: String
if let lastError {
let rendered = (lastError as? SSHTransportError).map { "\($0.asCoreError)" } ?? "\(lastError)"
detail = "; last error: \(rendered)"
} else {
detail = ""
}
throw CoreError.timeout("ssh on \(host):\(port)\(detail)")
}