Path MTU 블랙홀: 큰 패킷에서만 터지는 버그

연결은 된다. 작은 요청도 잘 된다. 그런데 큰 전송이 영원히 멈추고, 다들 앱을 탓한다. 대개는 중간의 라우터가 큰 패킷을 조용히 버리고, 그 사실을 알려줄 ICMP 메시지를 어느 방화벽이 삼킨 것이다 — '네트워크가 된다'와 '네트워크가 큰 패킷을 통과시킨다'가 서로 다른 주장인, 인터넷 깊숙이 박힌 실패 방식.

받을 자격 이상으로 많은 오후를 망쳐온 버그 리포트가 하나 있다. 누군가 서버에 SSH로 접속한다. 로그인은 된다. ls를 치면 즉시 응답한다. 그런데 출력이 한 화면쯤 되는 명령을 돌리거나 파일 전송을 시작하면 터미널이 얼어붙는다. 죽는다 — 느린 게 아니라 멈춘다. 연결은 도구들이 합의하는 모든 정의상 여전히 “살아 있고”, 애플리케이션 로그에도 이상한 건 없다. 실제 데이터가 움직이려는 순간에만 멈출 뿐이다.

웹페이지도 마찬가지다. HTML은 도착하고 레이아웃이 그려지는데, 큰 이미지나 JSON 페이로드가 와야 할 자리에서 스피너를 띄운 채 멈춘다. 새로고침하면 이번엔 될지도. 안 될지도.

본능은 늘 같다. 애플리케이션을 탓한다. SSH 탓이야. 로드밸런서 탓이야. 프레임워크 탓이야. 거의 언제나 그 어느 것도 아니다. 지금 보고 있는 건 Path MTU 블랙홀이다 — 중간 어딘가의 라우터가 당신의 가장 큰 패킷을 조용히 버리고, 무슨 일이 일어났는지 정확히 알려줬을 진단 메시지를 누군가 2009년에 조여둔 방화벽이 떨궈버린 것이다. 작은 건 되고 큰 건 죽는다는 사실이 지문의 전부이고, 한번 읽을 줄 알게 되면 이 버그가 사방에 보인다.

아무도 일부러 고르지 않은 1500이라는 숫자

모든 링크에는 MTU(최대 전송 단위) — 한 번에 통째로 나를 수 있는 가장 큰 패킷 — 가 있다. 이더넷의 MTU는 1500바이트다. 이 숫자는 물리 법칙이 아니다. 1980년대 초 이더넷에 박힌 타협이다. 효율과, 한 스테이션이 회선을 얼마나 오래 독점할 수 있는가 사이의 균형점이었다. 그게 굳었고, 퍼졌고, 이제 사실상 인터넷 전체가 1500이 정상이라고 조용히 가정한다. 애플리케이션 개발자는 이걸 신경 쓴 적이 없다. 대부분의 시스템 관리자도, 이게 물어뜯는 날이 오기 전까지는.

문제는 1500이 순수 이더넷에서만 참이라는 것이다. 트래픽이 캡슐화 오버헤드가 있는 무언가를 지나는 순간, 실제 쓸 수 있는 MTU가 줄어든다. 많은 DSL·광 회선이 쓰는 PPPoE 프레이밍은 8바이트를 떼어가 1492가 남는다. VPN은 모든 패킷을 또 다른 헤더로 감싼다 — WireGuard, IPsec, GRE 터널이 저마다 몫을 잘라내, 실제 MTU를 1420이나 1400, 혹은 프로토콜마다 찾아봐야 하는 어떤 숫자로 떨군다. 그래서 노트북에서 서버까지의 경로는 1500바이트 홉이 깔끔하게 이어진 길인 경우가 드물다. 중간 어딘가에 더 작은 링크가 있기 마련이고, 1500에 맞춰 만든 패킷은 그리로 못 지나간다.

원래는 이렇게 되어야 한다

인터넷에는 바로 이 상황을 위한 장치가 있고, 서류상으로는 우아하다. Path MTU Discovery라 부르고, 1990년에 RFC 1191로 표준화됐다.

현대 TCP는 패킷에 DF(Don’t Fragment) 플래그를 세운다. 경로상의 모든 라우터에게 내리는 명령이다: 이 패킷을 맞추겠다고 작은 조각으로 자르지 마라, 너무 크면 거부해라. 그래서 풀사이즈 패킷이 더 작은 링크에 부딪히면, 앞의 라우터는 조용히 조각내지 않는다. 패킷을 버리고 ICMP 메시지를 되돌려 보낸다 — 타입 3, 코드 4, 이름부터 시원하게 직설적이다: “Fragmentation Needed and Don’t Fragment was Set.” 결정적으로, 이 메시지에는 패킷이 못 들어간 그 링크의 MTU가 담겨 있다.

