fix(apple): ATS blocked the library on every non-RFC1918 host

The game library rides the management REST API over HTTPS (TCP 47990) through
URLSession, authenticated by mTLS and pinned by SHA-256 fingerprint. The app
declared no App Transport Security policy at all, so it ran under default ATS --
which exempts only "local" destinations (.local, unqualified names, RFC1918 and
link-local literals) and applies the full policy everywhere else. The host
certificate is self-signed (and carries no SubjectAltName), so it cannot satisfy
that policy: the library loaded at 192.168.x and failed at the TLS layer on any
other address.

Field-reported against a Tailscale host. 100.64.0.0/10 is CGNAT, not RFC1918, so
the library failed there while streaming to the very same address worked -- the
QUIC stream plane is raw UDP and never enters the URL loading system. A WireGuard
peer or a public-IP host breaks identically.

Declares NSAllowsArbitraryLoads, which must stay the dict's only key: iOS 10+ and
macOS 10.12+ ignore it whenever a more granular ATS key sits alongside it. Trust
is unchanged -- LibraryTLSDelegate still pins the host by fingerprint and hands
every other origin (cover-art CDNs) to full system trust evaluation.
This commit is contained in:
2026-08-08 01:07:26 +02:00
parent 167e35f720
commit 78a02bc994
+19
View File
@@ -19,6 +19,25 @@
<array>
<string>_punktfunk._udp</string>
</array>
<!-- App Transport Security. We talk to the user's OWN host over HTTPS with a SELF-SIGNED
certificate at a user-supplied address, and verify it by SHA-256 fingerprint pinning
established during PIN pairing (LibraryTLSDelegate) — for a box on someone's LAN there is
no CA that could vouch for it, so pinning is the stronger check, not a weaker one.
Default ATS exempts only "local" destinations (.local, unqualified names, RFC1918 and
link-local literals). Every other address gets the full policy, which a self-signed cert
cannot satisfy — so the management API (game library) worked at 192.168.x but died at the
TLS layer on Tailscale's 100.64/10 CGNAT range, a WireGuard peer, or a public IP. The QUIC
stream plane never showed it: that is raw UDP and never enters the URL loading system.
⚠ This must stay the ONLY key in this dict. On iOS 10+/macOS 10.12+ the system IGNORES
NSAllowsArbitraryLoads whenever NSAllowsLocalNetworking, NSAllowsArbitraryLoadsInWebContent
or NSAllowsArbitraryLoadsForMedia is present alongside it — adding one silently restores
the bug. Other origins (cover-art CDNs) keep full system trust evaluation regardless:
LibraryTLSDelegate hands every non-host challenge to .performDefaultHandling. -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<!-- Background keep-alive (opt-in, iOS/iPadOS): the ONLY sanctioned way to keep the long-lived
QUIC socket + pump-thread set alive while backgrounded is the audio background mode, backed
by the session's real, audible remote audio (AVAudioEngine keeps rendering). Video decode is