"앱은 못 닿는데 curl은 멀쩡하면, 네트워크보다 ATS를 먼저 의심해."
ATS가 거절하는 것
App Transport Security는 Info.plist에 따로 적지 않는 한, 앱의 URLSession과 WKWebView 연결이 HTTPS를 쓰게 강제해. 거절된 요청은 NSURLErrorAppTransportSecurityRequiresSecureConnection, 에러 -1022로 실패해. tailnet에 올라간 엔진의 HTTP 쌍둥이는 평범한 HTTP라서, 거기로 접속하는 클라이언트는 첫 요청부터 ATS에 걸려.
규칙은 직접 재보고 나면 믿기 쉬워. 레슨을 준비하면서 작은 검사 앱 하나를 ATS 키만 바꿔 여러 벌 만들고, macOS 26에서 똑같은 요청을 보내봤어.
- 키 없음: 루프백으로 가는 평범한 HTTP는 됐어. 공개 호스트, IP 주소로 지정한 tailnet 동료 기기, tailnet 이름으로 가는 평범한 HTTP는 Mac 자기 이름까지 포함해서 전부 -1022로 실패했어.
NSAllowsLocalNetworking만: 루프백은 됐고 tailnet은 여전히 실패했어. ATS가 말하는 로컬에 tailnet 주소는 안 들어가.NSAllowsArbitraryLoads만: 모든 목적지가 200으로 대답했어.- 두 키 같이: 공개 호스트와 tailnet이 다시 -1022로 실패했어.
- 키 하나 없이 엔진의 HTTPS 쌍둥이로: 200.
혼자 있어야 하는 키
네 번째 결과는 애플 문서에 그대로 적혀 있어. iOS 10과 macOS 10.12부터, NSAllowsArbitraryLoadsForMedia, NSAllowsArbitraryLoadsInWebContent, NSAllowsLocalNetworking 중 하나라도 있으면 NSAllowsArbitraryLoads는 무시돼. 전체 허용 키 옆에 좁은 키를 하나 더 두면 이중 안전장치 같아 보이는데, 실은 첫 번째 안전장치를 조용히 떼어내는 거야. 두 키를 다 가진 가족 런처가 tailnet 엔진에 닿을 수 없다고 보고했는데, 같은 Mac에서 curl은 10 ms 만에 200을 받았어. 가족 규칙은 짧아. 예외 없이 HTTPS 쌍둥이를 쓰거나, 아니면 NSAllowsArbitraryLoads를 다른 키 없이 혼자 써. 가족 폰 앱 대부분은 전체 허용 키 하나만 들고 있어. 애플은 App Store 심사에서 그 키를 쓰는 사유를 요구하지만, 가족 빌드는 내부 TestFlight 테스터한테 가.
ATS에 대해 거짓말하는 도구들
curl은 ATS를 구현하지 않아서, 거기서 받은 200은 네트워크를 증명할 뿐 앱에 대해선 아무것도 증명 못 해. nscurl --ats-diagnostics는 검사하기 전에 URL을 https로 바꿔버려. 믿을 만한 검사는 진짜 앱과 똑같은 ATS 키를 가진 번들 앱뿐이야. 아래 스크립트가 만드는 게 바로 그거고.
WKWebView엔 함정이 하나 더 있어. 평범한 HTTP 페이지를 tailnet IP 주소로 띄우는 브라우저 셸 앱이 있었는데, 그 주소를 가진 Mac에선 됐고 다른 Mac에선 전부 -1022로 실패했어. WebKit의 ATS는 그 기계 자신의 인터페이스 주소만 봐주고, 원격 IP 리터럴은 안 봐줘. 그래서 엔진이 있는 Mac에서 테스트하면 버그가 가려져. NSAllowsArbitraryLoadsInWebContent는 예외를 웹 콘텐츠로만 좁히고, URLSession엔 ATS를 그대로 적용해. 그리고 NSExceptionDomains엔 이름뿐 아니라 주소나 범위도 적을 수 있어. macOS 26.6에서 재보니, 다른 키 없이 다른 Mac의 tailnet IP나 tailnet 범위 100.64.0.0/10에 NSExceptionAllowsInsecureHTTPLoads 예외를 주면 -1022가 200으로 바뀌었어. 두 키에 tailnet 이름과 같은 플래그를 더해도 200이 나왔고. 어느 쪽이든 대가는 주소나 호스트 이름이 번들 안에 고정된다는 거야.