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:
2026-07-03 03:50:45 -07:00
co-authored by Claude Fable 5
parent cb6976baa4
commit 94a962bd20
4 changed files with 214 additions and 41 deletions
@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
BF00000000000000000001 /* NucleicProtocol in Frameworks */ = {isa = PBXBuildFile; productRef = PD00000000000000000001 /* NucleicProtocol */; };
BF00000000000000000003 /* NucleicTailnet in Frameworks */ = {isa = PBXBuildFile; productRef = PD00000000000000000002 /* NucleicTailnet */; };
BF00000000000000000002 /* NucleicRemoteWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FR00000000000000000002 /* NucleicRemoteWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E7D43FF12FF0806800BF2407 /* app-logo-file.icon in Resources */ = {isa = PBXBuildFile; fileRef = E7D43FF02FF0806800BF2407 /* app-logo-file.icon */; };
E7D43FF12FF0806800BF2408 /* app-logo-canary.icon in Resources */ = {isa = PBXBuildFile; fileRef = E7D43FF02FF0806800BF2408 /* app-logo-canary.icon */; };
@@ -93,6 +94,7 @@
buildActionMask = 2147483647;
files = (
BF00000000000000000001 /* NucleicProtocol in Frameworks */,
BF00000000000000000003 /* NucleicTailnet in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -153,6 +155,7 @@
name = NucleicRemote;
packageProductDependencies = (
PD00000000000000000001 /* NucleicProtocol */,
PD00000000000000000002 /* NucleicTailnet */,
);
productName = NucleicRemote;
productReference = FR00000000000000000001 /* NucleicRemote.app */;
@@ -293,7 +296,7 @@
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_NO_COMMON_BLOCKS = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
@@ -310,7 +313,7 @@
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_NO_COMMON_BLOCKS = YES;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
@@ -342,7 +345,7 @@
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_CFBundleDisplayName = "Nucleic$(NUCLEIC_NAME_SUFFIX)";
NUCLEIC_NAME_SUFFIX = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -389,7 +392,7 @@
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_CFBundleDisplayName = "Nucleic$(NUCLEIC_NAME_SUFFIX)";
NUCLEIC_NAME_SUFFIX = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -423,7 +426,7 @@
INFOPLIST_FILE = NucleicRemoteWidgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NucleicRemoteWidgets;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -454,7 +457,7 @@
INFOPLIST_FILE = NucleicRemoteWidgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = NucleicRemoteWidgets;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -517,6 +520,10 @@
isa = XCSwiftPackageProductDependency;
productName = NucleicProtocol;
};
PD00000000000000000002 /* NucleicTailnet */ = {
isa = XCSwiftPackageProductDependency;
productName = NucleicTailnet;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = PJ00000000000000000001 /* Project object */;