컨테이너의 패킷은 tun0 에 도달하지 않으며, 컨테이너에 추가 권한을 줘도 달라지지 않으며, 게스트와 터널 사이에는 두 번의 경로 조회가 있습니다. 하나는 게스트의 네트워크 네임스페이스 안에서, 다른 하나는 프레임이 브리지에 도착한 뒤 호스트에서 일어나며, 두 번째는 일치 규칙이 다른 다른 테이블을 조회합니다. 제 기준 구성은 Linux 호스트로, OpenVPN 클라이언트가 10.8.0.10 을 tun0 에 유지하고, 172.18.0.0/16 에 Docker 브리지와 터널 쪽에서만 닿을 수 있는 사설 주소가 있습니다. 목표는 좁습니다: 그 게스트 서브넷만 터널로 나가고 호스트는 평소 업링크를 유지합니다.

컨테이너 안에서 VPN 클라이언트를 돌리는 것은 다른 설계이며, 지름길이 아닙니다. 그러면 컨테이너가 자기 터널과 자격 증명을 소유하고, 작업은 iptables에 남지 않고 Dockerfile로 옮겨갑니다.

포워딩은 목적지로 경로를 고르고, 그다음 NAT가 소스를 다시 씁니다

먼저 net.ipv4.ip_forward=1 를 설정하면, 포워딩된 패킷은 메인 테이블에서 목적지로만 라우팅됩니다. 소스는 그 결정에 관여하지 않습니다. 경로가 선택된 뒤에야 커널이 POSTROUTING 를 실행하며, 바로 그곳에 Docker 자체 규칙이 있습니다: -s 172.18.0.0/16 ! -o br-appnet -j MASQUERADE. 브리지를 떠나 다른 인터페이스로 가는 것은 모두 변환되고, 어떤 주소가 되는지는 경로가 보낸 인터페이스에 달려 있습니다.

이 규칙이 제게 자주 묻는 증상 대부분을 설명합니다. 메인 테이블에 사설 목적지 경로가 없으면 패킷은 호스트의 물리 주소를 달고 업링크로 나갑니다. 제공자가 목적지를 버리거나, 원격 사이트가 예상하지 못한 RFC 1918 소스를 버리면, 터널은 멀쩡히 살아 있는데 컨테이너는 시간 초과가 됩니다. tun0 에서 캡처하면 아무것도 보이지 않으며, 그것이 신호입니다. 경로가 정말 tun0 을 가리키면 MASQUERADE가 소스를 10.8.0.10 로 다시 쓰고, 같은 컨테이너가 아무것도 건드리지 않고 동작합니다.

게스트 소스를 지목하는 조회를 요청하세요. from 과 iif 없이는 호스트가 자기 소켓에 대해 답하고, 포워딩된 패킷이 절대 지나지 않을 경로를 보고합니다:

ip route get 192.168.11.222 from 172.18.0.5 iif br-appnet

그 답이 업링크를 지목하면 터널은 그림에 없고, 터널 쪽 설정은 도움이 되지 않습니다.

게스트를 터널 자신의 주소로 옮기기

가장 작은 유효한 변경은 나가는 지점에서 소스를 명시적으로 다시 쓰는 것입니다. 덧붙이지 말고 삽입하세요: Docker는 이미 POSTROUTING 의 맨 앞에 앉아 있으므로 -A 로 추가한 규칙은 Docker 규칙 뒤에서 평가되어 결코 발동하지 않습니다.

iptables -t nat -I POSTROUTING 1 -s 172.18.0.0/16 -o tun0 -j SNAT --to-source 10.8.0.10

이것이 유지될지는 두 조건이 결정합니다. 주소는 클라이언트가 실제로 보유한 것이어야 하므로, 클라이언트별 항목에 ifconfig-push 10.8.0.10 255.255.255.0 을 포함해 고정하고, 한 번도 할당되지 않은 주소를 지목하는 규칙은 오류가 아니라 조용한 드롭을 만듭니다. 그리고 원격 쪽은 그 소스를 받아들이고 응답을 되돌려 라우팅해야 하므로, 그곳 허용 목록에는 게스트 서브넷이 아니라 터널 주소를 적습니다. 클라이언트 주소가 동적이면, -j MASQUERADE 과 -o tun0 로 인터페이스가 가진 값을 따르지만, 고정 값에 맞춰 쓸 수 없는 원격 정책이라는 대가가 있습니다.

필터링이 나머지 절반입니다. Docker는 FORWARD 정책을 drop으로 두고 데몬이 재시작될 때마다 자기가 소유한 체인을 비우므로, 로컬 정책은 DOCKER-USER 에 넣으세요(이것은 살아남습니다):

