아이패드에 Tailscale이 켜져 있었다: WebRTC 디버깅기
아이패드에서만 특정 웹앱이 하얀 화면으로 멈추는 원인을 추적하는 과정을 담은 기술 블로그 글입니다. WebRTC 핸드셰이크는 정상적으로 완료되었으나 데이터 채널을 통해 전송된 일정 크기 이상의 청크가 아이패드 브라우저에 도달하지 못하는 문제였습니다. 최종적으로 VPN이었던 Tailscale의 MTU 설정과 WebRTC 라이브러리의 하드코딩된 버퍼 상수가 결합된 '트렌치코트를 입은 두 마리의 벌레' 같은 복합 버그가 원인으로 밝혀졌습니다.
← 모든 글
아이패드에 Tailscale이 켜져 있었다 2026-06-10
p2claw가 어떻게 작동하는지 잘 모른다면, 이 글을 읽기 전에 '작동 원리' 블로그 글을 먼저 확인해 보는 것이 좋습니다.
아이패드에서 p2claw 앱 중 하나를 열었더니 빈 페이지가 나왔습니다. 동일한 URL은 Mac, 리눅스 박스, 그리고 휴대폰에서는 정상적으로 작동했습니다. 같은 Wi-Fi, 같은 브라우저 엔진, 같은 네트워크 환경이었습니다. 좋은 탐정 소설처럼, 우리는 [아이패드, WebKit, Tailscale] 등 무려 여러 명의 용의자를 떠올렸고, 그들이 모두 무죄라는 결론을 내렸습니다. 적어도 그런 줄 알았죠.
결과적으로 이는 트렌치코트를 입고 서 있는 두 마리의 벌레(two bugs wearing a trenchcoat)였습니다. webrtc-rs의 하드코딩된 상수와, 우리의 완고한 끈기 끝에 발견한 Tailscale의 단 한 줄짜리 설계 결정이었습니다. 우리는 당일 바로 우회 패치를 적용했지만, 실제로 무엇을 패치한 것인지 이해하는 데는 2주가 더 걸렸습니다.
불만 사항 앱이 로딩 상태를 렌더링할 만큼의 HTML을 불러온 다음 멈췄습니다. 콘솔에 관련된 에러는 없었고, 서비스 워커(Service Worker)는 등록되었으며, WebRTC 핸드셰이크는 완료되었고, 데이터 채널이 열렸습니다 [ dc.readyState === "open" ]. 그리고 아무 일도 일어나지 않았습니다. 브라우저는 데이터 채널을 통해 첫 번째 GET / 요청을 보냈고 응답을 영원히 기다렸습니다. 반대편에 있는 박스 에이전트는 모든 것이 정상이라고 생각했습니다. 응답을 처리하고 바이트들을 채널에 밀어 넣었으니까요. 그저 그 데이터가 아이패드에는 결코 도달하지 않았을 뿐이었습니다. 여기서 끝이 아니라, 이건 하이젠버그(Heisenbug)였습니다. 미친 듯이 새로고침을 하면 가끔씩 페이지가 로드되기도 했습니다.
의심이 들 때는 계측(Instrument)을 하라 우리가 한 번째 유용한 일은 양쪽 연결 끝점의 로그를 남기고 시간별로 정렬하는 것이었습니다. 박스가 보낸 모든 청크, 브라우저가 받은 모든 청크, 그리고 가장 중요한 전달 확인을 대기하며 박스가 아웃바운드 버퍼에 쥐고 있던 데이터의 양을 확인했습니다. 이를 통해 데이터가 어느 지점에서 상대방에게 전달되지 못하고 있는지 파악할 수 있었습니다.
막다른 길 WebRTC 핸드셰이크를 포함해 모든 것을 의심해 보고 배제한 후, 우리는 지푸라기라도 잡는 심정이었습니다. WebRTC와 관련된 몇 가지 제한을 확인하고 네트워크 안정성을 두 번씩이나 점검했습니다.
메시지 크기 제한. WebRTC에서는 두 피어가 데이터를 교환하기 전에 각각 수용할 수 있는 가장 큰 단일 청크 크기에 합의합니다. 이보다 큰 데이터를 보내면 일부 브라우저는 조용히 연결을 끊어버립니다. 우리는 두 기기에서 이 제한( maxMessageSize )을 읽어들였습니다. 아이패드는 Mac과 정확히 똑같이 64kb를 보고했으며, 이는 우리가 보내던 7~8kb 크기의 청크보다 훨씬 컸습니다. 이후 우리는 메시지 청크 크기가 범인이라는 생각을 버리게 되었고, 이는 진짜 원인을 파악하는 것을 더 어렵게 만들었습니다.
불안정한 Wi-Fi. 가장 단순한 설명은 공기 중에 패킷이 손실되었다는 것입니다. 박스에서 ifstat과 tcpdump를 확인한 결과 문제가 없었고, (같은 Wi-Fi를 사용하는) 제 휴대폰에서도 같은 문제가 나타나지 않았습니다. 이는 아이패드에 특정된 문제여야 했지만, 그게 뭔지 알 도리가 없었습니다.
숫자들이 실제로 말해준 것 요청당 박스는 세 개의 청크를 보냈습니다. 220바이트 헤더, 7,874바이트 본문, 그리고 199바이트의 꼬리였습니다. 새로운 계측 도구는 발신자의 아웃바운드 버퍼가 약 8kb까지 올라간 뒤 멈추는 것을 보여주었습니다. 발신은 했지만 도착 확인을 영원히 받지 못한 본문 데이터를 쥐고 있었던 것입니다. 아이패드를 새로고침했을 때도 완전히 똑같은 패턴이 반복되었습니다. WebRTC 데이터 채널은 패킷 손실이 발생하기 쉬운 UDP 위에서 순서를 보장하는 전송을 수행하므로, 하나의 청크가 누락되면 그 이후의 모든 메시지가 차단(block)됩니다. 아이패드의 브라우저 JS 콘솔에서는 오직 하나의 청크(220바이트 헤더)만 수신된 것을 확인할 수 있었고 이후로는 아무것도 없었습니다. 본문이나 이후 요청의 작은 헤더들도 보이지 않았습니다. iOS의 모든 브라우저가 내부적으로 WebKit을 사용한다는 점을 생각하여 Mac의 Safari에서 테스트를 진행해 보았지만, Mac은 8kb 및 11kb 청크를 아무런 문제 없이 받았습니다.
"범인은 Tailscale이었다" 2시간 동안 WebKit 관련 가설을 세우던 중, 나는 Mac과 달리 아이패드에는 Tailscale이 활성화되어 있다는 사실을 깨달았습니다. Tailscale은 일종의 VPN이며, VPN은 트래픽을 추가적인 레이어로 감싸 패킷 하나당 담을 수 있는 데이터의 공간을 줄여버립니다. 결국 Mac에 비해 아이패드로 전송될 때 커다란 응답들이 더 많고 작은 조각으로 쪼개지게 된 것입니다. WebKit은 유저스페이스에서 데이터 채널을 직접 구현하며, incl