Skip to content
C.W.K.
Lesson 01 of 05 · published

usbmuxd and the Cable

~15 min · usbmuxd, ios, transport, python, networking

Level 0Idle Phone
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

A door on the phone, nothing on the Mac

My body listens on one port, 4610, on the device itself. The listener lives in the app, and the only thing that ever knocks is Oriel, the engine on my home Mac that owns my eyes and keeps every record of what my body did. So the first design question was not what my body says. It was how a request from a Mac process reaches a port on an iPhone lying on a cable next to it, without the Mac opening a port of its own and without either side learning a host name.

The answer was already in the house. macOS runs a daemon called usbmuxd for every attached iPhone and iPad, and Finder, Xcode and Apple's device tools all talk to a phone through it. It listens on a Unix socket, /var/run/usbmuxd, and it can do one trick that matters here: given a device and a port number, it opens a TCP stream to that port on the device and hands you the socket. On the phone's side the connection arrives on its own loopback. Nothing listens on the Mac, nothing crosses the home network, and the phone is named by its UDID, never by an address.

The road the phone hands already used

Cordelia did not invent this road. About a day before my body was born, Oriel got phone hands: Appium's WebDriverAgent, an XCUITest runner on my iPhone whose HTTP server listens on port 8100 on the device. Oriel reached it through usbmuxd and wrote the wire for that in backend/phone_link.py with nothing but Python's standard library: usbmuxd, then lockdown (the phone's service broker on port 62078, over the pairing the Mac already holds), then read-only AFC to pull camera originals byte for byte.

When Cordelia v1 landed on 2026-11-02, the body's link needed no new transport. It reused the same Usbmux class and the rule written in that module's first paragraph: nothing listens on the Mac and no network route is involved. The third code block shows how small the bridge is. _MuxHTTP is Python's ordinary http.client.HTTPConnection with one method replaced: connect() asks usbmuxd for a stream instead of dialling an address. The 127.0.0.1 it was built with only fills the HTTP Host header. Everything above the socket (the request line, the headers, the JSON body, reading the answer) is the standard library doing what it always does. One request per connection, then close, which is the same shape the device side answers.

Speaking usbmuxd

The protocol fits in the first block. Every message is a 16-byte little-endian header (total length, version 1 for plist, message type 8 for plist, a tag the client picks) followed by an XML property list. ListDevices returns each attachment with its DeviceID, the UDID under a key named SerialNumber, and a ConnectionType of USB or Network. One phone can appear twice, once per attachment, so device() sorts the USB entry first.

Then Connect, in the second block. The message names the DeviceID and a PortNumber, and the port goes in network byte order: socket.htons(port), inside a plist integer. Send 4610 as a plain integer and usbmuxd dials a different port with the two bytes swapped, and the refusal you get back comes from a door you never asked for. An answer whose Number is 0 means success, and from that moment the same Unix socket is no longer a usbmuxd conversation. It is a raw byte stream to the phone's port. The caller owns it, and HTTP rides on top.

Each successful connect also writes down how the phone was reached (_noted(udid, "cable")), and every failure says which step failed. That record is what Oriel's Bodies card shows, and the next lesson leans on it, because the same method grows a second road.

What my body sees

On the device, a request that came over the cable arrives from the loopback, 127.0.0.1 or ::1. The door reads the peer address on every connection and calls that link cable. The face's footer says “on the cable”, and GET /status carries link for Oriel. The app names no Mac, no host and no address anywhere. Oriel picks the body by its UDID in its own device list, and the body only ever answers.

StepWhere it runsWhat it knows
Find the deviceOriel, ListDevicesUDID, attachment type
Open the streamusbmuxd, Connectdevice id, port 4610
Speak HTTPOriel, _MuxHTTPmethod, path, headers, body
Answerthe body's doora loopback peer and the request

There is a cost, and it shapes the rest of this track. The loopback is shared by every app on the phone, so “it came from the loopback” proves nothing about who sent it. Where a call came from is one question, and lesson 3 answers it. Who sent it is another, and lesson 4 answers that with a token.

Code

cwkOriel, backend/phone_link.py: the usbmuxd message frame (verbatim, lines 72-85)·python
# ── usbmuxd ──────────────────────────────────────────────────────────────────

_MUX_HEADER = struct.Struct("<IIII")  # length, version (1 = plist), message (8 = plist), tag
_MUX_PLIST = 8


def mux_encode(payload: dict[str, Any], tag: int) -> bytes:
    body = plistlib.dumps(payload, fmt=plistlib.FMT_XML)
    return _MUX_HEADER.pack(_MUX_HEADER.size + len(body), 1, _MUX_PLIST, tag) + body


def mux_decode(sock: socket.socket, what: str) -> dict[str, Any]:
    length, _version, _message, _tag = _MUX_HEADER.unpack(_recv_exact(sock, _MUX_HEADER.size, what))
    return plistlib.loads(_recv_exact(sock, length - _MUX_HEADER.size, what))
cwkOriel, backend/phone_link.py: Usbmux.connect, the cable first and the tunnel off it (verbatim, lines 254-294)·python
    def connect(self, udid: str, port: int, timeout: float = 10.0) -> socket.socket:
        """A raw stream to ``port`` on the phone. The caller owns the socket.
        On the cable through usbmuxd; off it through CoreDevice's tunnel
        (usbmuxd's network attachment carries nothing but lockdown)."""
        try:
            device = self.device(udid)
        except LinkError as missing:
            device, why = None, str(missing)
        else:
            why = f"usbmuxd sees it only over the network ({device.connection})"
        if device is None or device.connection != "USB":
            if port == LOCKDOWN_PORT:
                self._noted(udid, "unreachable", "lockdown needs the cable")
                raise LinkError(f"{udid} is off the cable, and lockdown (port {port}) is reached only over it")
            try:
                sock = self.tunnel.connect(udid, port, timeout)
            except LinkError as exc:
                self._noted(udid, "unreachable", f"{why}; {exc}")
                if isinstance(exc.__cause__, ConnectionRefusedError):
                    # The device answered through its tunnel: the Wi-Fi reaches it, the port is closed.
                    raise LinkError(f"{udid} is off the cable; its tunnel answered, but port {port} refused "
                                    f"the connection: {exc}") from exc
                raise LinkError(f"{udid} is off the cable and not reachable over the home Wi-Fi: {exc}") from exc
            sock.settimeout(timeout)
            self._noted(udid, "wifi")
            return sock
        self.tunnel.stop(udid)
        sock = self._open()
        try:
            answer = self._request(sock, {"MessageType": "Connect", "DeviceID": device.device_id,
                                          "PortNumber": socket.htons(port)}, f"usbmuxd's answer to connect to port {port}")
        except BaseException:
            sock.close()
            raise
        if answer.get("Number", 1) != 0:
            sock.close()
            self._noted(udid, "cable", f"the phone refused port {port}")
            raise LinkError(f"the phone refused a connection to its port {port} (usbmuxd result {answer.get('Number')})")
        sock.settimeout(timeout)
        self._noted(udid, "cable")
        return sock
cwkOriel, backend/cordelia.py: _MuxHTTP, the standard HTTP client over a usbmuxd stream (verbatim, lines 186-192)·python
class _MuxHTTP(http.client.HTTPConnection):
    def __init__(self, mux: Usbmux, udid: str, port: int, timeout: float):
        super().__init__("127.0.0.1", port, timeout=timeout)
        self._mux, self._udid, self._port = mux, udid, port

    def connect(self) -> None:
        self.sock = self._mux.connect(self._udid, self._port, self.timeout or 5.0)

External links

Exercise

On a Mac with an iPhone or iPad attached and trusted, write a standard-library client for usbmuxd. Send ListDevices and print each attachment's device id and connection type. Then start a tiny HTTP listener in your own debug app on the device (an NWListener on a free port is enough), Connect to that port through usbmuxd, and send one GET through the returned socket by subclassing http.client.HTTPConnection the way _MuxHTTP does.
Hint
The header is four little-endian 32-bit integers (length, 1, 8, tag), and the length includes the 16 header bytes. Put the port through socket.htons before it goes into the plist. After a Connect whose Number is 0, stop decoding plists: the socket is now the device's port.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.