2019년 2월 1일, 제멋대로 동작하는 DNS 서버 뒤에 있던 도메인들이 업데이트된 리졸버에서 해석되지 않기 시작했다. 장애 때문도, 공격 때문도 아니었다. 전 세계 DNS 리졸버를 운영하는 사람들이 몇 달 전부터 미리 약속해둔 대로, 그날부터 친절하기를 그만뒀기 때문이다.
20년 동안 리졸버들은 규칙을 못 지키는 서버들의 뒤를 조용히 치워주고 있었다. 플래그 데이에 그걸 멈췄다. 그러자 설정이 잘못된 도메인 한 무더기가 — 한꺼번에 — 안전망이 사라졌다는 걸 알게 됐다.
20년을 잡아먹은 임시방편
DNS의 사정은 이렇다. 와이어 포맷은 1987년에 굳었고, RFC 1035는 UDP 응답 하나에 512바이트를 줬다. 응답이 IP 주소 하나와 TTL이던 시절엔 충분했다. DNSSEC 서명이든, IPv6 레코드 한 움큼이든, 엽서보다 큰 무언가를 담으려는 순간 부족해졌다.
그래서 1999년, EDNS(0)가 나왔다 — Extension Mechanisms for DNS, 원래는 RFC 2671, 지금은 RFC 6891이다. EDNS는 리졸버가 질의에 덧붙이는 유사 레코드로, 사실상 “나 더 큰 응답 받을 수 있고, 이런 새 기능도 이해한다”고 말한다. 현대 DNS가 딛고 선 토대다. DNSSEC은 이것 없이는 작동하지 않는다.
문제는 상당수의 DNS 서버가 — 더 나쁘게는, 그 앞에 놓인 상당수의 방화벽이 — EDNS를 엉망으로 처리했다는 것이다. 모르는 옵션이 붙은 질의를 그냥 버리는 곳도, 쓰레기 응답을 돌려주는 곳도, 그냥 침묵하는 곳도 있었다. 2004년에 “이상해 보이는” DNS 패킷을 막도록 설정된 방화벽은 EDNS 질의를 죄다 삼키고 한마디도 하지 않았다.
리졸버 개발자들은 합리적인 선택을 했다. EDNS 질의에 응답이 없으면, 서버가 오래된 물건이라 여기고 EDNS 부분을 떼어낸 뒤 1987년식 순수 DNS로 다시 시도했다. 통했다. 도메인은 해석됐고, 다들 만족스럽게 집에 갔다.
문제는 “EDNS 빼고 재시도”가 비싸다는 것이다. 재시도 한 번마다 타임아웃을 — 흔히 1초 이상을 — 기다린 다음에야 두 번째 시도가 나갔다. 게다가 이 방식은 고장 난 설정에 조용히 상을 줬다. 당신의 방화벽이 EDNS를 몰래 삼키고 있어도 당신은 영영 알 수 없었다. 전 세계 리졸버가 느린 재시도 한 번씩으로 당신 대신 덮어줬으니까.
그런 게 20년이었다. 폴백 코드는 릴리스마다 더 기괴해졌고, 특정 고장 동작마다 특수 케이스가 붙었으며, 손대면 뭐가 깨질지 아무도 온전히 몰라 아무도 건드리기 싫어하는 종류의 물건이 됐다.
안전망을 끊다
그래서 주요 구현체들 — BIND을 만드는 ISC, Unbound를 만드는 NLnet Labs, PowerDNS, Knot을 만드는 CZ.NIC — 여기에 구글과 클라우드플레어의 대형 공용 리졸버가 더해, 조율되고 약간 무자비한 일을 했다. 날짜를 정하고 공표했다. 2019년 2월 1일부터 임시방편은 없다. 당신의 권위 서버가 EDNS 질의에 아예 응답하지 않으면, 우리는 그걸 죽은 것으로 취급한다. “오래된” 게 아니라, 죽은 것으로.
BIND은 9.14에서 폴백을 걷어냈다. Unbound는 1.9.0에서 했다. Knot Resolver는 이미 조여둔 상태였다. 공용 리졸버들은 각자의 인프라에 이를 실었다. 폭발 반경은 들리는 것보다 좁았다. EDNS에 제대로 응답하는 서버는 — 심지어 “그 옵션은 지원 안 한다”고 답하더라도 — 멀쩡했다. 다친 건 EDNS 질의를 순수한 침묵으로 맞은 서버들이었다. 그런 도메인은 업데이트된 리졸버에서 해석이 멈췄고, 대형 공용 리졸버가 전환하고 운영자들이 BIND·Unbound·PowerDNS Recursor(4.2.0)를 올려 갈수록 영향받는 사용자도 늘었다.
무모하게 들린다. 아니었다. 공개 날짜를 정하고, 웹사이트를 세우고, 누구나 도메인을 붙여 넣어볼 수 있는 테스트 페이지를 만든 이유는 단 하나 — 실패를 조용하고 느린 것에서 시끄럽고 또렷한 것으로 바꾸기 위해서였다. 테스트를 돌려 빨간불을 본 운영자는 몇 주의 예고와, 고칠 것 하나를 명확히 손에 쥐었다. 대개는 방화벽 규칙, 가끔은 10년 묵은 DNS 장비였다. 대안 — 임시방편을 영원히 짊어지는 것 — 은 그 고장 난 설정들이 영영 고쳐지지 않고, 그 아래 모든 것이 계속 세금을 무는 것을 뜻했다.
내가 좋아하는 대목이 여기다. 인터넷 인프라가 장기적 부패를 죽이려고 단기적 고통을, 그것도 카운트다운 시계까지 걸고 일부러 택한 드문 사례다. 아무도 시키지 않았다. DNS 경찰 같은 건 없다. 그저 한 줌의 메인테이너들이 이제 그만하면 됐다고 결정했을 뿐이다.
아무도 예상 못 한 속편: 1232바이트
2019년 플래그 데이는 EDNS를 말할 줄 모르는 서버를 처리했다. 2020년 플래그 데이는 EDNS를 너무 열성적으로 말하는 서버를 겨냥했다.
2020년의 문제는 단편화(fragmentation)였다. EDNS는 리졸버가 얼마나 큰 UDP 응답을 받을 수 있는지 광고하게 해주는데, 오랫동안 흔한 기본값은 4096바이트였다. 하지만 4096바이트 UDP 데이터그램은 일반 이더넷 프레임이 한 번에 실어 나르는 약 1500바이트보다 훨씬 크다. 그래서 응답은 IP 계층에서 여러 조각으로 쪼개지고, 받는 쪽에서 다시 조립돼야 한다.
IP 단편화는 특히 DNS에 나쁜 소식이다. 조각은 잃어버리기 쉽고, 중간 장비가 떨궈버리기 쉬우며 — 무서운 대목은 — 위조하기가 더 쉽다. 응답을 단편화되게 만들 수 있는 공격자는 조립 과정에 위조된 두 번째 조각을 끼워 넣을 확률이 훨씬 높아지는데, 이건 캐시 포이즈닝 경로다. 단편화는 성능 성가심을 조용히 보안 문제로 바꿔놓았다.
2020년의 해법은 거의 공격적일 만큼 단순했다. 더 작은 버퍼를 광고하라. 권장 값은 1232바이트가 됐고, 이건 감이 아니라 산수에서 나왔다. IPv6가 절대 단편화가 필요 없다고 보장하는 최소 MTU 1280바이트를 잡는다. 40바이트 IPv6 헤더를 뺀다. 8바이트 UDP 헤더를 뺀다. 남는 1232바이트가 어떤 경로든 통째로 건너갈 DNS 페이로드다.
절충은 정직하다. 응답이 그보다 크면 서버가 truncation 비트를 세우고, 리졸버는 질의 전체를 TCP로 재시도한다. 그래서 2020년 플래그 데이의 또 다른 요구가 바로 이것이었다. 모든 DNS 서버는 — 권위든 재귀든 — 53번 포트에서 실제로 TCP에 응답해야 한다. “DNS는 UDP”라는 건 늘 속설이었다. TCP는 처음부터 규격에 있었다. 그런데 많은 운영자가 필요 없다는 이유로 그냥 막아뒀다. 2020년 이후, 필요해졌다.
그것이 남긴 것
플래그 데이들은 뉴스가 되지 않았다. CVE도, 벤더 워룸도, 숨 가쁜 사후분석도 없었다. 그저 고장 난 동작을 참아주기를 그만두겠다는 조율된 결정을, 공개 마감일과 테스트 페이지로 실행했을 뿐이다.
교훈은 사실 EDNS에 관한 게 아니다. 호환성 임시방편은 부채이고, 부채는 복리로 불어난다는 것이다. “고장 난 경우는 그냥 우리가 처리하지”라는 작은 자비는, 20년과 수조 건의 질의에 곱해지면 시스템 전체를 인질로 잡는 그것이 된다. 가끔은 누군가 플래그 데이를 선언하고, 날짜를 공표하고, 고장 난 것들을 — 시끄럽게, 예정대로, 고칠 수 있는 사람들 눈앞에서 — 깨지게 놔둬야 한다.
인터넷의 문제 대부분은 그런 대접을 못 받는다. 임시방편을 받고, 그다음엔 그 임시방편을 위한 임시방편을, 영원히 받는다. DNS는 플래그 데이를 두 번 받았고, 그 덕에 나아졌다.