Виправте трафік 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 повідомляє, чи файл є заглушкою 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-mount цього файлу, тож правка спершу потребує розмонтування. Задати --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-юніт або таблицю 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-суфікси VPN Windows, тому браузер працює, а 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.confupstream, який його розв'язує? - На хості: чи ввімкнене пересилання, чи прив'язаний NAT до інтерфейсу тунелю і чи приймає ланцюжок пересилання перехід від мосту до тунелю?
- У партнера: чи знає він зворотний шлях або ви покладаєтеся на NAT, щоб приховати підмережу контейнера?
- На платформі: WSL у режимі NAT чи дзеркальному, і контейнер — Linux чи Windows?
Повторіть кожен тест у свіжому контейнері або оболонці. Довготривалий процес із кешованою відповіддю або встановленим з'єднанням і далі падатиме після того, як конфігурація стане правильною, і цей збій виглядає точно як виправлення, що не спрацювало.