호스트의 브라우저는 VPN 출구 주소를 보여 주고, 컨테이너는 집 인터넷 연결을 보고하며, 누군가는 그것을 유출이라고 부릅니다. 아무것도 유출되지 않았습니다. 호스트 터널은 호스트 네트워크 네임스페이스에 속한 인터페이스, 경로, 규칙의 묶음이고, 브리지 위의 컨테이너는 자체 기본 경로를 가진 다른 네임스페이스입니다. VPN 클라이언트가 몇 주째 연결되어 있는데도 컨테이너 패킷이 한 번도 그 안으로 들어가지 않은 Compose 스택에 저는 앉아 있었습니다.

트레이 아이콘보다 먼저 네임스페이스를 확인하십시오

네임스페이스가 이 논의의 전부입니다. Docker가 브리지 네트워크에서 컨테이너를 시작하면 네트워크 네임스페이스를 만들고, veth 쌍의 한쪽 끝을 그 안에 붙이고, 그 네임스페이스에 주소와 기본 경로를 줍니다. 터널 인터페이스는 이름이 무엇이든 호스트 네임스페이스에 존재합니다. 컨테이너 안의 트래픽은 그것을 볼 수 없고, 어떤 클라이언트 설정도 바깥에서 그것을 바꾸지 못합니다.

격리에서 빠져나오는 지원되는 방법은 두 가지입니다. --network host는 컨테이너를 호스트 네임스페이스 안에 두므로 호스트 경로와 따라서 터널을 물려받습니다. --network container:vpn는 다른 컨테이너의 네임스페이스를 공유하며, 대부분의 사이드카 설계가 워크로드를 VPN 컨테이너로 보내는 방식이 이것입니다. 둘 다 명시적 선택이며, 보통은 둘 중 하나만 배포하는 사람이 의도한 것입니다.

호스트 모드는 공짜 업그레이드가 아닙니다. 컨테이너는 자기 주소를 잃고, 포트 게시가 적용되지 않으며, 그곳에서 NET_ADMIN를 가진 프로세스는 호스트 라우팅을 다시 쓸 수 있습니다. 그것을 받아들이는 것이 옳은 결정일 수 있습니다. 이미 켜져 있다고 가정하는 것이, 경로에 한 번도 들어간 적 없는 터널을 밤새 디버깅하는 이유입니다.

패킷이 브리지를 벗어나는 과정을 따라가십시오

브리지 컨테이너는 자기 패킷을 기본 게이트웨이로 보내며, 그것이 호스트의 브리지 인터페이스입니다. 그러면 호스트가 패킷을 전달하고, 기본 구성에서는 패킷이 나가기 전에 출발지 주소를 자기 주소로 변환합니다. 바로 그 변환 단계가 정책을 조용히 무력화합니다.

패킷이 터널 끝점에 도달할 때쯤이면 호스트 주소를 지니고 있습니다. 컨테이너의 원래 주소를 기준으로 한 규칙, 정책 경로, 분할 터널 결정은 더 이상 일치하지 않고, 서로 다른 출구로 나가야 하는 두 컨테이너를 출발지만으로 구분할 수 없습니다. 컨테이너별 라우팅이 필요하다면 결정은 변환 전에, 또는 당신이 통제하는 네임스페이스 안에서 이루어져야 합니다.

Docker는 이 규칙들을 스스로 유지하며, 데몬이 어떤 방화벽 백엔드로 구성되었든 그 백엔드를 통해 관리합니다. 전달되는 트래픽을 위한 지원되는 훅 밖에서 손으로 쓴 규칙, DOCKER-USER, 은 데몬이 다시 시작되거나 호스트가 업그레이드될 때 재정렬되거나 사라질 수 있습니다. Docker의 패킷 필터링 문서는 체인 순서를 보여 줍니다. 실무적 요점은 모든 규칙에 소유자와, 실제로 시험해 본 재적재 경로가 필요하다는 것입니다.

두 시스템을 따로 시험하십시오

한 네임스페이스의 증거는 다른 쪽에 대해 아무것도 말해 주지 않습니다. VPN 컨테이너 안에서 실행한 요청은 그 컨테이너에 대해 터널이 작동함을 증명합니다. 그것은 호스트에 대한 증거가 아니며, 같은 명령을 --network host 컨테이너에서 실행한 것은 브리지에 대한 증거가 아닙니다.

# host namespace
ip route get 1.1.1.1
ip -brief addr show

