Files
cmux/Sources/GhosttyCrashBreadcrumb.swift
3822f1dd47 Run cmux iOS over authenticated Iroh transport (#7908)
* feat(iroh): bridge the production Swift endpoint

* feat(iroh): run the mac host transport

* test(iroh): reject local binding substitution

* test(ios): cover pooled fallback route

* fix(iroh): bind discovery to the local app instance

* fix(ios): retain successful pooled route

* test(iroh): require online-first host policy fallback

* feat(iroh): start hosts from verified offline policy

* feat(iroh): persist mac offline host policy

* fix(iroh): reject partial online binding rotation

* test(iroh): reject unvalidated private fallback

* feat(iroh): own iOS endpoint and multistream sessions

* fix(iroh): revalidate private fallback paths

* fix(iroh): accept JSON media type parameters

* ci(iroh): test full app on Intel Sonoma

* ci(iroh): run transport tests on Intel Sonoma

* test(iroh): cover abandoned relay reservations

* fix(iroh): expire abandoned relay reservations

* feat(iroh): expose admitted host multistream sessions

* test(iroh): require bounded incoming streams

* fix(iroh): bound peer-created QUIC streams

* feat(iroh): defer iOS transport activation

* feat(ios): prefer verified Iroh routes

* test(iroh): preserve endpoint on preferred port collision

* test(iroh): cover LAN rendezvous consistency

* fix(iroh): fall back from occupied preferred port

* feat(iroh): derive private rotating LAN aliases

* fix(iroh): serialize LAN discovery with revocation

* feat(iroh): make secure pairing the default

* docs(iroh): record offline and LAN trust boundaries

* feat(iroh): cache verified client policy offline

* docs(iroh): add Apple and proxy launch caveats

* docs(iroh): clarify Apple local network prompting

* test(iroh): cover offline cache teardown races

* fix(iroh): fence offline cache teardown

* test(iroh): cover online admission leases

* feat(iroh): gate online admission leases

* feat(iroh): add authenticated Bonjour LAN fallback

* feat(iroh): enforce online revocation leases

* test(iroh): cover offline admission leases

* test(iroh): cover canonical trust errors

* fix(iroh): harden trust broker boundaries

* fix(iroh): bound offline admission leases

* test(iroh): cover policy refresh revision races

* fix(iroh): fence admission policy refreshes

* docs(iroh): narrow private network release scope

* test(tailscale): reject unbound bearer routes

* fix(mobile): state private network boundaries

* test(tailscale): reject unbound bearer routes

Cover numeric-only Tailscale bearer routes and reject authorization, DNS, and route substitution before transport writes.

* docs(iroh): specify NAT authorization barrier

* fix(tailscale): bind bearer writes to live tunnel

* test(iroh): require acknowledged NAT admission barrier

* test(tailscale): reject route-only transport bypass

* fix(tailscale): close route-only transport bypass

* fix(iroh): acknowledge NAT admission before app streams

* test(iroh): hide database failure details

* fix(iroh): defer reservation constraint validation

* test(iroh): retain revocation monitor after handoff

* fix(iroh): retain revocation monitor for connection

* test(iroh): reject broker credential redirects

* fix(iroh): block broker credential redirects

* test(iroh): fail closed on terminal foreground policy

* fix(iroh): fail closed on terminal policy refresh

* test(iroh): prevent raw fallback after admission failure

* fix(iroh): pin authenticated pairings to Iroh

* test(auth): reject device registry redirects

* test(iroh): lock registration identity and relay bootstrap

* fix(iroh): preserve registration trust identity

* fix(auth): reject credentialed API redirects

* test(iroh): keep direct paths out of cloud storage

* fix(iroh): keep direct paths device local

* test(iroh): evict remotely closed client sessions

* test(iroh): recover dead session on foreground

* test(iroh): prevent server path-hint disclosure

* fix(iroh): recover suspended client sessions

* fix(iroh): keep private paths off server surfaces

* test(iroh): reject overlapping LAN bootstrap routes

* fix(iroh): reject ambiguous LAN interfaces

* test(iroh): bound pending admissions per identity

* fix(iroh): limit pending admissions per peer

* test(iroh): require owned server event stream

* test(auth): bound credentialed HTTP responses

* test(iroh): reject concurrent control owners

* fix(auth): cap credentialed HTTP responses

* test(iroh): cover firewall dependency failures

* fix(iroh): bound firewall availability checks

* feat(iroh): deliver server events on owned stream

* test(iroh): cap stalled firewall work

* test(iroh): bound active sessions per binding

* fix(iroh): cap stalled firewall work

* fix(iroh): cap active sessions per binding

* test(iroh): require firewall timeout recovery

* fix(iroh): abort stalled firewall checks

* fix(ci): isolate Iroh transport test suites

* test(iroh): route revocation to broker delete

* fix(iroh): send revocation to broker route

* test(mobile): bound concurrent RPC work

* fix(mobile): cap concurrent RPC work

* test(mobile): bound decoded frame batches

* fix(mobile): cap decoded frame batches

* test(iroh): bound pending Bonjour resolves

* test(iroh): gate reserved application lanes

* test(iroh): retain failed binding revocations

* fix(iroh): bound pending Bonjour resolves

* fix(iroh): gate reserved application lanes

* docs(iroh): narrow production multistream claims

* build(iroh): pin attested Swift fork release

* test(iroh): require retry-safe binding revocation

* fix(iroh): make binding revocation retry-safe

* fix(iroh): durably retry binding revocations

* build(iroh): lock iOS Swift fork release

* test(iroh): retain Bonjour observation lifetime

* test(auth): prepare before raced sign-out clear

* test(iroh): quarantine failed sign-out persistence

* test(ios): quarantine failed Iroh sign-out

* fix(iroh): quarantine incomplete sign-out teardown

* fix(iroh): clear host network state in quarantine

* fix(auth): quarantine Iroh before sign-out clear

* fix(ios): quarantine incomplete Iroh sign-out

* fix(mobile): type Iroh binding snapshot

* fix(ios): wait for auth clear before Iroh recovery

* build(iroh): lock app Swift fork release

* fix(iroh): persist secrets in ad-hoc debug builds

* test(iroh): require local-only HTTP minter opt-in

* feat(iroh): add loopback relay minter runner

* test(tailscale): require numeric registry targets

* feat(iroh): gate local relay minter HTTP

* fix(iroh): normalize local minter opt-in

* test(ios): require tagged API origin bake

* fix(ios): bake tagged API origin

* fix(tailscale): pin MagicDNS remotes to peer IPs

* fix(tailscale): reject inactive peer snapshots

* build(iroh): pin hardened FFI release

* test(auth): preserve auto-login during token reads

* test(auth): preserve manual sign-in during token reads

* test(iroh): require startup network event delivery

* fix(iroh): establish endpoint observation before activation

* fix(auth): preserve active session writers

* test(iroh): accept existing binding registration responses

* fix(iroh): accept existing binding relay status

* test(iroh): keep host active after refresh throttling

* fix(iroh): preserve host during broker throttling

* test(iroh): preserve client during broker throttling

* fix(iroh): retain verified policy during broker outages

* fix(iroh): decode broker dates on older macOS

* test(iroh): reject synthetic network change floods

* fix(iroh): observe address changes without feedback loop

* test(iroh): accept canonical UUID identity case

* fix(iroh): canonicalize pinned device UUIDs

* fix(iroh): harden compatibility and private routes

* refactor(iroh): split runtime ownership boundaries

* test(iroh): repair authorization suite split boundaries

* test(iroh): link mobile RPC authorization tests

* test(iroh): support compatibility compilers

* test(iroh): cover uppercase UUID fallback paths

* fix(iroh): canonicalize device UUID authority

* test(iroh): support Intel Sonoma compiler

* test(iroh): avoid non-Sendable fixture captures

* fix(updater): handle Intel-only Sparkle reason

* test(iroh): cover bearer and discovery overload

* fix(iroh): close route and discovery gaps

* fix(ci): close Iroh compatibility regressions

* feat(iroh): integrate endpoint-bound relay fleet

* fix(ci): wrap command timers for Intel Swift

* fix(ios): expose relay deployment to Sendable factory

* test(ci): cover private networking on Intel Sonoma

* test(ci): support Intel Swift Testing macros

* test(iroh): expose relay refresh expiry gap

* fix(iroh): retry relay refresh before expiry

* fix(ios): serialize Iroh quarantine recovery

* refactor(auth): isolate lifecycle revision API

* fix(ci): eliminate Iroh Swift 6 warnings

* fix(ci): support Intel Xcode 16.2

* fix(ci): mark canvas clock sleep sendable

* fix(ci): bridge canvas preferences to main actor

* fix(ci): support sidebar git on Xcode 16.2

* fix(ci): mark RPC termination handler sendable

* fix(ci): support CLI on Xcode 16.2

* fix(ci): support app target on Xcode 16.2

* fix(ci): finish Xcode 16.2 source compatibility

* iroh: point the broker relay fleet at the 7 self-hosted relay.cmux.dev URLs

Replaces the 4 hosted iroh.link relays with our self-hosted fleet in both
allowlists (web MANAGED_RELAY_URLS + presence worker APPROVED_IROH_RELAY_URLS,
kept in lockstep) and the tests that referenced hosted URLs. The self-hosted
relays run iroh-relay 1.0.2 behind per-region MIG+L4-LB (zero-downtime
upgrades), gated by the cmux EdDSA JWT that /api/relay/token (merged, #7879)
mints.

* fix(ci): support trailing closure on Xcode 16.2

* ci: allow Intel compatibility suite to finish

* test(ci): avoid Xcode 16.2 require recursion

* ci: focus Intel compatibility coverage

* fix(ci): stabilize replay ownership and Intel budget

* test(ios): isolate authoritative resync coverage

* feat(iroh): add secure flexible relay policy

* test(iroh): split relay runtime coverage

* test(iroh): allow self-hosted broker without legacy minter

* fix(iroh): make hosted relay minter optional

* test(iroh): cover public firewall host fallback

* fix(iroh): use public host for firewall checks

* fix(iroh): keep accepts and sign-out responsive

* test(iroh): reproduce lost online reachability

* fix(iroh): republish endpoint online routes

* test(iroh): reproduce coalesced route refresh

* fix(iroh): replay coalesced route refreshes

* refactor(iroh): split oversized runtime files

* test(iroh): cover lifecycle refresh races

* fix(iroh): fence lifecycle refresh work

* test(ios): reproduce loopback dev auto-pair race

* test(ios): cover redacted dev Iroh attach URLs

* fix(ios): wait for redacted Iroh dev attach ticket

* test(ios): reproduce Iroh cold-start attach race

* fix(ios): await Iroh before dev auto-pair

* feat(iroh): add server-driven relay preferences

* feat(iroh): complete relay controls and multistream runtime

* ci: rehearse staging migrations from dispatched branch

* Make managed Iroh credentials server-driven

* feat(iroh): expose redacted live path diagnostics

* security(iroh): stage relay policy key rotation

* fix(iroh): use instance-scoped host display name

* test(iroh): require dev attach targets to prefer identity routes

* fix(iroh): prefer identity routes for dev attach

* fix(web): include shared relay catalog in Next root

* test(web): keep relay catalog inside Next boundary

* fix(web): generate relay catalog inside runtime boundaries

* test(mobile): cover transport lifetime ownership

* fix(mobile): retain Iroh transport lifetime

* test(iroh): cover relay policy clock skew

* fix(iroh): tolerate bounded relay policy clock skew

* test(iroh): cover admitted session lifetime

* fix(iroh): separate admission and session lifetimes

* test(iroh): cover relay and route renewal stalls

* fix(iroh): keep relay routes renewed through storage stalls

* test(iroh): cover nonblocking binding persistence

* fix(iroh): publish bindings before secure persistence

* test(iroh): cover strict transport verification modes

* feat(iroh): add strict transport verification modes

* test(iroh): await nonblocking relay persistence

* test(iroh): cover live peer connection quotas

* fix(iroh): bound live sessions per endpoint

* test(iroh): cover broker-aware route renewal backoff

* fix(iroh): back off broker route renewal retries

* test(mobile): cover superseded Iroh transport cleanup

* fix(iroh): close unowned mobile sessions

* test(iroh): reproduce stale reconnect sessions

* fix(iroh): replace stale peer sessions on admission

* feat(iroh): add debug transport mode menu

* Add regression coverage for Iroh merge blockers

* Fix Iroh relay and reconnect merge blockers

---------

Co-authored-by: lawrencecchen <[email protected]>
Co-authored-by: cmux reload-cloud <[email protected]>
Co-authored-by: Aziz Albahar <[email protected]>
2026-07-14 20:17:42 -05:00

113 lines
4.2 KiB
Swift

import Foundation
enum GhosttyCrashBreadcrumb {
struct PendingCrash: Equatable, Sendable {
let fileURL: URL
let modifiedAt: Date
}
static let lastCleanExitDefaultsKey = "ghosttyCrashBreadcrumb.lastCleanExitAt"
static let lastShownCrashDefaultsKey = "ghosttyCrashBreadcrumb.lastShownCrashAt"
static let notificationTabId = UUID(uuidString: "00000000-0000-0000-0000-000000003873")!
nonisolated static var defaultCrashDirectoryURL: URL {
SessionPersistencePolicy.defaultCmuxCrashDirectoryURL()
}
nonisolated static var defaultCrashDirectoryURLs: [URL] {
SessionPersistencePolicy.cmuxCrashDirectoryURLs()
}
#if compiler(>=6.2)
@concurrent
#else
@Sendable
#endif
nonisolated static func pendingCrashFromDefaultStorage() async -> PendingCrash? {
await Task.detached(priority: .utility) {
pendingCrash(in: defaultCrashDirectoryURLs)
}.value
}
nonisolated static func pendingCrash(
in crashDirectoryURL: URL = defaultCrashDirectoryURL,
defaults: UserDefaults = .standard,
fileManager: FileManager = .default,
currentExecutableURL: URL? = Bundle.main.executableURL
) -> PendingCrash? {
pendingCrash(
in: [crashDirectoryURL],
defaults: defaults,
fileManager: fileManager,
currentExecutableURL: currentExecutableURL
)
}
nonisolated static func pendingCrash(
in crashDirectoryURLs: [URL],
defaults: UserDefaults = .standard,
fileManager: FileManager = .default,
currentExecutableURL: URL? = Bundle.main.executableURL
) -> PendingCrash? {
let latest = crashDirectoryURLs.compactMap {
latestCrashFile(
in: $0,
fileManager: fileManager,
currentExecutableURL: currentExecutableURL
)
}
.max { lhs, rhs in lhs.modifiedAt < rhs.modifiedAt }
guard let latest else {
return nil
}
let lastCleanExit = defaults.object(forKey: lastCleanExitDefaultsKey) as? Date ?? .distantPast
let lastShownCrash = defaults.object(forKey: lastShownCrashDefaultsKey) as? Date ?? .distantPast
guard latest.modifiedAt > lastCleanExit, latest.modifiedAt > lastShownCrash else {
return nil
}
return latest
}
private static func latestCrashFile(
in crashDirectoryURL: URL,
fileManager: FileManager,
currentExecutableURL: URL?
) -> PendingCrash? {
guard let urls = try? fileManager.contentsOfDirectory(
at: crashDirectoryURL,
includingPropertiesForKeys: [.contentModificationDateKey],
options: [.skipsHiddenFiles]
) else {
return nil
}
return urls
.filter { $0.pathExtension == "ghosttycrash" }
.filter { crashReportMatchesCurrentExecutable($0, currentExecutableURL: currentExecutableURL) }
.compactMap { url -> PendingCrash? in
guard let values = try? url.resourceValues(forKeys: [.contentModificationDateKey]),
let modifiedAt = values.contentModificationDate else {
return nil
}
return PendingCrash(fileURL: url, modifiedAt: modifiedAt)
}
.max { lhs, rhs in lhs.modifiedAt < rhs.modifiedAt }
}
nonisolated static func markShown(_ pendingCrash: PendingCrash, defaults: UserDefaults = .standard) {
defaults.set(pendingCrash.modifiedAt, forKey: lastShownCrashDefaultsKey)
}
nonisolated static func markCleanExit(defaults: UserDefaults = .standard, date: Date = Date()) {
defaults.set(date, forKey: lastCleanExitDefaultsKey)
}
private static func crashReportMatchesCurrentExecutable(_ url: URL, currentExecutableURL: URL?) -> Bool {
guard let currentExecutableURL else { return true }
guard let reportedExecutablePaths = GhosttyCrashReportMetadata.reportedExecutablePaths(in: url) else { return true }
let currentExecutablePath = GhosttyCrashReportMetadata.normalizedPath(currentExecutableURL.path)
return reportedExecutablePaths.contains(currentExecutablePath)
}
}