Remote iOS Deployment Over ZeroTier: What Worked, What Didn’t
Remote iOS Deployment Over ZeroTier: What Worked, What Didn't
Date: 2026-04-17 Author: Oguzhan Cakmak Project: MindType (LLMFriend Keyboard)
The Problem
Deploying dev builds of MindType to a physical iPhone requires either a USB cable or being on the same WiFi network. Both are limiting — USB tethers you to a desk, and same-WiFi breaks down when the phone is on cellular or a different network entirely.
We wanted a single workflow: build on Mac, install on iPhone, from anywhere — using ZeroTier as a virtual LAN connecting both devices over the internet.
Setup
- Mac: ZeroTier CLI, IP
10.144.0.70on private networkcehalet_lan - iPhone 15 Pro Max: ZeroTier iOS app, IP
10.144.0.35 - Network: Private ZeroTier network
d5e5fb65372d78a9, both devices authorized - Xcode 16+ with
devicectlavailable - Prior USB pairing completed (device trusted, pair records in
~/Library/Lockdown/)
Attempt 1: Xcode Wireless Debugging Over ZeroTier
The Idea
Xcode's "Connect via network" feature lets you build and debug wirelessly. If we could make it work over ZeroTier instead of local WiFi, we'd have full Xcode functionality — Cmd+R, breakpoints, console — from anywhere.
The Challenge: Bonjour Discovery
Xcode discovers devices via Bonjour (mDNS multicast on 224.0.0.251:5353). ZeroTier supports multicast, but Xcode won't see devices that aren't advertising on the local network.
Solution: dns-sd -P Proxy Registration
We registered the iPhone's Bonjour service manually on the Mac, tricking Xcode into thinking the phone was on the local network:
dns-sd -P "Oguzhan's iPhone 15 Pro Max" \
_apple-mobdev2._tcp. local. 62078 \
Oguzhans-iPhone-ZT.local. 10.144.0.35
This tells macOS: "There's an iPhone at 10.144.0.35:62078 offering the _apple-mobdev2._tcp service."
Result: Xcode Sees the Device

Xcode's device picker showed the iPhone with the globe (network) icon, both in "Recent" and "iOS Device" sections. Build Succeeded appeared at the top — Xcode recognized the device.
We automated this with a launchd agent so it persists across reboots:
~/Library/LaunchAgents/com.muratcakmak.xcode-zt-proxy.plist
But Then: The 5G Test
When we switched the iPhone from WiFi to 5G (keeping ZeroTier connected), everything broke:

