Connecting to iOS devices over USB via CoreDevice IPv6 tunnels, bypassing WiFi/mDNS discovery.
When WiFi is unreliable (VPN interference, network segmentation, mDNS issues), ButtonHeist automatically discovers USB-connected devices alongside WiFi devices. No extra flags or scripts needed — buttonheist list shows both.
USB discovery uses the same protocol as WiFi. The only difference is how the endpoint is found: Bonjour for WiFi, CoreDevice IPv6 tunnel for USB.
When an iOS device is connected via USB and recognized by Xcode/CoreDevice:
- CoreDevice creates a tunnel on a
utuninterface (typicallyutun5) - IPv6 addresses are assigned:
- Mac:
fd9a:6190:eed7::2(or similar ULA prefix) - Device:
fd9a:6190:eed7::1
- Mac:
- TCP connections can be made directly to the device's IPv6 address
Note:
USBDeviceDiscovery(in the ButtonHeist framework) is defined but not currently wired intoTheHandoff. USB devices are discovered via Bonjour over the CoreDevice IPv6 tunnel — no separate USB discovery step is needed.
The USBDeviceDiscovery class implements this flow:
- Polls
xcrun devicectl list devicesto find connected devices - Parses
lsof -i -P -noutput to locate the CoreDevice IPv6 tunnel address - Constructs an
NWEndpointwith the IPv6 address and port - Produces a
DiscoveredDevice— identical to Bonjour-discovered devices
TheInsideJob uses an OS-assigned port advertised via Bonjour. USB-connected devices are reachable on the same port via the CoreDevice IPv6 tunnel.
- Device must be "connected" in devicectl (USB cable attached, trusted)
- TheInsideJob must use IPv6 dual-stack (enabled by default)
- App must be running on the device with TheInsideJob started
- Xcode command line tools installed (
xcrunmust be available)
# List all devices (WiFi and USB appear together)
buttonheist list
# Connect to a USB device by name
buttonheist --device "iPhone 15 Pro" activate --identifier myButton
# Take a screenshot over USB
buttonheist --device "iPhone 15 Pro" screenshot --output screen.pngTarget a USB device in .mcp.json:
{
"mcpServers": {
"buttonheist": {
"command": "./ButtonHeistMCP/.build/release/buttonheist-mcp",
"args": ["--device", "iPhone 15 Pro"]
}
}
}xcodebuild -workspace ButtonHeist.xcworkspace \
-scheme BH Demo \
-destination 'platform=iOS,name=Your Device Name' \
-allowProvisioningUpdates \
CODE_SIGN_STYLE=Automatic \
DEVELOPMENT_TEAM=YOUR_TEAM_ID \
buildxcrun devicectl device install app \
--device "Your Device Name" \
~/Library/Developer/Xcode/DerivedData/ButtonHeist-*/Build/Products/Debug-iphoneos/BH Demo.app
xcrun devicectl device process launch \
--device "Your Device Name" \
--terminate-existing --activate \
com.buttonheist.testappxcrun devicectl list devicesLook for connected status:
Test Phone 15 Pro Test-Phone-15-Pro.coredevice.local ... connected iPhone 15 Pro
lsof -i -P -n | grep CoreDev | grep -oE '\[fd[0-9a-f:]+::[12]\]' | head -1sequenceDiagram
participant USB as USBDeviceDiscovery
participant XC as xcrun devicectl
participant LS as lsof
participant EV as onEvent callback
loop Every 3 seconds (poll)
USB->>XC: discoverConnectedDevices()<br>xcrun devicectl list devices
XC-->>USB: Device names with "connected" status
USB->>LS: findIPv6Tunnel()<br>lsof -i -P -n
LS-->>USB: CoreDevice fd-prefix IPv6 address
alt New device found
USB->>USB: Construct NWEndpoint<br>hostPort(ipv6Address, port)
USB->>USB: Create DiscoveredDevice<br>id: "usb-{name}"
USB->>EV: .found(device)
else Device disappeared
USB->>EV: .lost(device)
end
end
Note: The protocol requires token authentication before any commands are accepted.
# Using netcat (must authenticate first)
nc -6 "fd9a:6190:eed7::1" <port> # use port from `buttonheist list --format json`
# Server sends: {"protocolVersion":"6.1","requestId":null,"type":"serverHello"}
# Send: {"protocolVersion":"6.1","requestId":null,"type":"clientHello"}
# Server sends: {"protocolVersion":"6.1","requestId":null,"type":"authRequired"}
# Send: {"protocolVersion":"6.1","requestId":null,"type":"authenticate","payload":{"token":"your-token"}}
# Server sends: {"protocolVersion":"6.1","requestId":null,"type":"info","payload":{...}}
# Send: {"protocolVersion":"6.1","requestId":null,"type":"requestInterface"}USB connections use the same wire protocol as WiFi. See the Wire Protocol Specification for message format, authentication flow, and command reference.
The ServerTransport uses Network framework (NWListener) with IPv6 dual-stack to accept both IPv4 and IPv6 connections:
// Network framework listener
let parameters = NWParameters.tcp
let host: NWEndpoint.Host = bindToLoopback ? .ipv6(.loopback) : .ipv6(.any)
parameters.requiredLocalEndpoint = .hostPort(host: host, port: NWEndpoint.Port(rawValue: port)!)
let listener = try NWListener(using: parameters)On simulators, the server binds to loopback only (::1) by default. On physical devices, it binds to all interfaces (::) to accept USB tunnel connections. The bind address is controlled by the bindToLoopback parameter on ServerTransport.start().
This allows:
- Simulator connections via
127.0.0.1(loopback) - USB device connections via the CoreDevice IPv6 tunnel
- WiFi connections via local network IPv4/IPv6
Common issues that USB bypasses:
- VPN routing: VPN may route local traffic through tunnel
- mDNS blocking: Bonjour discovery blocked or filtered
- Network segmentation: Device on different subnet
- Firewall rules: Port blocked on WiFi interface
- App not running or TheInsideJob not started
- Wrong port (verify Info.plist has correct port)
- Device went to sleep/background
- Device not connected or not trusted
- CoreDevice tunnel not established
- Check
xcrun devicectl list devicesfor "connected" status
- Wrong IPv6 prefix (check
lsof -i -P -n | grep CoreDev) - Tunnel interface not up (reconnect USB cable)
- Verify device shows as "connected" in
xcrun devicectl list devices - Ensure app is running on the device
- USB discovery polls every 3 seconds — wait a moment
By default, TheInsideJob only accepts connections from simulators (loopback) and USB devices. Network/WiFi connections are rejected unless explicitly allowed via the INSIDEJOB_SCOPE environment variable:
# Default: simulator and USB only
INSIDEJOB_SCOPE=simulator,usb
# Also allow WiFi/LAN connections
INSIDEJOB_SCOPE=simulator,usb,network
# USB only (reject simulator and WiFi)
INSIDEJOB_SCOPE=usbThe scope filter classifies connections once they reach the .ready state, using typed NWEndpoint.Host values and interface detection:
- simulator: Loopback (
::1,127.0.0.1) - usb:
anpiinterface (Apple Network Private Interface — CoreDevice USB tunnel) - network: Everything else (WiFi, LAN, public)
Connections from disallowed scopes are rejected at the .ready state, before authentication.
- Wire Protocol — Message format (identical over WiFi and USB)
- Project Overview — Architecture and quick start
- CLI Reference — All commands work over USB