Remote iOS Deployment Over ZeroTier: What Worked, What Didn’t

by Oguzhan Cakmakdevtoolsiosotaxcodezerotier

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.70 on private network cehalet_lan
  • iPhone 15 Pro Max: ZeroTier iOS app, IP 10.144.0.35
  • Network: Private ZeroTier network d5e5fb65372d78a9, both devices authorized
  • Xcode 16+ with devicectl available
  • 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 device picker showing iPhone via ZeroTier

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:

Xcode preparation error

"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 for en0utun interfaces (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 via uv tool install. Attempted lockdown start-tunnel --mobdev2 to create a RemoteXPC tunnel. Same failure: can't reach port 62078 over the ZT interface.
  • devicectl — Shows the phone as unavailable when not on local WiFi. No --address flag 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  │
              └────────┘
  1. xcodebuild archive + xcodebuild -exportArchive creates a dev-signed .ipa
  2. A manifest.plist points Safari to the IPA URL with itms-services:// protocol
  3. A Python HTTPS server (with mkcert TLS certs) serves everything on the ZeroTier IP
  4. 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_VERSION from project.pbxproj automatically
  • Generating ExportOptions.plist if missing
  • Building the archive and exporting the IPA
  • Generating manifest.plist and index.html with correct version and IP
  • Copying the mkcert root CA for first-time phone setup
  • Generating a QR code (if qrencode is 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:

  1. Download the mkcert root CA from the landing page
  2. Settings → Profile Downloaded → Install the profile
  3. Settings → General → About → Certificate Trust Settings → Toggle on the mkcert certificate
  4. 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

  1. iOS restricts developer services to physical interfaces. lockdownd/remoted use getifaddrs() filtered to en0. This is a deliberate Apple security decision, not a bug. No amount of VPN configuration will change it on a non-jailbroken device.

  2. 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.

  3. 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.

  4. 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.

  5. 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
← All notes