발신자는 이걸 받고 “아, 들어가는 최대치가 1400이구나”를 읽어, 패킷을 1400으로 줄여 재전송한다. 그 뒤로는 모든 게 흐른다. 연결은 1초도 안 되는 사이에 스스로 회복하고, 아무도 그런 일이 있었는지 모른다. 그게 설계가 작동하는 모습이다. 정말 영리하다: 네트워크가 쓸 정확한 크기를 알려주고, 당신은 거기에 맞춘다.

이 모든 게 가정 하나에 매달려 있다. 그 ICMP 메시지가 발신자에게 돌아가야 한다는 것.

왜 메시지가 먹히는가

2000년대 어느 시점, 방화벽 커뮤니티에 통념 하나가 굳었다: ICMP는 보안 위험이니 막아라. 실제 근거가 있었다 — ping 홍수, ICMP 리다이렉트 악용, 에코 패킷에 데이터를 실어 나르는 터널링. 하지만 그건 메스가 아니라 마체테로 적용됐다. 관리자들은 네트워크 경계에서 ICMP를 전부, 모든 타입을, 통째로 떨구는 규칙을 썼다. 그리고 “모든 타입”에는 path-MTU 장치 전체가 매달린 바로 그 메시지, 타입 3 코드 4가 휩쓸려 들어갔다.

이제 무슨 일이 벌어지는지 보자. 1500에 맞춘 패킷이 1400바이트 링크에 부딪힌다. 라우터가 버리고 충실하게 “fragmentation needed, MTU 1400”을 돌려보낸다. 그 응답이 발신자를 향해 가다가, ICMP를 전부 삼키도록 설정된 방화벽에 닿아 사라진다. 발신자는 아무것도 배우지 못한다. 발신자가 아는 한, 그 패킷은 그냥… ACK를 못 받은 것이다. 그래서 TCP는 ACK 없는 패킷에 늘 하는 그 한 가지를 한다: 기다렸다가, 똑같은 크기 초과 패킷을 그대로 재전송한다. 그게 같은 링크에 부딪혀 또 버려지고, 같은 ICMP 메시지를 낳고, 그게 또 먹힌다. 영원히.

그게 블랙홀이다. 라우터는 제 일을 하고 있다. ICMP 장치도 제 일을 하고 있다. 방화벽 하나가 나쁜 소식을 나르는 사슬의 유일한 고리를 조용히 제거했고, 그 결과가 완벽하게 연결됐다가 풀사이즈 패킷을 보내야 하는 순간 죽는 연결이다.

작은 건 되는 이유가 바로 이것이다. TCP 핸드셰이크 — SYN, SYN-ACK, ACK — 는 작아서 어떤 MTU에도 넉넉히 들어간다. ls, 로그인 프롬프트, HTTP 응답의 첫 수백 바이트: 다 작고, 다 들어가고, 다 멀쩡하다. 실제로 1500에 밀어붙이는 패킷 — 대량 전송, 큰 출력, 이미지, POST 본문 — 이 구멍에 빠진다. 연결은 망가진 게 아니다. 크기 선택적이고, 크기 선택적 실패가 한눈에 알아보게 되는 그 서명이다.

RFC 2923이 이 모든 걸 2000년에 “TCP Problems with Path MTU Discovery”라는 근사하게 건조한 제목으로 적어뒀다. 이 문제는 투표권을 가질 만큼 늙었다. 사라지지 않았다. 원인 — 반사적으로 ICMP를 막는 것 — 이 여전히, 그 때문에 전송이 멈추는 걸 한 번도 본 적 없는 사람들 사이에서 모범 사례로 취급되기 때문이다.

IPv6은 이걸 선택이 아니라 필수로 만들었다

단편화가 구해줄 거라 기대하며 이걸 피할 수 있다고 생각한다면, IPv6에 소식이 있다. IPv4에서는 DF가 안 세워져 있으면 중간 라우터가 크기 초과 패킷을 직접 조각낼 수 있기라도 하다. IPv6은 그걸 통째로 없앴다: 경로 중간의 라우터는 단편화가 금지된다. 패킷이 다음 링크에 너무 크면, 라우터가 할 수 있는 유일한 수는 버리고 ICMPv6 “Packet Too Big” 메시지(타입 2)를 돌려보내는 것뿐이다. IPv6에서 Path MTU Discovery는 있으면 좋은 게 아니라 유일한 장치이고, 구조적으로 짐을 진다.

그래서 같은 방화벽 반사를 ICMPv6에 적용하면 더 파괴적이다. ICMPv6 Packet Too Big을 막으면 성능이 떨어진 IPv6이 나오는 게 아니라, 핸드셰이크는 끝내고 모든 큰 전송을 블랙홀에 빠뜨리는 IPv6이 나온다. 같은 장비의 IPv4는 멀쩡히 돌아가니, 다들 엉뚱한 유령을 쫓게 된다. IPv6 배치가 늘수록 이 실패 방식도 같이 는다.

