惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
T
The Blog of Author Tim Ferriss
Recent Announcements
Recent Announcements
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
The Register - Security
The Register - Security
MongoDB | Blog
MongoDB | Blog
U
Unit 42
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
P
Privacy International News Feed
L
LINUX DO - 最新话题
博客园_首页
博客园 - Franky
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
P
Privacy & Cybersecurity Law Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
K
Kaspersky official blog
C
Cisco Blogs
博客园 - 【当耐特】
阮一峰的网络日志
阮一峰的网络日志
I
Intezer
罗磊的独立博客
MyScale Blog
MyScale Blog
Last Week in AI
Last Week in AI
A
About on SuperTechFans
G
GRAHAM CLULEY
Y
Y Combinator Blog
Microsoft Security Blog
Microsoft Security Blog
GbyAI
GbyAI
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
D
DataBreaches.Net
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
I
InfoQ
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 叶小钗
Project Zero
Project Zero

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
How to remotely iterate & deploy your sideloaded iOS-apps over tailnet
Kevin Paters · 2026-05-19 · via DEV Community

Sideloading an iPhone over Tailscale from anywhere, no USB, Touch grass while CI/CD on iOS.

TL;DR

iOS 17+ moved iPhone development tooling onto a stack — CoreDevice + RemoteXPC + Bonjour — that's deliberately scoped to local-network adjacency. The official position is: USB-tether to the Mac, or have the iPhone on the Mac's Wi-Fi. There is no "remote install" knob in Xcode or xcrun devicectl.

I wanted a roaming pipeline: edit code on a Linux build host, sideload onto an iPhone that's on cellular two countries away, all over Tailscale. After a long detour into the wrong rabbit holes, the working architecture turned out to be a one-LaunchAgent combination of dns-sd -P Bonjour fabrication and socat TCP/UDP proxying. The whole bridge fits in ~50 lines of bash and works because every "structural" blocker is actually a layering choice — and each layer can be sidestepped without violating the layer above.

This post walks through the protocol stack, the wrong turns (instructive), and the actual working bridge.

