호스트 VPN을 무시하는 Docker 및 WSL 트래픽 수정
컨테이너와 WSL은 자체 경로, 리졸버, 전달 경로를 유지하므로 호스트 터널이 정상이어도 오래된 DNS나 출구 없이 남습니다. 각 계층을 수정하십시오.
올봄 어느 아침의 대부분을 내부 이름 하나도 확인하지 못하는 컨테이너를 쫓으며 보냈습니다. 옆의 브라우저는 아무 문제 없이 위키를 열고 있었고, 터널은 올라와 있었으며 브라우저가 그것을 증명했습니다. 그런데 컨테이너는 다른 라우팅 테이블과 다른 리졸버를 가진 다른 네트워크 네임스페이스에 있었습니다. 다음은 지금 제가 확인하는 순서입니다. 먼저 라우팅 테이블, 다음 이름 확인, 세 번째 전달, 마지막으로 호스트 플랫폼입니다. 이미 실패하는 첫 단계에서 멈추십시오. 스택 아래쪽에 규칙을 추가하면 경계를 옮기는 대신 숨기게 됩니다.
규칙을 바꾸기 전에 세 라우팅 테이블을 비교하십시오
Docker 기본 브리지의 컨테이너에는 docker0 게이트웨이를 향하는 기본 경로가 하나뿐이고 tun0의 존재를 모릅니다. 호스트에는 tun0, VPN이 밀어 넣은 경로, 그리고 예상과 다른 인터페이스로 검사 트래픽을 내보내는 정책 규칙이 있을 수 있습니다. WSL은 자체 가상 어댑터 뒤에 세 번째 테이블을 추가합니다. 어느 것이든 편집하기 전에 세 개를 모두 출력하십시오.
| Context | Command | What a wrong answer looks like |
|---|---|---|
| Host | ip route get 10.8.0.20 | Leaves a physical interface instead of tun0 |
| Container | ip route | Only 172.17.0.0/16 via 172.17.0.1 |
| Container DNS | cat /etc/resolv.conf | A public resolver, or an upstream the tunnel never pushed |
| WSL | ip addr; ip route | Only the WSL subnet, no Windows VPN adapter |
컨테이너의 유일한 경로가 브리지이고 그 네임스페이스에 tun0이 전혀 나타나지 않는다면 아래의 무엇도 도움이 되지 않습니다. 네임서버 변경은 경로를 만들지 않고, 경로는 피어가 받아들일 터널 ID를 만들지 않습니다. 셸 안에서 보면 DNS 실패와 경로 실패가 똑같이 보이기 때문에 순서가 중요합니다.
리졸버를 공용이 아니라 터널로 지정하십시오
호스트에서 resolvectl status 는 링크별 DNS 서버를 나열하고, readlink -f /etc/resolv.conf 는 그 파일이 systemd 스텁인지, NetworkManager 파일인지, 공급업체 클라이언트가 다시 쓴 것인지 알려 줍니다. 사용자 정의 네트워크의 컨테이너 안에서는 /etc/resolv.conf 가 127.0.0.11 를 가리킵니다. Docker의 내장 리졸버이며 데몬이 구성된 업스트림으로 전달합니다. 반면 기본 브리지의 컨테이너는 생성될 때 호스트 파일의 사본을 받아 그대로 유지합니다.
그 사본 때문에 컨테이너가 시작된 뒤 올라온 터널이 컨테이너를 예전 업스트림에 계속 질의하게 만듭니다. VPN 리졸버를 명시적으로 넘기거나 docker run --dns 10.8.0.53, 혹은 "dns": ["10.8.0.53"] 에 /etc/docker/daemon.json 를 설정하고 데몬을 다시 시작하거나, 터널이 올라온 뒤 컨테이너를 다시 만드십시오. 다시 만드는 편이 한 시간 전에는 맞았던 리졸버를 디버깅하는 것보다 보통 쌉니다.
공용 리졸버로 바꾸는 것은 증상을 유지하는 수리입니다. 인터넷은 확인되지만 모든 사설 영역에서 실패하며, 터널이 존재하는 이유인 필터링을 조용히 우회할 수 있습니다. 실행 중인 컨테이너의 /etc/resolv.conf 안에 쓰려고 하면 Device or resource busy 를 예상하십시오. Docker가 그 파일을 bind 마운트하므로 편집하려면 먼저 마운트 해제가 필요합니다. --dns 를 생성 시에 설정하는 것이 재시작을 견디는 순서입니다.
컨테이너가 터널을 빌릴 때는 전달하고 변환하십시오
기본 브리지에서는 호스트가 docker0와 tun0 사이의 라우터이며 기본적으로 두 작업 모두 하지 않습니다. 전달을 켜고 소스 주소를 변환해 피어가 되돌아갈 경로가 없는 주소 대신 호스트의 터널 주소를 보게 하십시오(tun0은 클라이언트가 인터페이스에 붙이는 이름으로 바꾸십시오):
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
iptables -A FORWARD -i docker0 -o tun0 -j ACCEPT
iptables -A FORWARD -i tun0 -o docker0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTFORWARD 규칙은 전달 정책이 DROP인 호스트에서 중요합니다. Docker는 자체 체인을 삽입하므로 읽는 대신 패킷으로 시험하십시오: iptables -S: 컨테이너에서 ip route get 10.8.0.20 는 브리지 게이트웨이를 가리켜야 하고, nc -vz 10.8.0.20 443 는 리졸버가 아니라 터널에서 연결되거나 시간 초과되어야 합니다.
MASQUERADE가 없으면 피어는 172.17.0.0/16으로 돌아갈 경로가 필요하며, 대부분의 원격 접속 VPN은 이를 밀어 넣지 않습니다. 여기서 정책 기반 클라이언트가 계획을 깨뜨리기도 합니다. 특정 UID나 cgroups의 트래픽만 잡으면 전달 규칙이 터널 안의 패킷을 결코 보지 못하며, 수정은 iptables가 아니라 클라이언트의 라우팅 정책에 속합니다. 손으로 추가한 규칙은 재부팅 시 사라지므로 systemd 유닛이나 netfilter-persistent 테이블에 넣고 재부팅 후 다시 시험하십시오. 늦게 올라온 터널이 이를 쓸어 버릴 수 있습니다.
목표가 격리라면 터널을 컨테이너 안에 두십시오
하나의 컨테이너가 특정 노드로 나가야 한다면 그 컨테이너에서 클라이언트를 실행하십시오. 그러면 --cap-add NET_ADMIN 와 --device /dev/net/tun 가 필요하며, 그때 자체 tun0, 경로, 방화벽을 갖습니다. 다른 컨테이너가 그를 통과한다면 컨테이너 안에 전달과 MASQUERADE 규칙도 필요하고, 그 피어들은 그를 가리키는 명시적 경로나 프록시가 필요합니다. Docker 기본 브리지는 그 경로를 대신 추가해 주지 않습니다.
호스트 네트워킹, docker run --network host 은 저렴한 버전입니다. 컨테이너가 호스트 네임스페이스에 들어가 터널 경로를 직접 봅니다. 다만 네트워크 격리를 포기하고 포트 충돌을 부르며 실제로는 Linux 선택지입니다. 게시된 포트는 대체가 아닙니다. -p 8080:80 는 서비스를 호스트에서 접근 가능하게 하지만 내부 10.x 주소를 컨테이너 안에서 접근 가능하게 하지는 않으며, 둘을 혼동하는 것이 라우팅 문제를 노출된 데이터베이스 포트로 바꾸는 길입니다.
이름 실패와 겹치는 서브넷을 구분하십시오
Docker 기본 브리지는 172.17.0.0/16이고 많은 기업 VPN 풀이 172.16.0.0/12 안에 있습니다. 터널이 그 범위를 가지면 컨테이너로 향하던 트래픽이 대신 터널로 라우팅될 수 있습니다. 이름이 실패하고 오류는 DNS처럼 보이며 문제는 주소입니다. 리졸버가 아니라 목적지를 확인하십시오: ip route get 172.17.0.5 를 호스트에서 실행한 뒤 브리지 접두사를 VPN 풀 및 docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' 과 비교하십시오.
수리는 범위 하나를 옮기는 것입니다. daemon.json 에서 Docker의 기본 주소 풀을 바꾸거나 집선 장치의 풀을 바꿉니다. 여기서 네임서버를 하나 더 추가해도 아무 효과가 없고 ssh -b 도 마찬가지입니다. 클라이언트를 인터페이스에 묶으면 소스 주소가 바뀔 뿐 터널로 향하는 목적지 경로는 바뀌지 않습니다. 빠른 분업은 이렇습니다. 호스트에서는 확인되지만 컨테이너에서는 안 되는 이름은 리졸버 문제이고, 컨테이너에서 멈추지만 호스트에서 되는 직접 IP 연결은 경로나 전달 체인 문제입니다.
WSL과 Docker Desktop은 Linux 규칙을 따르지 않습니다
기본 NAT 모드에서 WSL은 자체 가상 어댑터 뒤에 있고 Windows VPN의 내부 경로나 DNS 접미사를 받지 않습니다. 그래서 브라우저는 되고 ping 는 배포판 안에서 안 됩니다. 최근 Windows 11 빌드에서 제공되는 미러 네트워킹은 대신 호스트 인터페이스를 공유하지만, .wslconfig 에서 켜고 배포판을 wsl --shutdown 로 다시 시작해야 합니다. 수정을 적용하기 전에 wslinfo --networking-mode 로 활성 모드를 확인하십시오. NAT 답은 미러 모드에 틀리고 그 반대도 마찬가지입니다.
Docker Desktop은 계층을 하나 더 얹습니다. Windows의 Linux 컨테이너는 역사적으로 vpnkit이라 불린 NAT 구현 뒤에 있고 경로가 네이티브 Linux 호스트와 다릅니다. Windows 컨테이너는 Hyper-V 가상 스위치를 통해 연결되며, 기업 클라이언트가 그 스위치의 트래픽을 막을 수 있어 같은 머신의 Linux 컨테이너는 네트워크에 닿아도 Windows 컨테이너는 아무것도 못 할 수 있습니다. 물리 어댑터에 묶인 외부 스위치가 흔한 우회책이며, 정책이 허용하지 않을 수 있는 방식으로 컨테이너의 네트워크 정체성을 바꿉니다.
다른 규칙을 쓰기 전에 네 계층을 순서대로 짚고 틀린 곳에서 멈추십시오:
- 컨테이너 안에서:
ip route가 사설 접두사에 도달하는지, 그리고/etc/resolv.conf가 그것을 확인하는 업스트림을 가리키는지. - 호스트에서: 전달이 켜져 있는지, NAT가 터널 인터페이스에 묶여 있는지, 전달 체인이 브리지에서 터널로 허용하는지.
- 피어에서: 되돌아갈 경로를 아는지, 아니면 컨테이너 서브넷을 숨기려고 NAT에 의존하는지.
- 플랫폼에서: WSL이 NAT인지 미러 모드인지, 컨테이너가 Linux인지 Windows인지.
모든 시험을 새 컨테이너나 셸에서 다시 실행하십시오. 캐시된 답이나 이미 맺어진 연결이 있는 오래 사는 프로세스는 구성이 맞은 뒤에도 계속 실패하며, 그 실패는 효과가 없던 수리와 똑같이 보입니다.