Skip to content

Latest commit

 

History

History
242 lines (174 loc) · 8.01 KB

File metadata and controls

242 lines (174 loc) · 8.01 KB

USB Device Connectivity

Connecting to iOS devices over USB via CoreDevice IPv6 tunnels, bypassing WiFi/mDNS discovery.

Overview

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.

How It Works

The CoreDevice IPv6 Tunnel

When an iOS device is connected via USB and recognized by Xcode/CoreDevice:

  1. CoreDevice creates a tunnel on a utun interface (typically utun5)
  2. IPv6 addresses are assigned:
    • Mac: fd9a:6190:eed7::2 (or similar ULA prefix)
    • Device: fd9a:6190:eed7::1
  3. TCP connections can be made directly to the device's IPv6 address

Automatic Discovery

Note: USBDeviceDiscovery (in the ButtonHeist framework) is defined but not currently wired into TheHandoff. USB devices are discovered via Bonjour over the CoreDevice IPv6 tunnel — no separate USB discovery step is needed.

The USBDeviceDiscovery class implements this flow:

  1. Polls xcrun devicectl list devices to find connected devices
  2. Parses lsof -i -P -n output to locate the CoreDevice IPv6 tunnel address
  3. Constructs an NWEndpoint with the IPv6 address and port
  4. Produces a DiscoveredDevice — identical to Bonjour-discovered devices

Port Discovery

TheInsideJob uses an OS-assigned port advertised via Bonjour. USB-connected devices are reachable on the same port via the CoreDevice IPv6 tunnel.

Requirements

  1. Device must be "connected" in devicectl (USB cable attached, trusted)
  2. TheInsideJob must use IPv6 dual-stack (enabled by default)
  3. App must be running on the device with TheInsideJob started
  4. Xcode command line tools installed (xcrun must be available)

Usage

CLI

# 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.png

MCP Server

Target a USB device in .mcp.json:

{
  "mcpServers": {
    "buttonheist": {
      "command": "./ButtonHeistMCP/.build/release/buttonheist-mcp",
      "args": ["--device", "iPhone 15 Pro"]
    }
  }
}

Building and Deploying to Device

Command Line Build

xcodebuild -workspace ButtonHeist.xcworkspace \
  -scheme BH Demo \
  -destination 'platform=iOS,name=Your Device Name' \
  -allowProvisioningUpdates \
  CODE_SIGN_STYLE=Automatic \
  DEVELOPMENT_TEAM=YOUR_TEAM_ID \
  build

Install to Device

xcrun 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.testapp

Discovering the Tunnel Manually

Check Device Status

xcrun devicectl list devices

Look for connected status:

Test Phone 15 Pro     Test-Phone-15-Pro.coredevice.local    ...   connected   iPhone 15 Pro

Find the IPv6 Tunnel Address

lsof -i -P -n | grep CoreDev | grep -oE '\[fd[0-9a-f:]+::[12]\]' | head -1

USB Discovery Flow

sequenceDiagram
    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
Loading

Manual Connection (for debugging)

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"}

Message Protocol

USB connections use the same wire protocol as WiFi. See the Wire Protocol Specification for message format, authentication flow, and command reference.

Implementation Details

IPv6 Dual-Stack Server

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

Why WiFi Might Fail

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

Troubleshooting

"Connection refused"

  • App not running or TheInsideJob not started
  • Wrong port (verify Info.plist has correct port)
  • Device went to sleep/background

"No route to host"

  • Device not connected or not trusted
  • CoreDevice tunnel not established
  • Check xcrun devicectl list devices for "connected" status

"Network is unreachable"

  • Wrong IPv6 prefix (check lsof -i -P -n | grep CoreDev)
  • Tunnel interface not up (reconnect USB cable)

USB device not appearing in buttonheist list

  • 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

Connection Scopes

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=usb

The 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: anpi interface (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.

See Also