Як спрямувати через OpenVPN лише трафік контейнерів Docker
Залиште хост на звичайному uplink і помістіть у тунель одну підмережу контейнерів: порядок NAT, правила джерела, iroute OpenVPN і перевірки MSS.
Пакет контейнера ніколи не досягає tun0 , і додаткові можливості контейнера цього не змінять. Між гостем і тунелем стоять два пошуки маршруту: один усередині мережевого простору імен гостя, один на хості після того, як кадр потрапить на міст, і другий звертається до іншої таблиці з іншими правилами збігу. Моя еталонна схема — хост Linux із OpenVPN клієнтом, який тримає 10.8.0.10 на tun0 , мостом Docker на 172.18.0.0/16 , і приватною адресою, до якої може дістатися лише бік тунелю. Мета залишається вузькою: ця гостьова підмережа виходить через тунель, а хост зберігає звичайний uplink.
Запускати VPN-клієнт усередині контейнера — інша архітектура , а не обхідний шлях. Тоді контейнер володіє своїм тунелем і обліковими даними, а робота переїжджає в Dockerfile замість того, щоб залишатися в iptables.
Пересилання вибирає маршрут за призначенням, потім NAT переписує джерело
Коли задано net.ipv4.ip_forward=1 , пересланий пакет маршрутизується лише за призначенням, у головній таблиці. Джерело не відіграє в цьому рішенні жодної ролі. Лише після вибору маршруту ядро запускає POSTROUTING , і саме там стоїть власне правило Docker: -s 172.18.0.0/16 ! -o br-appnet -j MASQUERADE . Усе, що залишає міст у бік іншого інтерфейсу, перекладається, і адреса, якою воно стає, залежить від інтерфейсу, куди його надіслав маршрут.
Це правило пояснює більшість симптомів, про які мене питають. Якщо в головній таблиці немає маршруту до приватного призначення, пакет виходить через uplink із фізичною адресою хоста. Провайдер відкидає призначення, або віддалений сайт відкидає джерело 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Якщо ця відповідь називає uplink, тунель не в грі, і жодне налаштування з боку тунелю не допоможе.
Перенесіть гостя на власну адресу тунелю
Найменша робоча зміна — явний перепис джерела на виході. Вставте його, а не додавайте: 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, перш ніж звинувачувати сервер у зависанні.
Пройдіть список і зупиніться на першій несправності. Саме цей шар треба виправляти; усе, що додається нижче, лише приховує межу, що ламається.