Как направить трафик хоста через контейнер 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Если список маршрутов содержит интерфейс туннеля, а адрес принадлежит провайдеру, приложение внутри туннеля. Если интерфейса нет, app стартовал раньше, чем у vpn появился туннель, поэтому пересоздайте app, как только vpn сообщит о здоровье.
Поместите настоящий слушатель прокси в пространство имён для клиентов хоста
Настольный браузер не может войти в пространство имён, поэтому туннель нужно выставить как службу. Запустите настоящий прокси SOCKS или HTTP внутри пространства имён vpn и опубликуйте только его порт:
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 отправляет имя прокси; браузеры показывают тот же переключатель под другой меткой. Локальное разрешение выдаёт запрос даже тогда, когда страница загружается через туннель.
Считайте контейнер шлюзом только при наличии обратного маршрута
Маршрутизаторы, виртуальные машины и телефоны не будут использовать поле прокси HTTP, а некоторые приложения полностью игнорируют переменные прокси. Маршрутизация их трафика означает уровень 3: контейнер пересылает пакеты и транслирует адреса на интерфейсе туннеля.
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADEДве вещи легко упустить. Клиенту нужен маршрут, направляющий его шлюз по умолчанию на этот хост, а ответы должны найти путь назад — либо через статический маршрут на вышестоящем маршрутизаторе, либо через NAT на интерфейсе, обращённом к клиентам. Без одного из этого туннель уносит запрос наружу, и ответ исчезает.
У него и самый широкий радиус действия. Хост, служащий шлюзом для всей LAN, отправляет каждого неверно настроенного клиента через вашу учётную запись VPN, а падение туннеля либо гасит LAN, либо возвращается к ISP в зависимости от kill switch.
Проверьте путь выхода, затем откатывайте по порядку
Проверяйте в этом порядке и останавливайтесь на первой ошибке: адрес выхода внутри пространства имён, адрес выхода с хоста через прокси, затем разрешение имён для частного хоста.
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. Каждое из этого тихо отказывает в продакшене, и каждое — проверка в одну строку выше.