Поправете трафика на Docker и WSL, който игнорира VPN на хоста
Контейнерите и 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 никога не се появява в пространството му от имена, нищо по-долу няма да помогне: никаква промяна на сървър за имена не създава маршрут и никакъв маршрут не създава идентичност на тунел, която партньорът ще приеме. Редът е важен, защото отказ на DNS и отказ на маршрут изглеждат еднакво отвътре в шел.
Насочете резолвера към тунела, а не към публичен
На хоста, resolvectl status изброява DNS сървъри за всяка връзка, а readlink -f /etc/resolv.conf ви казва дали файлът е stub на systemd, файл на NetworkManager или нещо, което клиент на доставчик е презаписал. Вътре в контейнер в дефинирана от потребителя мрежа, /etc/resolv.conf именува 127.0.0.11, вградения резолвер на Docker, който препраща към какъвто upstream е бил конфигуриран демонът. Контейнер в моста по подразбиране вместо това е получил копие на файла на хоста при създаването си и го пази.
Това копие е причината тунел, който се вдига след стартирането на контейнера, да остави контейнера да пита стария upstream. Или подайте резолвера на 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-монтира този файл, така че редакцията първо се нуждае от unmount. Задаването на --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 ACCEPTПравилата FORWARD имат значение на хостове, чиято политика за препращане е 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 unit или 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}}'.
Поправката е да преместите един от диапазоните: или различен пул адреси по подразбиране за Docker в daemon.json или различен пул на концентратора. Добавянето на още един сървър за имена не прави нищо тук, нито пък ssh -b: обвързването на клиента към интерфейс променя адреса на източника, а не маршрут на дестинация, който сочи в тунела. Бързо разделение на труда: име, което се резолвва на хоста, но не в контейнера, е проблем на резолвера, докато директна IP връзка, която увисва в контейнера, но работи на хоста, е проблем на маршрут или верига за препращане.
WSL и Docker Desktop не следват правилата на Linux
В режима си по подразбиране NAT WSL стои зад собствения си виртуален адаптер и не получава вътрешните маршрути или DNS суфикси на Windows VPN, затова браузърът работи, а ping вътре в дистрибуцията не. Огледалната мрежа, налична в скорошни сборки на Windows 11, вместо това споделя интерфейсите на хоста, но трябва да бъде включена в .wslconfig и дистрибуцията да бъде рестартирана с wsl --shutdown. Потвърдете активния режим с wslinfo --networking-mode преди да приложите поправка, защото отговорът за NAT е грешен за огледален режим и обратното.
Docker Desktop добавя още един слой. Linux контейнерите на Windows стоят зад NAT реализация, исторически наричана vpnkit, и пътят им се различава от този на нативен Linux хост. Windows контейнерите се свързват през виртуален комутатор Hyper-V и корпоративен клиент може да блокира трафика от този комутатор, така че Linux контейнерите на същата машина могат да достигат мрежата, докато Windows контейнерите нямат нищо. Външен комутатор, обвързан с физическия адаптер, е обичайното решение и променя мрежовата идентичност на контейнера по начини, които политиката може да не позволява.
Преди да напишете още едно правило, преминете четирите слоя по ред и спрете там, където един от тях е грешен:
- Вътре в контейнера: дали
ip routeдостига частния префикс и дали/etc/resolv.confименува upstream, който го резолвва? - На хоста: включено ли е препращането, обвързан ли е NAT към интерфейса на тунела и приема ли веригата за препращане мост към тунел?
- На партньора: знае ли обратен път или разчитате NAT да скрие подмрежата на контейнера?
- На платформата: WSL в режим NAT или огледален е, и контейнерът Linux или Windows е?
Стартирайте отново всеки тест в нов контейнер или шел. Дълготраен процес с кеширан отговор или установена връзка ще продължи да се проваля, след като конфигурацията е правилна, и този отказ изглежда точно като поправка, която не е проработила.