iptables -I DOCKER-USER -i br-appnet -o tun0 -j ACCEPT
iptables -I DOCKER-USER -i tun0 -o br-appnet -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

돌아오는 규칙이 중요합니다: tun0 에 도착하는 응답은 브리지로 연 연결에 대해 나가는 방향이 허용되었다는 이유만으로 허용되지 않습니다.

소스 규칙으로 호스트를 직접 유지하기

브리지 전체가 터널을 써야 하지만 호스트의 다른 무엇도 쓰면 안 될 때는 NAT를 건너뛰고 그 소스에 자체 테이블을 주세요. 게스트 주소는 그대로 처음부터 끝까지 보입니다.

echo "200 vpnbox" >> /etc/iproute2/rt_tables
ip rule add from 172.18.0.0/16 lookup vpnbox pref 1000
ip route add default dev tun0 table vpnbox

규칙은 메인 테이블보다 먼저 조회되고, from 가 소스와 일치합니다. 터널 자체의 바깥 패킷은 호스트의 물리 소스를 실어 나르므로 from 172.18.0.0/16 과 절대 일치하지 않고, 터널은 스스로를 안으로 되돌려 라우팅할 수 없습니다. 이 자기 배제가 구성을 안정적으로 만듭니다.

돌아오는 경로에는 이제 도움이 필요합니다. 원격 쪽은 변환되지 않은 172.18.0.x 을 보고 그것이 어디에 있는지 알 이유가 없습니다. 서버에서는 클라이언트별 파일에 iroute 172.18.0.0 255.255.0.0 이 필요해 서버의 내부 테이블이 그 프리픽스를 이 클라이언트에 넘기고, 푸시되는 route 172.18.0.0 255.255.0.0 이 필요해 다른 클라이언트가 그것이 있음을 압니다.

믿기 전에 두 가지 확인을 합니다. 실행할 것은 ip route show default 입니다: redirect-gateway def1 로 테이블에는 0.0.0.0/1 과 128.0.0.0/1 이 tun0 을 통해 들어 있고, 호스트는 전혀 직접적이지 않습니다—그 절반짜리 기본 경로가 default 을 대체하지 않고 이기기 때문입니다. ip rule add 로 추가한 규칙도 재부팅 때 사라지므로 up 스크립트에 담으세요.

패킷 크기는 두 번째로 만나는 실패입니다

1500바이트 브리지 인터페이스는 바깥 UDP와 IP 헤더가 붙으면 터널이 감당할 수 있는 양보다 더 넘깁니다. 작은 교환은 지나가므로 DNS가 풀리고 TLS 핸드셰이크가 끝나지만, 그다음 큰 업로드는 어디에서도 오류 없이 멈춥니다. 보낸 쪽은 같은 세그먼트 크기를 계속 재시도합니다—줄이라고 알려 줄 ICMP 단편화 필요 메시지가 결코 도착하지 않기 때문입니다.

인터페이스에서, MSS에서, 또는 둘 다에서 고치세요. 클램핑에는 게스트의 협조가 필요 없습니다:

ip link set dev tun0 mtu 1400
iptables -t mangle -A FORWARD -o tun0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

클램핑은 TCP만 다시 씁니다. UDP, QUIC 포함은 대신 게스트 인터페이스를 낮춰야 하며, --opt com.docker.network.driver.mtu=1400 을 네트워크를 만들 때나 해당 compose 키로 지정합니다.

DNS와 반복할 가치가 있는 점검

이름 해석도 같은 경로를 따릅니다. Docker의 내장 리졸버는 127.0.0.11 에서 호스트의 설정된 서버로 전달하므로, 컨테이너는 호스트가 쓰는 것을 물려받습니다. 사설 영역이 터널로만 닿는 리졸버에서만 응답되면, 게스트를 그 주소로 향하게 하고 getent hosts 로 컨테이너 안에서 시험하세요(호스트에서가 아닙니다).

  • 실패하는 요청은 게스트 안에서 실행하세요—호스트 쪽 시험은 다른 소스 주소를 쓰고 문제를 가립니다.
  • 추가한 NAT와 필터 규칙의 카운터를 읽으세요. 일치가 없는 규칙은 라우팅 실수이지 정책 실수가 아닙니다.
  • SNAT 규칙의 터널 주소가 클라이언트가 실제로 보유한 것인지 확인하세요.
  • 멈춤을 서버 탓으로 돌리기 전에 게스트 MTU를 낮추거나 MSS를 클램프하세요.

목록을 따라 내려가 첫 실패에서 멈추세요. 고칠 것은 그 계층이고, 더 아래에 무엇을 더해도 끊어진 경계를 가릴 뿐입니다.

관련 글