VPN 킬 스위치가 포트 53을 막으면 DNS가 풀리지 않는 이유
킬 스위치는 DNS를 의도적으로 막을 수 있다. 터널 안에서만 푸는 방식과 로컬 리졸버를 예외로 두는 방식을 비교하고, 어느 계층이 실제로 실패했는지 가려내야 한다.
킬 스위치가 터널을 끊으면 DNS 경로도 함께 끊긴다. 그리고 남은 리졸버는 기대했던 그 리졸버가 아닌 경우가 대부분이다. 이 순간을 다루는 두 가지 설계는 크게 다르다. 터널 안에 있지 않은 모든 DNS 패킷을 막거나, 아니면리졸버 하나를 예외로 남겨 두는방식이다. 둘 다 근거가 있다. 브라우저 탭에서 보면 증상이 거의 같아서, 먼저 어느 설계를 실제로 배포했는지 확인해야 한다.
이 구분을 늘 눈앞에 두는 이유는 고치는 방향이 경우마다 정반대이기 때문이다. 한 설계에는 더 넓은 예외가, 다른 설계에는 더 좁은 예외가 필요하다. 짐작으로 밀어붙이면 프라이버시를 잃거나 반나절을 잃는다.
같은 장애에 대한 두 가지 DNS 설계
설계 A는 리졸버를 터널 뒤에 있는 또 하나의 서비스로 취급한다. 방화벽 규칙은 UDP와 TCP 53을 터널 인터페이스에서만 허용하고, 클라이언트에 주는 주소는 터널 안의 리졸버다. 터널이 끊기면 조회가 멈추는데, 그것은 고장이 아니라 의도된 결과다.
설계 B는 로컬 네트워크에서 리졸버를 계속 닿을 수 있게 둔다. 보통은 192.168.1.1의 공유기나 127.0.0.53의 운영체제 스텁이다. 이름 해석이 터널과 함께 죽어서는 안 된다는 논리다. 집 전체의 이름을 공유기가 풀어 주는 가정용 LAN에서는 편하다. 하지만 호텔이나 공항 Wi-Fi에서는 같은 예외가 남의 서버를 가리키고, 웹 트래픽이 막힌 채로도 입력하는 모든 이름이 드러난다.
경계는 누가 리졸버를 소유하느냐다. 로컬 리졸버를 신뢰한다면 예외는 작고 정직한 구멍이다. 신뢰하지 않는다면 터널 전용 설계만이 버틴다. 대가는 끊긴 터널이 모든 이름을 함께 데려간다는 것이다.
VPN 클라이언트 자신의 조회는 왜 계속 동작하는가
클라이언트는 터널이 생기기 전에 게이트웨이 호스트 이름을 풀어야 한다. 그래서 엄격한 차단이 클라이언트의 부트스트랩 경로까지 덮어서는 안 된다. 대부분의 클라이언트는 엔드포인트 주소를 캐시하거나, 그 한 호스트에 대한 문서화된 부트스트랩 예외를 제공한다. 규칙 집합이 그 조회를 막으면 터널은 영영 돌아오지 않고, 로그에는 자기 서버에 닿지 못하는 클라이언트가 남는다. 이는 DNS 결함이 아니라 정책 결함이다.
분할 DNS도 이 문제가 드러나는 자리다. 클라이언트는 내부 영역을 사설 리졸버로 밀어 넣고 나머지는 시스템 리졸버에 맡긴다. 터널이 끊긴 뒤 공개 이름은 여전히 풀릴 수 있지만 내부 이름은 시간 초과가 난다. 사설 영역에 남은 경로가 없기 때문이다. 사용자에게는 "VPN이 인터넷을 망가뜨렸다"이고, 리졸버 로그에는 끝내 도착하지 않은 질의다.
systemd-resolved 아래에서는 그 분할을 직접 볼 수 있다.resolvectl status는 링크별 DNS 서버와 라우팅 도메인을 나열한다. 터널이 내려간 뒤에도 내부 도메인은 더 이상 존재하지 않는 링크의 서버를 계속 가리킨다. 127.0.0.53의 스텁은 질의를 여전히 받아들이고, 그다음 응답하지 않을 상류를 기다린다. 그래서 이 실패는 즉시 돌아오지 않고 몇 초가 걸린다.
암호화된 DNS는 패킷 필터를 우회하지 않는다
DNS over HTTPS와 DNS over TLS는 443과 853으로 다닌다. 그래서 53번 포트를 막는 규칙은 그 둘에 대해 아무 말도 하지 않는다. 포트로 "DNS"를 막는 것은 포트에 대한 정책이지 이름 해석에 대한 정책이 아니다. 킬 스위치가 터널 밖의 모든 트래픽을 막는다면 DoH는 우연히 덮인다. 캡티브 포털이나 공개 리졸버를 위해 예외를 뚫어 두었다면 덮이지 않는다.
값싼 지렛대는 클라이언트 쪽 제어다. Firefox는 카나리 이름use-application-dns.net이 풀리지 않으면 자기 DoH 리졸버를 포기한다. 관리되는 네트워크가 그 도메인을 NXDOMAIN으로 공표하는 이유이고, 기업 정책으로 꺼진 상태로 고정할 수도 있다. Android Private DNS와 Chrome의 보안 DNS에는 각자의 스위치와 각자의 기본값이 있다. 그것들을 끄는 편이 리졸버 주소를 쫓아다니는 것보다 믿을 만하다. 주소 목록은 바뀌지만 당신의 규칙 집합은 바뀌지 않기 때문이다.
이것이 유출과 전혀 무관한 경우도 있다. 리졸버를 고정한 애플리케이션도 거기에 닿을 네트워크 경로가 필요하다. 암호화가 바꾸는 것은 누가 질의를 읽을 수 있느냐이지, 패킷이 기계를 떠날 수 있느냐가 아니다. 그래서 기기 전체 차단은 그 설정을 건드리지 않고도 무력화한다.
증상을 계층에 맞춰라
대부분의 보고는 한 문장으로 들어오고, 그 문장은 대개 이미 계층을 지목한다. 아래 표는 내가 규칙을 건드리기 전에 훑는 순서다. 목적은 질의가 막혔는지, 잘못 보내졌는지, 애초에 보내지지 않았는지를 가리는 것이다.
| Symptom | Likely layer | First check |
|---|---|---|
| Every name fails at once, instantly | Resolver answering with a refusal | Query the resolver by address, not by name |
| Lookups hang for seconds, then time out | Packets dropped with no response | Watch port 53 on the physical link |
| Public names resolve, internal ones do not | Split-DNS routing domain | Per-link DNS servers on the tunnel |
| Names resolve, the page still fails | Not DNS: routing or MTU | Connect to the IP directly |
| Only one application fails | Pinned resolver or cached answer | Check that app's DNS setting |
사람들이 잘못 진단하는 것은 첫 두 줄이다. 조용한 탈락과 거부는 브라우저에서 똑같이 "DNS가 고장 났다"로 보이지만, 킬 스위치가 제 일을 하고 있다는 뜻인 것은 둘 중 하나뿐이다. 거부는 무언가가 응답했다는 뜻이므로, 차단은 당신이 생각하는 곳에 있지 않다.
리졸버 정책을 되돌린 뒤 증명하라
증상이 아니라 설계를 고쳐라. 터널 안에서만 푸는 방식이라면 터널 인터페이스에서 53을 허용하고, 내부 리졸버를 터널 구성과 함께 밀어 넣고, 물리 링크에는 DNS 허용을 전혀 남기지 않는다. wg-quick에서는 리졸버를 인터페이스 구성에 적는 것이DNS =줄이다. 그래서 그 줄을 빠뜨린 터널은 옛 리졸버를 그대로 두고 모든 질의를 잘못된 경로로 보낸다.wg-quick 설명서가 그 동작을 플랫폼별로 기록한다.
그다음 이 순서로 확인한다. Linux에서는resolvectl status가 활성 링크를 보여 주고,dig +short example.com가 공개 이름을 시험하고,dig @10.8.0.1 internal.example.com가 사설 이름을 시험하고,sudo tcpdump -ni any port 53가 둘을 함께 돌리는 동안 패킷이 어느 인터페이스를 쓰는지 보여 준다. Windows에서는Get-DnsClientServerAddress -AddressFamily IPv4와Resolve-DnsName example.com가 같은 두 가지 사실을 준다. 누가 설정되어 있고, 누가 응답하는가.
부정 테스트로 마무리한다. 터널을 끊고, 새 공개 이름이 실패하는지 확인하고, 사설 이름이 실패하는지 확인하고,53번 포트 패킷이 물리 어댑터를 떠나지 않았음을 확인한다. 캐시된 응답에만 통하는 차단은 전혀 통하지 않는다.
티켓을 닫기 전 점검 목록:
- 장애 중에 쓰인 리졸버가 정책이 의도한 그 리졸버다.
- 클라이언트 자신의 엔드포인트 조회는 캐시나 예외로 여전히 허용된다.
- DoH와 DoT는 차단에 포함되거나 정책으로 꺼져 있다.
- 터널이 사라지면 사설 영역은 열린 상태가 아니라 닫힌 상태로 실패한다.