Docker VPN 컨테이너를 통해 호스트 트래픽을 라우팅하는 방법
VPN 컨테이너는 프록시가 아니라 네트워크 네임스페이스입니다. 공유 netns, 실제 SOCKS 리스너 또는 라우팅된 게이트웨이를 고른 뒤 출구 경로를 확인하세요.
터널 컨테이너는 동작하고, docker compose exec vpn curl -s https://ifconfig.me는 공급자 주소로 응답하지만, 데스크톱의 브라우저는 여전히 ISP를 통해 나갑니다. 이 간극은 예상된 것입니다. VPN 클라이언트를 실행하는 컨테이너는 네트워크 네임스페이스, 라우트, 터널 인터페이스를 보유할 뿐 HTTP나 SOCKS를 말하는 것은 없으며, 그 위에 포트를 게시한다고 해서 없던 리스너가 생기지는 않습니다.
클라이언트가 어디서 실행되는지가 설계를 결정합니다. 형제 컨테이너는 터널의 네임스페이스로 들어갈 수 있지만 데스크톱 애플리케이션은 그럴 수 없고, 라우터에는 프록시 필드가 아니라 라우트가 필요합니다. 매핑을 잘못하면 증상은 같습니다. 연결 거부, 또는 조용히 옛 경로로 가는 트래픽입니다.
클라이언트가 실행되는 위치에 구성을 맞추세요
Compose를 편집하기 전에 한 가지를 물으세요. 클라이언트를 VPN 컨테이너의 네트워크 네임스페이스 안에서 시작할 수 있습니까? 이미 Linux 컨테이너로 실행되는 것은 가능하지만, 책상 위의 브라우저, IDE, CLI는 프록시 설정이 프로토콜 서비스를 기대하지 네임스페이스를 기대하지 않으므로 불가능합니다.
| Client | Arrangement | What must already exist |
|---|---|---|
| Another Compose service | Shares the vpn namespace via network_mode: service:vpn | A working tunnel and correct start order |
| Host browser, IDE or CLI | Proxy listener inside the namespace, published to host loopback | A configured SOCKS or HTTP proxy and its access rules |
| Router, VM or phone | The container as a layer-3 gateway | Forwarding, NAT and a return route |
이 셋은 섞이지 않습니다. 컨테이너의 브리지 주소를 가리키는 HTTP 프록시 필드는 그곳에 프록시가 듣고 있지 않으면 아무것도 닿지 못하고, 프록시 리스너는 라우터의 트래픽을 라우팅하지 않습니다. 먼저 결정하고 해당 절을 따르세요.
터널 네임스페이스를 형제 컨테이너와 공유하세요
프록시 프로토콜이 개입하지 않으므로 가장 저렴한 선택입니다. 앱은 vpn 서비스의 네임스페이스에 들어가 그 인터페이스, 라우트, 포트 공간을 물려받습니다:
services:
vpn:
image: your-vpn-client
cap_add:
- NET_ADMIN
healthcheck:
test: ["CMD-SHELL", "ip link show tun0 >/dev/null"]
interval: 10s
retries: 5
app:
image: your-app
network_mode: service:vpn
depends_on:
vpn:
condition: service_healthy네임스페이스가 다른 서비스에 속한다는 사실에서 세 가지 제약이 따릅니다. 앱은 붙을 네임스페이스가 없으므로 자체 networks 키를 받지 못합니다. 포트 게시는 네임스페이스가 사는 vpn의 몫입니다. 그리고 vpn을 다시 만들면 앱이 들어간 네임스페이스가 파괴되므로 docker compose up -d --force-recreate vpn는 app도 다시 만들라고 남기며, 단순 재시작으로는 충분하지 않을 때가 있습니다. Compose 서비스 참조가 이 구성과 포트 게시 제약을 다룹니다.
리졸버가 함께 온다고 가정하지 마세요. /etc/resolv.conf는 컨테이너별 파일이며 네트워크 네임스페이스의 일부가 아니므로, 패킷이 터널로 나가는 동안에도 앱은 호스트의 리졸버에 질의할 수 있습니다. 컨테이너 안에서 그것을 읽고, 사설 이름을 터널로 해석해야 할 때 앱에 dns:를 설정하세요.
docker compose exec app ip route
docker compose exec app cat /etc/resolv.conf
docker compose exec app curl -s https://ifconfig.me라우트 목록에 터널 인터페이스가 있고 주소가 공급자의 것이라면 앱은 터널 안에 있습니다. 인터페이스가 없다면 vpn에 터널이 생기기 전에 app이 시작된 것이므로, vpn이 정상이라고 보고하면 app을 다시 만드세요.
호스트 클라이언트를 위해 네임스페이스에 실제 프록시 리스너를 두세요
데스크톱 브라우저는 네임스페이스에 들어갈 수 없으므로 터널을 서비스로 노출해야 합니다. vpn 네임스페이스 안에서 실제 SOCKS 또는 HTTP 프록시를 실행하고 그 포트만 게시하세요:
services:
vpn:
ports:
- "127.0.0.1:1080:1080"
proxy:
image: your-proxy
network_mode: service:vpn
command: ["--socks", "0.0.0.0:1080"]호스트 쪽 주소가 그 매핑의 중요한 절반입니다. 왼쪽의 127.0.0.1:1080는 리스너를 LAN 밖에 두고, 맨 "1080:1080"는 당신의 머신에 닿을 수 있는 모든 기기에 열린 프록시를 게시합니다.
컨테이너 쪽에는 그 나름의 함정이 있습니다. 게시된 포트는 브리지 위 컨테이너 주소로 전달되지 loopback으로 전달되지 않으므로, 네임스페이스 안에서 --socks 127.0.0.1:1080로 시작한 프록시는 형제의 연결을 받아들이고 게시 포트로 오는 모든 것을 거부합니다. 그것을 0.0.0.0에 묶고 격리는 호스트 쪽 loopback 바인딩에 맡기세요. 같은 규칙이 프록시 구성 파일의 수신 주소에도 적용됩니다.
그다음 이름을 어디서 해석할지 정하세요. curl에서 --socks5는 호스트명을 로컬에서 해석하고 --socks5-hostname는 이름을 프록시로 보냅니다. 브라우저는 같은 스위치를 다른 레이블로 노출합니다. 로컬 해석은 페이지가 터널로 로드되더라도 조회를 유출합니다.
돌아오는 라우트가 있을 때만 컨테이너를 게이트웨이로 취급하세요
라우터, VM, 휴대폰은 HTTP 프록시 필드를 쓰지 않을 것이고 일부 애플리케이션은 프록시 변수를 완전히 무시합니다. 그들의 트래픽을 라우팅한다는 것은 3계층을 뜻합니다: 컨테이너가 패킷을 전달하고 터널 인터페이스에서 주소를 변환합니다.
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE두 가지를 놓치기 쉽습니다. 클라이언트에는 기본 게이트웨이를 이 호스트로 향하게 하는 라우트가 필요하고, 응답은 되돌아올 길을 찾아야 합니다. 상위 라우터의 정적 라우트이거나 클라이언트를 향한 인터페이스의 NAT입니다. 둘 중 하나가 없으면 터널은 요청을 밖으로 실어 보내고 응답은 사라집니다.
영향 범위도 가장 넓습니다. 전체 LAN의 게이트웨이 역할을 하는 호스트는 잘못 구성된 모든 클라이언트를 당신의 VPN 계정으로 보내고, 터널이 끊기면 kill switch에 따라 LAN이 어두워지거나 ISP로 되돌아갑니다.
출구 경로를 확인한 뒤 순서대로 되돌리세요
이 순서로 확인하고 첫 실패에서 멈추세요: 네임스페이스 안의 출구 주소, 호스트에서 프록시를 거친 출구 주소, 그다음 사설 호스트의 이름 해석입니다.
docker compose exec vpn curl -s https://ifconfig.me
curl -s -x socks5h://127.0.0.1:1080 https://ifconfig.me
curl -s -x socks5h://127.0.0.1:1080 http://internal.example/health처음 두 주소는 공급자와 일치해야 하고, 세 번째는 이름 오류로 실패하는 대신 프록시를 통해 해석되어야 합니다. 첫 줄이 일치하고 둘째 줄이 실패한다면 리스너나 게시 포트가 잘못된 것이지 터널이 아닙니다.
되돌리기는 설정의 반대입니다. 셸 프로필과 모든 systemd 유닛에서 프록시 환경 변수를 제거한 뒤 스택을 내려 공유 네임스페이스가 소유자와 함께 사라지게 하세요. 프록시 서비스를 지우기 전에 게시 포트를 지우세요. 그렇지 않으면 호스트에 아무도 읽지 않는 loopback 리스너가 남습니다.
계속하기 전에 네 가지를 확인하세요. 관심 있는 클라이언트가 실제로 터널에 닿는지, 포트를 게시하는 것은 네임스페이스 소유자뿐인지, 사설 이름이 의도한 곳에서 해석되는지, loopback이 아닌 호스트 주소에서 아무것도 듣고 있지 않은지입니다. 각각은 프로덕션에서 조용히 실패하며, 각각은 위의 한 줄 점검입니다.