"Browsing on the local area network for Oguzhan's iPhone 15 Pro Max, which has previously reported preparation errors"
The phone was pingable over ZeroTier (~450ms latency, some packet loss) but port 62078 was closed:
$ ping 10.144.0.35
4 packets received, 20% packet loss, avg 473ms
$ nc -zv 10.144.0.35 62078
Connection refused
A full port scan found zero open TCP ports on the phone over ZeroTier.
Root Cause: iOS Interface Binding Restriction
Deep research confirmed this is a hard iOS limitation, not a configuration issue:
lockdownd/remoted(the services Xcode connects to) bind exclusively to the physical WiFi interface (en0), not VPN/tunnel interfaces- iOS uses
getifaddrs()and explicitly filters foren0—utuninterfaces (used by ZeroTier, Tailscale, WireGuard) are skipped - ICMP works because ping uses the kernel's IP stack; TCP services choose which interface to bind to
- No MDM profile, setting, or workaround exists for non-jailbroken devices
- Tailscale users report the exact same limitation
Verdict: Xcode wireless debugging over VPN is impossible without jailbreak.
What We Also Tried
pymobiledevice3— Installed viauv tool install. Attemptedlockdown start-tunnel --mobdev2to create a RemoteXPC tunnel. Same failure: can't reach port 62078 over the ZT interface.devicectl— Shows the phone asunavailablewhen not on local WiFi. No--addressflag to force an IP.- Port scanning — Scanned ranges 49152-49250, 58000-58100, 62070-62090 plus common ports. All closed over ZeroTier.
Important Gotcha: ZeroTier Default Route
When testing on 5G, we initially had "Enable Default Route" toggled ON in the ZeroTier iOS app. This routes all phone traffic through ZeroTier — including ZeroTier's own UDP tunnel traffic. This creates a routing loop and kills connectivity entirely:
$ ping 10.144.0.35
No route to host
Host is down
Fix: Turn off "Enable Default Route", keep "Enable On Demand" on. The network must be restarted (toggle off/on) for the change to take effect.
Attempt 2: OTA Install Over ZeroTier
Since Xcode can't debug over ZeroTier, we pivoted to OTA (Over-The-Air) installation — pre-build an IPA and install it via Safari, like a mini App Store.
How OTA Install Works
Mac (Xcode) iPhone (Safari)
┌──────────┐ ┌──────────┐
│ xcodebuild│──> .ipa │ │
│ archive │ │ │ Download │
└──────────┘ ▼ │ Install │
┌──────────┐ ┌────────┐ HTTPS │ │
│ Python │ │manifest│ ──────────> │ │
│ HTTPS │ │ .plist │ │ │
│ server │ │ .ipa │ └──────────┘
└──────────┘ │ .html │
└────────┘
xcodebuild archive+xcodebuild -exportArchivecreates a dev-signed.ipa- A
manifest.plistpoints Safari to the IPA URL withitms-services://protocol - A Python HTTPS server (with mkcert TLS certs) serves everything on the ZeroTier IP
- Safari on the phone downloads and installs the app
The Script: scripts/ota-install.sh
We wrapped the entire flow into a single script:
./scripts/ota-install.sh
It handles:
- Extracting
MARKETING_VERSIONfromproject.pbxprojautomatically - Generating
ExportOptions.plistif missing - Building the archive and exporting the IPA
- Generating
manifest.plistandindex.htmlwith correct version and IP - Copying the mkcert root CA for first-time phone setup
- Generating a QR code (if
qrencodeis installed) - Starting the HTTPS server on
https://10.144.0.70:8443/
One-Time Phone Setup
Before the first OTA install, the phone needs to trust the self-signed TLS certificate:
- Download the mkcert root CA from the landing page
- Settings → Profile Downloaded → Install the profile
- Settings → General → About → Certificate Trust Settings → Toggle on the mkcert certificate
- After first app install: Settings → General → VPN & Device Management → Trust the developer profile
Result
The landing page loads over ZeroTier, the IPA downloads, and iOS installs the app. No USB, no same-WiFi requirement, works from anywhere with internet.
Trade-off: No debugger. This is install-only — you can't set breakpoints or see console output. For debugging, you still need USB or same-WiFi.
Summary
| Approach | Works? | Debugger? | Notes |
|----------|--------|-----------|-------|
| USB | Yes | Yes | Requires physical cable |
| Xcode WiFi (same network) | Yes | Yes | Both devices on same WiFi |
| Xcode WiFi over ZeroTier | No | - | iOS binds lockdownd to en0 only |
| pymobiledevice3 over ZeroTier | No | - | Same port 62078 binding issue |
| OTA install over ZeroTier | Yes | No | Safari install, no debugger |
Key Learnings
-
iOS restricts developer services to physical interfaces.
lockdownd/remotedusegetifaddrs()filtered toen0. This is a deliberate Apple security decision, not a bug. No amount of VPN configuration will change it on a non-jailbroken device. -
Bonjour proxy (
dns-sd -P) works for discovery but not connection. Xcode sees the device and even reports "Build Succeeded" when on the same network. But discovery ≠ connectivity — the underlying TCP connection still needs to reach port 62078. -
ZeroTier Default Route is a trap on iOS. Enabling it routes ZeroTier's own tunnel traffic through itself, breaking everything. Always leave it off on the phone.
-
OTA install is the practical solution for remote deployment. It's not as seamless as Cmd+R, but it works reliably over any IP path. The one-time cert setup takes 2 minutes.
-
mkcert makes self-signed TLS painless. The root CA installs once on the phone and all future certs are trusted automatically. No need for Let's Encrypt or real domains.
Files Created
scripts/ota-install.sh— One-command OTA build and serve~/Library/LaunchAgents/com.muratcakmak.xcode-zt-proxy.plist— Auto-start Bonjour proxy (useful when on same WiFi, harmless otherwise)/tmp/llmfriend-ota/— Staging directory for certs, archives, and served files