Skip to content

v11.3.0

Latest

Choose a tag to compare

@doronz88 doronz88 released this 31 Aug 08:18
ec4ac06

Highlights

✨ Reach a tunneld you have no route to

tunneld only ever reported tunnel addresses: GET / hands back something like
fdba:ae68:c15f::1:63727, and the client was expected to dial it. That works only when the tunnel
interface lives on the client's own host, so the moment tunneld runs somewhere else — in a docker
container while the client is on the host, or on a device-lab machine while the client is a CI
worker — the reported address is unreachable and the API is useless to that client.

The new WS /connect endpoint (#1893, @cyclomati) bridges into the tunnel through the HTTP API
instead, and pymobiledevice3 now speaks it itself, so every command works against a remote
tunneld with no extra tooling:

# on the machine with the device attached
sudo pymobiledevice3 remote tunneld --host 0.0.0.0

# anywhere that can reach it — IPv6 hosts go in brackets
pymobiledevice3 developer dvt ls / --tunnel 'UDID@lab-mac:49151'

Every connection the RSD opens — the RemoteXPC handshake and each service — is carried over its own
websocket. Library consumers get the same from get_tunneld_devices(('lab-mac', 49151)) and
get_tunneld_device_by_udid(...): bridging turns on automatically whenever the tunneld host is
non-loopback, and bridge=True forces it for addresses that only look local, such as an SSH
port-forward (ssh -L 49151:127.0.0.1:49151 lab-mac, reached on the CLI as --tunnel UDID@127.0.0.1).

✨ Front an entire device lab behind one tunneld

A tunneld could already federate others' listings, but the addresses in those listings are ULAs on
the upstream host's utun, reachable only from a client that already had L3 connectivity there — so
federation quietly presupposed a VPN or hand-rolled routes, and a device discovered through an
aggregator was a dead end.

Now a /connect for a device an instance does not serve itself is relayed to the upstream that owns
it, so clients need a route to one tunneld and nothing else:

# on every host with devices attached
sudo pymobiledevice3 remote tunneld --host 0.0.0.0

# on the one host clients can reach; monitors off so it federates rather than
# competing for devices attached here
sudo pymobiledevice3 remote tunneld --host 0.0.0.0 \
    --no-usb --no-wifi --no-usbmux --no-mobdev2 \
    --upstream http://lab-1:49151 --upstream http://lab-2:49151

# from anywhere: any device in the lab, addressed through the front tunneld
pymobiledevice3 developer dvt ls / --tunnel 'UDID@front:49151'

--upstream is repeatable and seeds the same set POST /upstream manages at runtime. Listing
entries now carry an originnull for devices the queried instance serves directly, otherwise
the URL of the hop that reported them — so a client that can reach that upstream may skip the
relay. An upstream's close codes are passed through unchanged, so a 4404/4502 names the
tunneld that actually refused rather than collapsing into a generic failure.

As with the rest of the tunneld HTTP API, /connect is unauthenticated, and an aggregator relays
into other hosts' devices — bind non-loopback addresses on trusted networks only.

💥 tunneld --uds has been removed

--uds only ever moved the HTTP control channel onto a unix socket, while the data plane — the RSD
dials to the reported tunnel-address:tunnel-port, and now the /connect bridge — always relied on
TCP, so it never delivered a pure-unix path. The option is gone, TunneldAddress narrows to
(host, port), and a --tunnel value with a non-numeric :suffix now fails with a message naming
the removal instead of being silently treated as a socket path. Use --host/--port and the
matching UDID:PORT form.

🐛 Federation no longer drops the listings it was meant to merge

Tunnelds that register each other (A → B → A) recursed on every GET /: the nesting spawned a
growing tree of in-flight requests against the bounded to_thread executor, and the innermost
fetches lost the race against their parents' 2s timeout. With two mutually registered instances,
each one's listing was missing the other's device — the exact thing federation exists to provide.

Federated requests now carry x-tunneld-hops-remaining (4 by default), decremented per hop and not
forwarded at zero, and a tunnel reachable through several paths is listed once instead of once per
path.

🐛 /connect addresses a tunnel, not just a device

A device can have more than one tunnel — a front tunneld that also monitors the same host holds
its own alongside the federated one. Keying on the UDID alone paired whichever tunnel was found
locally with the port the caller asked for, dialing an endpoint that exists nowhere:

4502 failed to connect to [fdc9:ec7b:4fdf::1]:63727: [Errno 61] Connect call failed

/connect now takes an optional ?address= naming the tunnel the caller means; a local tunnel is
used only when it matches, and otherwise the request falls through to the upstream serving that
exact tunnel. Older tunnelds ignore the parameter and keep their UDID-only behaviour.

🐛 Upstream URLs that could never work are rejected at registration

Federation reads an upstream address twice — requests fetches the listing from the URL, while the
relay dials the host and port parsed out of it — and the two only agreed on the exact spelling
http://HOST:PORT. A schemeless lab-1:49151 parses as scheme lab-1 with no host, so the listing
fetch threw InvalidSchema into a debug log and the relay refused: registering one did nothing at
all, silently. An https:// upstream fetched its listing over TLS but was relayed to in plaintext,
on port 80 when no port was given.

HOST, HOST:PORT and http://HOST[:PORT] (IPv6 in brackets) are now all accepted and stored
canonically with tunneld's default port filled in, and anything neither path could act on fails
loudly — a usage error from --upstream, a 400 from POST /upstream. https:// is rejected
outright rather than half-supported, since the relay speaks plaintext.

✨ XDG Base Directory Specification on Linux

Fresh Linux installations now place pymobiledevice3's home folder at $XDG_DATA_HOME/pymobiledevice3,
defaulting to ~/.local/share/pymobiledevice3 when the variable is unset or relative, as the spec
requires. An existing ~/.pymobiledevice3 is preferred and keeps being used, so current setups are
unaffected; macOS and Windows are unchanged, and WSL inherits the Linux behavior. Intermediate
directories created under sudo are chowned back to the invoking user. Fixes #1601.

What's Changed

New Contributors

Full Changelog: v11.2.4...v11.3.0