30초 만에 잡는 법

거창한 도구는 필요 없다. ping으로 DF 비트와 페이로드 크기를 지정할 수 있어, 손으로 경로 MTU를 이진 탐색할 수 있다. 리눅스에서:

ping -M do -s 1472 example.com

1472는 의도적이다: 페이로드 1472바이트 + ICMP 헤더 8바이트 + IP 헤더 20바이트 = 정확히 1500. 이게 성공하면 전체 경로가 1500바이트 패킷을 나른다. 타임아웃 나는데 더 작은 크기 — 예컨대 -s 1400 — 는 깔끔히 통과한다면, 찾은 것이다: 경로상 어떤 링크가 풀사이즈 패킷을 못 받는데 동시에 그 사실을 알려야 할 ICMP가 떨궈지고 있다. 정상 경로는 1472에서 성공하거나, 안 되면 실제 MTU를 알려준다. 블랙홀은 그냥 멈춘다, 조용히, 당신의 멈춘 전송이 그랬던 것과 똑같이.

진짜 해결책 둘과 해결책 아닌 것 하나

솔깃한 비-해결책은 모든 곳에서 ICMP를 열고 PMTUD가 건강하다고 선언하는 것이다. 원리상 맞고 실무상 가망 없다. 당신과 나머지 인터넷 사이의 모든 방화벽을 통제하지 못하기 때문이다. 당신이 결코 볼 일 없는 어떤 미들박스가 계속 그 메시지를 먹을 것이다.

실무에서 실제로 버티는 해결책은 MSS 클램핑이다. TCP는 핸드셰이크 중 MSS(최대 세그먼트 크기)를 협상한다 — 각 쪽이 받을 수 있는 가장 큰 덩어리를 광고한다. 더 작은 MTU 링크의 경계에 있는 라우터(VPN 집중기, PPPoE 게이트웨이)가 SYN이 지나갈 때 그 광고된 MSS를 아래로 다시 써서, 양쪽이 처음부터 실제 경로에 맞는 세그먼트 크기에 합의하게 만들 수 있다. 크기 초과 패킷이 애초에 보내지지 않으니, ICMP가 필요할 일도 없다. 깨진 메시지가 돌아오길 기대하는 대신 블랙홀을 옆으로 비켜 간다. 리눅스 방화벽에서는 규칙 한 줄 — --clamp-mss-to-pmtu — 이고, 화려하진 않지만 인터넷의 터널 대부분이 이 문제를 조용히 피하는 방식이다.

더 원칙적인 답은 RFC 4821, Packetization Layer Path MTU Discovery다. ICMP가 실패를 알려주리라 믿는 대신, 전송 계층이 직접 탐침한다 — 일부러 더 큰 패킷을 시도하고 ACK가 오는지 지켜보며, 살아남는 것으로부터 경로 MTU를 추론한다. ICMP의 도움이 전혀 필요 없고, 그게 정확히 요점이다: 진단 메시지가 없다고 가정한다. 현실 세계에서 실제로 자주 없으니까. 옳은 설계다. 그리고 20년이 지나도, 이 버그를 얼마나 철저히 죽일 수 있는지에 비하면 여전히 덜 배치돼 있다 — 계층을 제대로 고치는 것보다 MSS를 클램핑하고 넘어가는 게 얼마나 더 쉬운지를 말해준다.

당신의 모니터링이 하지 않는 주장

여기서 정말 가져갔으면 하는 게 있다. “네트워크가 살아 있다”와 “네트워크가 풀사이즈 패킷을 통과시킨다”는 서로 다른 진술이고, 당신 인프라를 지켜보는 거의 모든 것이 첫 번째만 확인한다. 가동 모니터는 호스트에 ping을 날린다 — 작은 패킷, 통과, 초록불. 헬스체크는 수백 바이트를 반환하는 엔드포인트를 친다 — 작다, 초록불. 전부 초록인데, 그 사이 모든 큰 업로드는 1500바이트 가정이 1400바이트 터널을 만나고 둘을 화해시켰을 메시지가 당신이 관리하지도 않는 어떤 방화벽에서 떨궈졌기 때문에 블랙홀에서 조용히 죽는다.

1500바이트 MTU는 인터넷이 너무 오래전에 합의해 이제는 보이지 않게 된 숫자 중 하나이고, 보이지 않는 가정은 가장 요란하게 실패하고 가장 늦게 진단되는 가정이다. 연결은 되는데 큰 전송이 멈추면, 애플리케이션부터 보지 마라. 패킷의 크기부터 봐라. 패킷이 구멍이 어디 있는지 당신에게 말해주려 애쓰는 중이니까.

토론 참여

← 블로그로 돌아가기