The setup

  • Build host (Linux): edits the SwiftUI source, drives xcodebuild over SSH, holds the canonical project files. Reachable on a Tailscale tailnet (Headscale-coordinated). (I'm on an OCI free-tier shape running Claude Code - how to get your own)
  • Mac: only the build slave. Has Xcode, the signing identity, and runs xcodebuild / xcrun devicectl. On the same tailnet.
  • iPhone: a real iOS 26 device with a free Apple Developer Personal Team. On the same tailnet via the Tailscale iOS app. Wants to receive the built .ipa and run it.

The pipeline is edit on Linux → sync to Mac → xcodebuild archive + export → devicectl install → iPhone runs the app. The first four steps were already working over SSH/rsync; the last step is the focus here.

The iOS-17+ developer-services stack

For our purposes, the relevant protocol layers on an iPhone are:

  1. Bonjour / _remotepairing._tcp.local.: iPhone advertises a CoreDevice front-door service on TCP port 49152 via mDNS. Discovery is intentionally link-local: this is what makes Xcode "see" connected iPhones on the same Wi-Fi.
  2. RemoteServiceDiscovery (RSD): a small JSON/HTTP-2 service reachable at the advertised port that returns a manifest of available developer services (DebugProxy, AFC, InstallationProxy, etc.) and the dynamic ports they listen on.
  3. Trusted tunnel: once Mac and iPhone have RSD-handshaked, they establish a QUIC tunnel for the actual developer-services traffic. The tunnel listens on a dynamic port iPhone picks per session — we observed 55110-55115 across attempts on iOS 26.5.
  4. CoreDevice service multiplexer: AFC file transfers, installation_proxy, debug interfaces, etc. all ride the trusted tunnel.

The whole thing is gated on Bonjour announcing the iPhone to mDNSResponder on Mac, with remoted then driving the rest of the stack.

Why Tailscale (or any L3 VPN) breaks the default flow

Tailscale gives you a unicast IP route between any two devices in your tailnet, anywhere on the internet. That's all it gives you: no broadcast, no multicast, no L2 frames. Bonjour relies entirely on link-local multicast (224.0.0.251, TTL 1). iOS's mDNSResponder explicitly skips point-to-point interfaces (this is documented and there's nothing you can configure) — even if you somehow forced multicast onto the WireGuard tunnel, Apple wouldn't announce on it.

So a roaming iPhone on Tailscale is, from Mac's remoted viewpoint, invisible. xcrun devicectl list devices shows the iPhone as unavailable. No install possible.

The wrong turns

Bonjour reflectors

The obvious-looking answer is "run a Bonjour reflector". There are several: avahi-daemon --reflector, Gandem/bonjour-reflector, the Spiceworks community's Linux gateway recipe, even Palo Alto firewalls. They all work the same way: a daemon sits on a host with NICs in two L2 segments and rebroadcasts mDNS packets between them.

None of these apply to Tailscale. Reflectors operate at the L2 frame layer (gopacket+pcap+802.1Q tags, or at minimum a tcpdump-able interface receiving multicast). tailscale0 is a userspace WireGuard tunnel exposing IP packets only — there are no Ethernet frames to capture, no broadcast domain on the tunnel side. The packets the reflectors need to relay don't exist on the tunnel side.

The only way to push mDNS across a WireGuard tunnel is to wrap the tunnel in an L2 overlay (VXLAN, GRETAP, L2TPv3 inside WireGuard). The iPhone cannot participate in such an overlay — there's no API for it without jailbreaking — so this is a dead end for roaming.

Direct lockdownd on TCP 62078 (the legacy protocol)

Before iOS 17, iPhones spoke plain TLS-wrapped property-list messages on TCP 62078 via usbmuxd. The protocol is still present on the device. libimobiledevice / ideviceinstaller and pymobiledevice3's LockdownClient both speak it. You'd think: just skip Bonjour, dial iphone-tailnet-ip:62078 directly with a stored pair record, ask installation_proxy to install the IPA.

I tried this. TCP 62078 accepts the connection, then resets on the first byte of any plausible protocol (length-prefixed XML plist, HTTP/2, HTTP/1.1, raw TLS). I sent literal zero bytes — connection idled, then closed cleanly. Sent anything meaningful — instant RST. Apple appears to have tightened the network-side lockdownd listener in iOS 17+ to require an active CoreDevice trusted tunnel before serving anything. The legacy protocol is effectively no longer exposed over Wi-Fi/tailnet.

pymobiledevice3 apps install --rsd HOST PORT

pymobiledevice3's CLI accepts --rsd HOST PORT for a direct RSD endpoint. I tried --rsd 100.64.0.5 58783 (the documented "hardcoded" RSD port) — refused. --rsd 100.64.0.5 49152 — TCP open but resets on handshake (wrong protocol — that port is RemotePairing, not RSD; the actual RSD port is allocated dynamically and announced via Bonjour TXT records we can't see over tailnet).

This is the recursive trap: to talk to RSD you need its dynamic port; to know the port you need Bonjour; we don't have Bonjour over tailnet.

Naive Bonjour spoof on Mac

The promising-looking idea: dns-sd -P lets you register fake Bonjour records into Mac's own mDNSResponder. So advertise iPhone's services locally on Mac with the hostname targeting iPhone's tailnet IP. remoted browses Bonjour, sees the announcement, connects to the iPhone over tailnet. Apple's mDNS API can't tell whether a record came from a real device on the LAN or from a local dns-sd -P registration.

This almost works. Discovery succeeds — xcrun devicectl list devices shows the iPhone as available (paired). But devicectl install fails. The reason, from log show --predicate 'process == "remotepairingd"':

[C905.1.1 Hostname#... interface: en0[802.11], scoped, ipv4, dns, uses wifi, ...]
nw_socket_connect connectx [C905:2] connectx(..., [srcif=14, ...]) failed: [61: Connection refused]

Enter fullscreen mode Exit fullscreen mode

remotepairingd scopes the outgoing TCP connection to the interface that announced the Bonjour record — en0 (Mac's Wi-Fi). It's calling nw_parameters_set_required_interface somewhere internal. So when our spoof points the hostname at 100.64.0.5 (iPhone's tailnet IP), the SYN goes out en0 — which has no route to 100.64.0.5 — into nothing. Refused or timed out.

This is by design: Bonjour is a same-link discovery system, and CoreDevice trusts that property by binding to the link. Excellent engineering for the home-network case; a wall for our roaming case.

I tried scoping the dns-sd -P to the Tailscale interface (-i utun4) — registration succeeds but the records are invisible because utun4 is point-to-point and mDNSResponder doesn't browse on it. Confirms Apple's own constraint that we can't sneak around.

I also confused myself by thinking the trusted tunnel listener was bound only to the USB-Ethernet IPv6 interface (because TCP probes to the dynamic port over tailnet were refused). They were refused because the trusted tunnel is QUIC over UDP, not TCP. The same UDP port responded fine on a UDP probe (nc -vzu). That oversight cost a few hours.

The actual working architecture

Once both real facts were on the table — remotepairingd scopes to the discovery interface, and iPhone's trusted-tunnel listener IS reachable on tailnet (UDP) — the answer falls out:

Spoof Bonjour pointing the iPhone's hostname to Mac's own en0 IP, and run a local proxy that bridges to the iPhone over Tailscale.

remotepairingd is satisfied: the discovered service appears reachable on the discovery interface (en0), so its scoped connection lands on a port that's actually listening — on Mac itself. The proxy then forwards the bytes to the iPhone via Tailscale. The CoreDevice handshake and TLS are end-to-end between remotepairingd and the iPhone; the proxy is a dumb byte relay, never inspecting or terminating TLS. Apple's interface-scoping invariant is honored at the OS level; the bridging happens at the application level above it.

Components

The Mac runs a single LaunchAgent (com.example.coredevice-tailnet) that does three things in parallel:

  1. Bonjour spoof. dns-sd -P registers three records into mDNSResponder:
    • _remotepairing._tcp (instance UUID + TXT pulled from the iPhone via one-time USB capture)
    • _remoted._tcp
    • _apple-mobdev2._tcp

All target the hostname My-iPhone.local (the iPhone's actual self-reported .local name). dns-sd -P's integrated A/AAAA registration resolves that hostname to Mac's own en0 IP — e.g. 192.168.1.190. /etc/hosts mirrors that as belt-and-braces.

  1. RemotePairing proxy. A socat instance binding en0_ip:49152 and forwarding to iphone_tailnet_ip:49152 for both TCP and UDP. This is the CoreDevice front door — the first thing remotepairingd connects to after discovering the spoofed Bonjour record.

  2. Trusted-tunnel port-range proxy. A range of socat instances binding en0_ip:55000-55300 and forwarding to the corresponding port on the iPhone's tailnet IP, both protocols. iPhone allocates its trusted-tunnel listener on a fresh port per pairing session (we observed 55110-55115); the range covers comfortably.

The script:

#!/bin/bash
set -euo pipefail

IPHONE_TAILNET_IP="100.64.0.5"
SPOOF_HOSTNAME="My-iPhone.local"
MAC_EN0_IP="$(ifconfig en0 | awk '/inet / {print $2; exit}')"

dns-sd -P "$RP_INSTANCE" _remotepairing._tcp local 49152 \
    "$SPOOF_HOSTNAME" "$MAC_EN0_IP" \
    identifier="$RP_INSTANCE" authTag="$RP_AUTHTAG" ver=24 minVer=8 flags=0 &

socat TCP-LISTEN:49152,bind="$MAC_EN0_IP",reuseaddr,fork TCP:"$IPHONE_TAILNET_IP":49152 &
socat UDP-LISTEN:49152,bind="$MAC_EN0_IP",reuseaddr,fork UDP:"$IPHONE_TAILNET_IP":49152 &

for port in $(seq 55000 55300); do
    socat TCP-LISTEN:"$port",bind="$MAC_EN0_IP",reuseaddr,fork TCP:"$IPHONE_TAILNET_IP":"$port" &
    socat UDP-LISTEN:"$port",bind="$MAC_EN0_IP",reuseaddr,fork UDP:"$IPHONE_TAILNET_IP":"$port" &
done
wait

Enter fullscreen mode Exit fullscreen mode

Wrapped in a LaunchAgent with KeepAlive: { SuccessfulExit: false, NetworkState: true } so it restarts on network changes; ThrottleInterval: 30s, NumberOfFiles: 4096 for the open-file headroom.

Capturing the Bonjour TXT data

The $RP_INSTANCE UUID and $RP_AUTHTAG are pulled from iPhone's real Bonjour announcement via a one-time USB capture:

# iPhone plugged via USB, Mac sees its native Bonjour announcement on
# the USB-Ethernet interface
dns-sd -B _remotepairing._tcp local.
# note instance UUID
dns-sd -L "<instance>" _remotepairing._tcp local.
# note port (49152), target hostname, and TXT record (identifier, authTag, ver, minVer, flags)

Enter fullscreen mode Exit fullscreen mode

The values went straight into the script. iPhone rotates the UUID/authTag per Wi-Fi session — we observed at least one rotation. So far the stale captured values keep passing remoted's pairing handshake in our tests, because CoreDevice auth is certificate-based (the pair record) rather than TXT-based. If a future iOS tightens this, we'd add a pymobiledevice3 lockdown save-pair-record + dns-sd capture step to the runbook.

Verification

iPhone unplugged from USB, sitting on a personal hotspot's Wi-Fi (different SSID, different LAN, Mac not on that network), Tailscale active. From the Linux build host:

$ make sideload
...
ARCHIVE SUCCEEDED
EXPORT SUCCEEDED
[sideload] preflight checks...
  ok: coredevice-tailnet LaunchAgent running
[sideload] device 09C62718-... state=available (paired)
00:22:06  Acquired tunnel connection to device.
00:22:06  Enabling developer disk image services.
00:22:07  Acquired usage assertion.
App installed:
• bundleID: com.example.hellotest

Enter fullscreen mode Exit fullscreen mode

The Acquired tunnel connection to device is the moneymaker: that's remoted completing the CoreDevice handshake over Tailscale via the local proxy. From there everything else (DDI services, install) just works.

Why this is robust

Every layer here uses Apple's own APIs as documented:

  • dns-sd -P is the documented "proxy registration" mode of DNSServiceRegister with kDNSServiceFlagsProxy. macOS treats the records exactly as if they came from a real Bonjour announcer on the LAN.
  • socat is byte-relaying TCP/UDP; nothing private about it.
  • remotepairingd's interface-scoping invariant is preserved. It connects locally on en0 because that's where it discovered the service.
  • The proxy never inspects, terminates, or modifies TLS. CoreDevice's certificate-based pairing auth runs end-to-end remoted ↔ iPhone exactly as it would over USB or LAN.

No kernel extensions. No PF rules. No mDNS daemon patching. No SIP-disabled tricks. No jailbreak. No modification of Apple binaries. The whole thing is a user-mode LaunchAgent and three CLI tools that ship with macOS + Homebrew (socat).

Known fragilities

  1. Bonjour record rotation. If iPhone rotates the _remotepairing._tcp UUID or authTag and Apple eventually checks them, the captured values need refreshing. Mitigation: USB-plug for 30 seconds + recapture.
  2. Mac en0 IP changes. When DHCP gives Mac a new LAN IP, restart the LaunchAgent (it reads en0 IP at startup) and update /etc/hosts. Could be scripted with a small sudoers-permitted helper.
  3. Tunnel port range. Observed 55110-55115; covered 55000-55300. If Apple ever shifts the allocator out of this window, expand.
  4. DDI staging. First USB pair stages the personalized Developer Disk Image on iPhone. Sticky until iPhone reboot, after which a brief USB re-tether is required to re-stage. (This is the only reason USB isn't permanently absent from the workflow.)
  5. Free Personal Team 7-day cert expiry. Orthogonal — re-sideload weekly. The Tailscale path makes re-sideloads cost ~10 seconds with no physical access.

  6. iPhone must be associated to a Wi-Fi SSID — radio-on-no-association is not sufficient. Confirmed by testing: Wi-Fi off entirely → all CoreDevice ports refused, listener not bound. Wi-Fi radio on but not joined to any SSID (auto-connect disabled, manually toggled) → same outcome, ports refused. Wi-Fi joined to any SSID (tethered phone hotspot, captive Wi-Fi, foreign Wi-Fi) → listener binds, install works over Tailscale.

This is consistent with Apple's broader pattern: developer services hang off the same WiFiManager association gate that governs AirDrop, Handoff, and Continuity activation. The framework distinguishes between radio-on-scanning and associated-to-network as separate states; subsystems including remotepairingd key off the latter. Tailscale's utun interface doesn't count — only the real Wi-Fi association flips the bit.

Practical impact is small. The SSID need not provide internet, need not reach Mac directly, and need not be trusted — Tailscale carries the actual transport. Any joined Wi-Fi (including tethering off a second phone's hotspot) suffices. The genuine pure-cellular case where no Wi-Fi exists anywhere is rare enough that this isn't a deal-breaker; in practice the iPhone has some Wi-Fi network within reach, and that's enough.

Apple's own mDNSResponder documentation (code-comments) suggest their reason for restricting multi-cast to Wifi is to reduce cellular data bills. Early 2010s thinking.

// We only attempt to send and receive multicast packets on interfaces that are
// (a) flagged as multicast-capable
// (b) *not* flagged as point-to-point (e.g. modem)
// Typically point-to-point interfaces are modems (including mobile-phone pseudo-modems), and we don't want
// to run up the user's bill sending multicast traffic over a link where there's only a single device at the
// other end, and that device (e.g. a modem bank) is probably not answering Multicast DNS queries anyway.

Enter fullscreen mode Exit fullscreen mode

Commit [Source]

Why publish this

There are a lot of "you can't sideload from outside the LAN" results in the iOS-dev community, and they're all true relative to the official tools. The structural blockers people cite (mDNSResponder skips point-to-point interfaces, CoreDevice scopes to the discovery link, RSD ports are dynamic, etc.) are all real. But the conclusion that follows — "therefore you can't" — only holds if you accept the layering.

The trick that opens it is letting the OS-level invariants stand, and bridging at the layer above. remotepairingd thinks it's talking to a Bonjour-announced peer on en0. It is — that peer just happens to forward bytes onward over Tailscale. Apple's invariants are preserved on the Mac side; on the wire, what matters is end-to-end TLS to a real iPhone, and Tailscale carries that fine.

Three things made this work:

  1. Reading the syslog (log show --predicate 'process == "remotepairingd"') and seeing the connectx... [Connection refused] with srcif=14, scoped, ipv4, dns, uses wifi — the smoking gun for interface scoping.
  2. Re-reading the port probes carefully: UDP-succeeded means the listener exists, even if TCP refused on the same port. The trusted tunnel is QUIC.
  3. An external reviewer who looked at the data and suggested "what if the proxy isn't a remote interceptor but a local destination?" That reframe turned a research project into a working pipeline.

For the next person hitting this wall: read log show on remotepairingd and look at what interface the connection scopes to. The answer is in that line.