누군가 브라우저가 터널을 쓰도록 요청했고, 돌아온 답은 목적지 포트 80과 443에 일치하는 방화벽 규칙이다. 정확해 보이지만 정확하지 않다. 포트 일치는 패킷을 전송 헤더로 고르며, 어느 프로그램이 소켓을 열었는지로 고르지 않는다. 그리고 현대의 브라우징은 TCP의 그 두 포트에 국한되지 않는다. 그 규칙은 포함하려던 트래픽의 일부를 놓치고, 건드릴 생각도 없던 트래픽을 잡는다. 쓰기 전에 세 가지 경계 중 무엇을 실제로 원하는지 정하라.

규칙은 전송 계층의 부류를 설명할 뿐 애플리케이션이 아니다

Linux에서 목적지 포트 일치는 TCP 헤더만 살피고 그 위는 보지 않는다. 그 패킷을 표시하고 정책 경로로 유도하는 일은 문서대로 동작하며, 그 기계는 페이로드가 무엇을 뜻하는지, 어느 프로세스가 보냈는지 알지 못한다. 표시는 패킷에 붙은 번호이지 애플리케이션에 붙은 정체성이 아니다.

nft add rule inet mangle output tcp dport { 80, 443 } meta mark set 0x1
ip rule add fwmark 0x1 table 100
ip route add default dev wg0 table 100

두 가지 결과가 곧바로 따른다. TCP 443에 접속하는 소프트웨어 업데이터는 브라우저와 같은 대우를 받는데, 그것이 괜찮을 수도 있고 다음 업데이트 주기를 기다리는 장애일 수도 있다. 그리고 다른 포트, 예컨대 8443에 일부러 공개한 웹 서비스는 완전히 규칙 밖으로 떨어지는데, 그것도 사용자가 신경 쓴 웹 트래픽이다.

요구가 정말 그 포트에 관한 것이라면 번호는 정당한 선택자다. 티켓에 «브라우저»라고 적혀 있고 규칙 집합에 포트 범위가 구현되면 그것은 허구가 된다. 이 간극은 문서화의 사소한 문제가 아니다. 검토에서 방어할 수 있는 규칙과 아무도 시험할 수 없는 규칙의 차이다.

HTTP/3는 당신의 TCP 규칙을 조용히 벗어난다

HTTP/3는 QUIC 위에서 동작하고, QUIC는 UDP로 운반된다. TCP에만 일치하는 규칙은 그것을 볼 수 없으므로, 당신의 시험 페이지를 통과한 그 브라우저가 아무도 설정을 바꾸지 않은 채 이후 요청을 UDP로 보내 터널을 우회할 수 있다. RFC 9114는 HTTP/3를 QUIC 기반 프로토콜로 설명하며, QUIC는 흔한 배포에서 기본 포트 443의 UDP를 쓴다.

특정 브라우저가 HTTP/3를 시도하는지는 그 설정, 사이트, 이전 시도의 성공 여부에 달려 있다. 그래서 기억으로 따지기 나쁜 대상이다. 대신 도구에 물어라:

curl --http3 -sS -o /dev/null -w '%{http_version} %{time_total}\n' https://example.com
ss -tunp | grep -E ':443\b'

curl 줄은 협상된 버전을 출력하므로 3이 나오면 UDP 경로가 쓰였음이 확인된다. 이어서 소켓 목록은 그 연결이 터널 인터페이스에 묶였는지 물리 인터페이스에 묶였는지 보여 준다. 당신의 curl 빌드에 HTTP/3가 없으면 그 플래그는 조용히 후퇴하지 않고 크게 실패하는데, 그것이 나름의 작은 자비다.

DNS는 따로 내려야 하는 결정이다

웹 트래픽용 포트 규칙은 이름 해석에 대해 아무 말도 하지 않는다. 브라우저는 시스템 리졸버, 브라우저 안에 설정된 리졸버, 또는 마지막으로 연결한 것이 밀어 넣은 리졸버를 쓸 수 있다. 그중 무엇이든 당신의 규칙이 고른 경로와 다른 경로로 질의를 보낼 수 있고, 그래서 전형적인 분열이 생긴다. 페이지는 열리는데 이름은 예상치 못한 곳에서 해석되고, 사내 사이트는 공인 주소로 해석되거나 아예 해석되지 않는다.

