Skip to content

Upstream pr/tvos support - #226

Open
andrew-harness wants to merge 6 commits into
claration:mainfrom
andrew-harness:upstream-pr/tvos-support
Open

Upstream pr/tvos support#226
andrew-harness wants to merge 6 commits into
claration:mainfrom
andrew-harness:upstream-pr/tvos-support

Conversation

@andrew-harness

Copy link
Copy Markdown

Implements #155, sideloading to an Apple TV over the network.

An Apple TV has no USB port, so it's reached over the network using Apple's own
RemotePairing/CoreDevice path: mDNS discovery, SRP pairing against the PIN shown on screen, a
TLS-PSK tunnel, RSD service discovery, then the same InstallationProxy install the USB path
already uses.

Verified on hardware

Apple TV 14,1 on tvOS 26.5, driven from Windows. Discovery, first-time pairing, reconnect, tunnel,
device registration, provisioning, signing, upload and install. The installed app shows up on the
home screen and launches and runs, which is the part that matters: a wrongly signed or wrongly
provisioned bundle installs fine and then refuses to launch.

Commits

Six commits, each one is a self-contained layer:

  1. fix: write zip entry names with forward slashes
  2. chore(deps): update jktcp for windowed TCP sends
  3. feat: discover Apple TVs over mDNS
  4. feat: pair with an Apple TV and install over a network tunnel
  5. feat: request tvOS provisioning from the developer portal
  6. feat: pair, select and install to Apple TVs from the GUI

I'm happy to split this into separate PRs if you'd rather review it that way.

Two things worth pointing out

The zip fix isn't tvOS related. archive_package_bundle built entry names with to_string_lossy
on a relative path, so on Windows they carried backslashes. Zip requires /, and readers split on
it to find the bundle, so IPAs exported on Windows were malformed. It's first in the series so it
can be taken on its own.

tvOS provisioning is selected in the request body, not the URL. The portal defaults every request
to iOS and returns error 8220 for an Apple TV even with the device registered and listDevices
returning it as tvOS. Sending DTDK_Platform=tvos and subPlatform=tvOS fixes it. The endpoint
URLs are unchanged: a /tvos/ path segment returns a non-plist body on this API version, so the
platform has to travel in the body. A/B tested against the live API with one variable changed. iOS
requests are unchanged byte for byte.

Performance

A network install of a 66 MB bundle took about three minutes at first, and is now about eight
seconds:

  • the tunnel MSS was never set, so segments were 1220 bytes while the tunnel had negotiated 16000
  • AFC costs a round trip per file open, write and close, and the bundle was 444 files, which over a
    tunnel was most of the transfer time. A network install now uploads a single archive. usbmuxd
    round trips are cheap, so it still mirrors the directory there.
  • the pinned jktcp sent one segment per round trip; 0.1.6 keeps a real send window

Entry names were built with to_string_lossy on a relative path, so on
Windows they carried backslashes. Zip requires '/', and readers split on
it to locate a bundle, so an archive exported on Windows could not be
read back by tooling that looks for Payload/<App>.app/Info.plist.

Join the path components rather than replacing separators, so a name
containing a literal backslash on a platform that permits one survives.
The pinned release sent one segment and waited for its acknowledgement
before sending the next, so any transfer over the userspace TCP stack ran
at one segment per round trip regardless of the link. 0.1.6 keeps a send
window bounded by the peer's advertised window, with window scaling.

Measured over a CoreDevice tunnel: 2.3 MB/s to 6.35 MB/s, no
retransmissions.
An Apple TV is reached over the network rather than usbmuxd, so it has to
be found before anything else can talk to it.

Adds a discovery module with a backend per platform. mdns-sd serves macOS
and Linux. It does not work on Windows: it binds its own socket on port
5353 and never receives the device's responses, because the Dnscache
service owns multicast DNS there, so Windows uses the OS resolver through
dnsapi.dll instead. Browsing alone returns SRV records only
intermittently, so each instance is resolved individually to obtain a
port.

Apple TVs advertise two RPPairing services that are not interchangeable.
_remotepairing-manual-pairing._tcp is advertised only while the device is
showing a pairing code and is the only one that accepts first-time
pairing; _remotepairing._tcp is advertised once a pairing exists and only
accepts reconnection. They carry different ports, and the identifier they
advertise changes between advertisements, so it cannot be used as an
identity. Devices are grouped by hostname instead, and a companion-link
advertisement supplies the model a paired device otherwise stops
publishing.
Pairing runs the SRP exchange over RPPairing. The device displays its
code only after accepting the request and stops displaying it when the
session closes, so pairing takes a provider rather than a code and
invokes it while the session is held open.

Installing establishes a TLS-PSK tunnel, runs the CoreDevice and RSD
handshakes, and then uses the same installation entry point USB does.
Reconnection is verify-only and never falls back to pair-setup, so
reaching a tunnel cannot make a device display a code to someone who did
not ask to pair.

The MSS is taken from the MTU the tunnel just negotiated. The stack
behind the tunnel sends one segment per round trip, so the segment size
sets the transfer rate outright; left unset it falls back to a default an
order of magnitude below what the tunnel carries.

mDNS never advertises a UDID, so the real one is read from the RSD
handshake. It is kept separate from the key the pairing file is stored
under, which leaves udid empty until a tunnel supplies it rather than
letting a fabricated value reach Apple.
Signing for an Apple TV failed with error 8220, "your team has no devices
from which to generate a provisioning profile", even with the device
registered and listDevices returning it as tvOS. The portal defaults every
request to iOS, so it looked for an iOS device and found none.

Carry the platform in the request body as DTDK_Platform and subPlatform.
The URL is not the mechanism: the /tvos/ path segment does not exist on
this API version and probing it returns a non-plist body. A/B tested
against the live API with one variable changed, downloadTeamProvisioningProfile
returns 8220 without these fields and a profile with them.

iOS sends no extra fields, so its requests stay byte-identical.
Certificates and app groups are deliberately left unparameterised; a
certificate issues correctly for a tvOS profile without them.

Refreshing also refuses to register a device whose UDID is still unknown,
which a network device's is until a tunnel supplies it.
Adds a pairing screen reached from the main screen: scan, pick a device,
enter the code it displays, pair. Once paired, an Apple TV appears in the
same device picker USB devices use, so installing to one goes through the
normal flow rather than a separate path.

Network devices are rescanned on a timer and only reported gone after two
consecutive misses, because a single missed response is common and would
otherwise move the user's device selection without them noticing.

Over a tunnel the bundle is uploaded as one archive rather than mirrored
file by file. AFC costs a round trip per file open, write and close, and a
tunnelled round trip is dear enough that hundreds of small files cost far
more than the bytes in them; measured on a 66 MB bundle of 444 files, that
overhead was most of the transfer. usbmuxd round trips are cheap by
comparison, so it keeps mirroring there, where compressing would cost more
than it saves.

The bar no longer claims a percentage for the upload, which reports none.
It names the size being sent instead, so a transfer that takes a while
does not read as a hang.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant