기기에서 shell을 얻어 /etc/resolv.conf를 읽거나 오래된 net.dns1 값을 캐내면 주소 하나가 나온다. 눈앞의 앱은 전혀 다른 경로로 해석한다. 현재 Android 릴리스에서 resolver는 네트워크별이며, 시스템은 각 Network에 대해 정책을 유지하고 요청을 보낸 socket에 적용한다. 파일과 옛 속성은 잔재이며 둘 다 당신의 질의가 실제로 지난 경로를 설명하지 못한다.

이 간극은 split-tunnel과 Private DNS를 배포할 때 드러나는데, 같은 휴대폰의 두 앱이 정당하게 다른 답을 보는 경우다. 이 안내는 잘못되었거나 빠진 조회에서 시작해 그 원인이 되는 구성으로 거슬러 올라간다.

resolver는 기기가 아니라 네트워크에 속한다

Android 9 이상의 이름 해석은 socket이 바인딩된 Network를 기준으로 하는 시스템 resolver를 거친다. 각 네트워크는 고유한 LinkProperties를 가지며 그 속성에는 해당 네트워크에만 적용되는 DNS 서버 목록, 검색 도메인, Private DNS 상태가 담긴다. 그 모델에서 파일로 읽을 수 있는 기기 전체 목록 하나에 대응하는 것은 없다.

VPN은 이 분리를 명확히 보여준다. 터널이 올라오면 VPN 앱이 VPN 자체 네트워크에 등록된 DNS 서버를 제공하므로 그를 통해 라우팅되는 앱은 그 서버들로 해석하고, 제외된 앱은 아래 네트워크를 계속 쓴다. 기기 하나, 답의 경로 둘, 모순 없음.

영향받은 앱 안에서 스냅숏을 읽어라

앱 자체에 물어라. Context와 일반 android.permission.ACCESS_NETWORK_STATE 권한을 가진 메서드는 시스템 속성을 건드리지 않고 스냅숏을 읽을 수 있다:

val cm = getSystemService(ConnectivityManager::class.java) ?: return
val network = cm.activeNetwork
val links = network?.let { cm.getLinkProperties(it) }

val dnsServers = links?.dnsServers
val privateDnsActive = links?.isPrivateDnsActive
val privateDnsName = links?.privateDnsServerName

activeNetwork는 당신 프로세스의 기본 네트워크이며 기기의 유일한 네트워크가 아니고, 호출자에게 쓸 만한 기본값이 없으면 null일 수 있다. 차단된 기본 VPN이 흔한 이유다. isPrivateDnsActive는 그 네트워크의 질의가 TLS를 거쳐 이름 있는 resolver로 가는지 알려주며, 이는 단순한 UDP 53 프로브가 보여줄 것을 바꾼다. API 표면은 LinkProperties 아래에 문서화되어 있고 그 안의 모든 값은 구성의 스냅숏이지 특정 질의가 특정 서버에 도달했다는 증거가 아니다.

기본 네트워크가 일을 하는 네트워크가 아닐 때

스냅숏은 기본값에 대해서만 답한다. 앱은 자기 프로세스나 단일 socket을 다른 네트워크에 바인딩할 수 있고 그러면 해석이 그 선택을 따르지만 당신의 진단은 여전히 기본값을 보고한다. 자체 DNS-over-HTTPS resolver를 가진 브라우저는 시스템 목록을 아예 참조하지 않으므로 그 답은 기기의 나머지에 대해 아무 말도 하지 않는다. 회사 프로필 앱은 자체 정책을 가진 별도 사용자 아래에서 돈다. 서버를 탓하기 전에 바인딩을 확인하라.

VPN 통합이 나머지 절반이다. dhcp-option DNS 줄을 OpenVPN 프로필에서 받는 것과 그 주소를 Android VPN 네트워크에 등록하는 것은 별개의 두 단계이며, 서버로 가는 경로가 없으면 잘못된 주소처럼 조용히 실패한다. 개발 머신에서 adb shell dumpsys connectivity는 빌드가 허용할 때 같은 링크 속성을 네트워크별로 출력하므로 코드를 쓰지 않고 목록을 확인할 수 있다.

구성을 허용된 조회 하나와 비교하라

사설 이름 공간에서 반드시 해석되어야 할 이름과 공개 이름 하나를 골라 두 결과를 스냅숏 옆에 적어라: 네트워크, 구성된 서버, Private DNS 상태, 정확한 오류 문구. 이름 있는 서버로의 직접 질의는 그 서버가 당신에게 답한다는 것을 증명하지만, 일반 앱 해석이 그것을 선택했다는 것은 증명하지 않는다. 캐시된 답은 새 테스트가 아니므로 캐시를 비우거나 오늘 아무도 묻지 않은 이름을 써라.

resolver가 특정 액세스 네트워크의 클라이언트만 처리한다면 VPN 이그레스 후 그것이 보는 것이 결과를 좌우할 수 있고, 이는 그것을 운영하는 쪽의 정책 문제이지 통신사가 UDP 53을 차단한다는 증거가 아니다. Private DNS가 활성인 곳에서는 정규화되지 않은 호스트 이름이 설계상 거부될 수 있지만 같은 서버가 공개 이름에는 정상적으로 답한다. 속성을 읽으면 어느 모드가 적용 중인지 알 수 있으니 패킷 필터를 추측하는 일을 멈추게 된다.

이 불일치를 소유한 계층을 고쳐라

전역 상태를 편집하는 대신 위로 올라가라. 앱이 잘못된 네트워크에 바인딩되었다면 바인딩을 고쳐라. 프로필이 잘못된 DNS 옵션을 준다면 프로필을 고쳐라. resolver가 당신의 사설 영역을 처리할 수 없는 네트워크에서 Private DNS가 엄격하다면 그것은 기기 관리 결정이지 기능을 어디서나 끌 이유가 아니다.

공개 resolver를 DHCP 훅이나 시스템 속성에 밀어 넣는 것은 사고를 끊임없이 만드는 수리다: 사설 이름이 해석되지 않고, 변경은 VPN 세션보다 오래 살아남으며, 기기의 다음 사람은 소유자 없는 낡은 주소를 발견한다. 변경을 그 불일치를 소유한 VPN, 앱 또는 관리 계층 안에 두고, 그런 다음 스냅숏을 다시 읽어 서버 목록이 실제로 바뀌었는지 확인하라.

결국 원하는 것은 하나의 네트워크, resolver 목록, Private DNS 상태에 묶인 앱 요청 하나다. 그 사슬을 보여줄 수 있게 되면 파일 속의 익숙한 주소는 더 이상 아무것의 증거가 아니다.

관련 글