이것을 명시적으로 정하라. 사내 이름을 터널로 해석해야 한다면 리졸버가 그 안에 있어야 하고, 규칙 집합은 그 리졸버로 가는 DNS를 허용하면서 다른 질의는 밖에 두어야 한다. 브라우저가 공인 사업자로 DNS over HTTPS를 쓴다면 질의는 당신의 리졸버를 완전히 우회하며 포트 규칙은 도울 수 없다. 선택을 라우팅 규칙과 같은 변경 기록에 적어라. 다음 장애를 디버깅하는 사람이 둘을 함께 읽기 때문이다.

그다음 페이지가 아니라 리졸버로 확인하라:

resolvectl query intranet.example.internal
resolvectl status | grep -A3 'Current DNS Server'

응답이 없는 것은 브라우저의 오류 메시지가 네트워크를 탓하더라도 해석 문제다. 이 두 장애는 사용자에게 똑같이 보이지만 전혀 다른 티켓이다.

세 가지 경계, 세 가지 다른 메커니즘

원하는 경계에 이름을 붙이는 순간 혼란의 대부분은 사라진다. 그것들은 서로 바꿀 수 없고, 터널이 끊길 때 각각 다른 고장 방식을 가진다.

What you actually wantMechanism that fitsWhat it does not cover
These transport ports use this exitFirewall mark plus policy routeApplications, HTTP/3 over UDP, DNS
This browsing session stays separateApplication-aware VPN, or a separate network namespace plus a proxyOther applications in the same session
Only these private sites are reachableDestination-based routes with private DNSPublic sites on the same address family

별도 브라우저 프로필은 가운데 줄의 함정이다. 쿠키와 기록을 분리할 뿐 호스트의 라우팅 테이블은 아무것도 바꾸지 않으므로 두 프로필은 같은 경로를 간다. 격리는 커널이 강제하는 것이 필요하다. 즉 네임스페이스, 컨테이너, 또는 자기 터널 인터페이스를 가진 애플리케이션이다.

어느 줄이 해당하든 터널이 떨어질 때 무슨 일이 일어나야 하는지 적어라. 인터페이스와 함께 사라지는 표시 기반 경로는 보통 물리 기본 경로로 되돌아가며, 그것이 fail-open 동작이다. 브라우징에는 받아들일 만하고 그 밖의 모든 것에는 아닐 수 있으며, 그것은 구현 세부가 아니라 결정이다.

당신이 덮는다고 말하는 흐름을 시험하라

인수 시험은 편했던 포트 번호가 아니라 당신이 적은 경계를 따라야 한다. 반드시 터널을 지나야 하는 흐름 하나, 지나서는 안 되는 흐름 하나, 생각하지 못한 전송을 쓰는 흐름 하나를 골라라:

ip route get 203.0.113.40
curl -sS -o /dev/null -w 'v4 %{http_version} %{remote_ip}\n' https://example.com
nft list ruleset | grep -A2 'dport'

경로 조회는 목적지가 실제로 어느 인터페이스를 쓰는지 알려 주며, 대부분의 보고서가 틀리는 사실이 바로 그것이다. curl 줄은 협상된 버전과 주소 체계를 확인한다. 규칙 집합 덤프는 시험 중에 추가하고 범위를 좁히는 것을 잊은 규칙을 잡아내며, 그렇게 임시 표시가 영구 fail-open 라우팅이 된다.

세 출력 모두를 시간 표시와 함께 기록하고 변경과 함께 보관하라. 나중에 누군가 «VPN이 인트라넷을 망쳤다»고 보고하면, 의도한 경계와 구현한 경계의 차이가 그 기록에 드러날 것이고, 수정은 논쟁이 아니라 규칙 변경이 될 것이다.

관련 글