"App unreachable, curl fine: think ATS before network."
What ATS Refuses
App Transport Security makes an app's URLSession and WKWebView connections use HTTPS unless the app's Info.plist says otherwise. A refused load fails with NSURLErrorAppTransportSecurityRequiresSecureConnection, error -1022. The engine's HTTP twin on the tailnet is plain HTTP, so a client that dials it meets ATS on its first request.
The rules are easier to trust once measured. For this lesson, copies of one tiny probe app were built with different ATS keys and asked the same questions on macOS 26:
- No keys: plain HTTP to loopback worked. Plain HTTP to a public host, to a tailnet peer by address, and to a tailnet name (even the Mac's own) all failed with -1022.
NSAllowsLocalNetworkingalone: loopback worked, and the tailnet still failed. A tailnet address is not what ATS means by local.NSAllowsArbitraryLoadsalone: every destination answered 200.- Both keys together: -1022 again for the public host and the tailnet.
- The engine's HTTPS twin, with no keys at all: 200.
The Key That Must Be Alone
Apple's documentation states the fourth result plainly: since iOS 10 and macOS 10.12, NSAllowsArbitraryLoads is ignored if NSAllowsArbitraryLoadsForMedia, NSAllowsArbitraryLoadsInWebContent or NSAllowsLocalNetworking is present. Adding a narrower key next to the blanket one looks like belt and braces and silently removes the belt. A family launcher with both keys reported its tailnet engine as unreachable while curl from the same Mac got a 200 in 10 ms. The family rule is short: use the HTTPS twin and no exception, or NSAllowsArbitraryLoads with nothing beside it. Most of the family's phone apps carry the blanket key alone. Apple asks for a justification of that key in App Store review; the family's builds go to internal TestFlight testers.
Instruments That Lie About ATS
curl does not implement ATS, so a 200 from it proves the network and nothing about the app. nscurl --ats-diagnostics rewrites the URL to https before testing. The only faithful probe is a bundled app with the real app's ATS keys, which is what the script below builds.
WKWebView adds a trap of its own. A browser-shell app loading a plain-HTTP page by tailnet IP address worked on the Mac that owned that address and failed with -1022 on every other Mac: WebKit's ATS exempts the machine's own interface addresses, not remote IP literals, so testing on the engine's Mac hides the bug. NSAllowsArbitraryLoadsInWebContent scopes the exception to web content and leaves URLSession under full ATS, and NSExceptionDomains takes an address or a range as well as a name. Measured on macOS 26.6, an exception with NSExceptionAllowsInsecureHTTPLoads for a peer's tailnet IP, or for the tailnet range 100.64.0.0/10, turned -1022 into 200 with no other keys, and both keys plus the tailnet name with the same flag returned 200 too. Each works at the price of an address or hostname fixed inside the bundle.