Add Tailscale (Tailnet) as a sync transport between Mac and iPhone
Settings ▸ Remote gains a "Connect via" picker — LAN (default), Tailscale (tailnet), or Relay (disabled, coming soon). On Tailnet, both devices run an embedded tsnet node via TailscaleKit (tailscale/libtailscale) and sync frames flow over the user's tailnet, so the phone can connect from anywhere the tailnet reaches; Noise E2EE runs above the transport unchanged. - NucleicTailnet (new target, macOS + iOS): TailnetNode wraps TailscaleKit's node lifecycle (auth-key login, generation-fenced start/stop since up() is un-cancellable) and drops to the framework's public C API for the data path — tailscale_dial/listen/accept hand back full-duplex socketpair fds, wrapped by FDFrameChannel (DispatchIO) into the shared FrameChannel seam. The Swift wrapper's one-way connection actors can't carry a bidirectional stream. - Host: TailnetListener adopts SyncListener; startSyncServer is single-flight and honors toggle-off/picker changes at the commit point; pairing QRs carry transport + tailnet IP/port hints (PairingPayload additive optional fields, forward/backward compatible over CBOR). - iPhone: pair/reconnect dial over whichever transport the pairing recorded; Settings gains a Tailscale auth-key field (Keychain, committed on editing end); connectivity chip shows "Connected · Tailnet". - TailscaleKit has no SwiftPM distribution: scripts/build-tailscalekit.sh builds a pinned libtailscale commit into an untracked local xcframework; Package.swift links it only when present (everything builds without it, the picker then reports Tailscale support as not built in), and the script clears SwiftPM's content-keyed manifest cache so the toggle is picked up. - iOS floor 17.0 → 18.1 (TailscaleKit requires the iOS 18 Swift runtime); package-app.sh embeds the framework in the .app like Sparkle. 703-test suite: no new failures (the 7 fake-claude/fake-grok staging issues reproduce identically on an untouched checkout — pre-existing, tracked separately). New coverage: FDFrameChannel over socketpairs, pairing-payload version-skew both directions, transport-setting resolution. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -3,8 +3,10 @@ import Security
|
||||
import NucleicProtocol
|
||||
|
||||
/// What the phone pins about its Mac at pairing (SYNC §4.2): the host's static key (for IK
|
||||
/// reconnect), a display name, and an optional LAN hint. The pairing secret is *not* stored —
|
||||
/// it's one-time. Non-secret, so UserDefaults is fine; the device private key goes to Keychain.
|
||||
/// reconnect), a display name, and the transport + connection hint from the QR — LAN
|
||||
/// host:port or the Mac's tailnet IP. The pairing secret is *not* stored — it's one-time.
|
||||
/// Non-secret, so UserDefaults is fine; the device private key goes to Keychain. The new
|
||||
/// optional fields decode as nil from a pre-transport record (= LAN).
|
||||
struct PairedHost: Codable, Equatable {
|
||||
var deviceID: String
|
||||
var hostName: String
|
||||
@@ -12,6 +14,12 @@ struct PairedHost: Codable, Equatable {
|
||||
var fingerprint: String
|
||||
var lanHost: String?
|
||||
var lanPort: UInt16?
|
||||
/// `SyncTransportHint` raw value; nil = LAN (records saved before transports existed).
|
||||
var transport: String?
|
||||
var tailnetHost: String?
|
||||
var tailnetPort: UInt16?
|
||||
|
||||
var transportHint: SyncTransportHint { transport.flatMap(SyncTransportHint.init(rawValue:)) ?? .lan }
|
||||
}
|
||||
|
||||
/// Loads/persists this device's long-term `DeviceIdentity` (Keychain) and the pinned host
|
||||
|
||||
Reference in New Issue
Block a user