# a fresh container on the same bridge network
docker run --rm --network appnet alpine sh -c 'ip route; cat /etc/resolv.conf; ip -6 route'

그다음에는 비슷한 이미지가 아니라 실제 애플리케이션 컨테이너 안에서 시험을 반복하십시오. 앱은 추가 경로, 프록시 환경 변수, 다른 리졸버를 지닐 수 있고, 그 각각이 답을 바꿉니다. 매번 이름 해석과 두 번째 주소 체계를 확인하십시오. 사용자 정의 브리지의 컨테이너는 Docker 내장 리졸버를 통해 127.0.0.11에서 해석되며, 브리지의 IPv6 기본 경로는 당신이 IPv4 전용 터널을 시험하느라 바쁜 동안 트래픽을 밖으로 내보낼 수 있습니다.

자격 증명을 만지기 전에 패턴을 읽으십시오

호스트는 되고 컨테이너는 안 될 때 원인은 거의 항상 구조적이며, 키를 돌리거나 클라이언트를 다시 설치하는 것은 오후 하나를 낭비합니다. 먼저 증상을 분류하면 수정은 보통 스스로 이름을 말합니다.

What you seeUsual causeFirst check
Host exits through the VPN, container shows the ISP addressThe container never entered the tunnelDefault route inside the container
Container works until the daemon restarts, then stopsRules outside the supported hook, or a changed backendDaemon's reported firewall backend and DOCKER-USER
Container loses internal hosts after the tunnel comes upVPN prefix overlaps the Docker bridge subnetip route on the host, then docker network inspect
Small replies arrive, large ones hangTunnel MTU or missing MSS clampingLink MTU on both ends of the tunnel
Container falls back to the ISP address when the tunnel dropsNothing enforces the kill switch for forwarded trafficStop the tunnel with a request in flight

겹침 행은 자기 문장을 받을 자격이 있습니다. 172.17.0.0/16으로, 또는 우연히 브리지 접두사를 포함하는 10.0.0.0/8로 경로를 밀어 넣는 VPN 프로필은 터널이 컨테이너 서브넷을 주장하게 만들고, 변환된 주소로 가는 응답은 잘못된 방향으로 가거나 사라집니다. Docker 네트워크의 번호를 다시 매기는 것이 예외를 쌓는 것보다 보통 더 깔끔합니다.

적용 범위를 명시하고, 그다음 일부러 깨뜨리십시오

검토를 견디는 설계는 두 가지입니다. VPN 컨테이너의 네임스페이스를 공유하면 적용 대상 워크로드가 모두 재시작 경계 하나를 가진 터널 하나를 쓰며, 한 서비스가 다른 출구나 자체 DNS 경로를 필요로 하기 전까지는 단순합니다. 또는 컨테이너 네트워크를 호스트의 VPN 게이트웨이로 라우팅하십시오. 전달을 켜고, 브리지 접두사를 터널로 향하게 하고, 반환 트래픽을 명시적으로 처리합니다. 그러면 네트워크별 통제를 얻는 대신 데몬과 클라이언트 업그레이드를 거치며 유지해야 할 규칙 집합을 떠안습니다.

어느 쪽이든 중요한 시험은 실패 상황입니다. 컨테이너 요청이 날아가는 중에 터널을 멈추고 패킷이 어디로 가는지 보십시오. 직접 경로로 되돌아간다면 적용 범위는 한 번도 강제된 적이 없습니다. 클라이언트의 kill switch는 클라이언트 자신의 트래픽을 다스릴 뿐 브리지에서 전달되는 패킷을 다스리지 않기 때문입니다. 그 뒤 데몬과 호스트를 다시 시작하고 결과를 두 번째로 확인하십시오. 손으로 쓴 규칙이 사라지는 순간이 바로 그때입니다.

끝났다고 말하기 전에 네 가지를 확인하십시오. 터널 인터페이스가 트래픽과 같은 네임스페이스에 있는지, 컨테이너 시험이 실제 컨테이너 안에서 돌았는지, 이름 해석과 두 주소 체계를 확인했는지, 터널 실패 시험이 여전히 닫힌 채로 실패하는지. 그다음 방화벽 백엔드, 클라이언트 버전, 각 규칙의 소유자를 적어 두십시오. 그 기록이 다음 업그레이드에서 오래된 가정이 문서화되지 않은 채 섞여 남는 것을 막습니다.

관련 글