Пакетът на контейнера никога не стига до tun0, а допълнителни права върху контейнера няма да променят това. Между госта и тунела стоят две търсения на маршрут: едно вътре в мрежовото пространство от имена на госта, едно на хоста, след като кадърът кацне на моста, а второто пита друга таблица с други правила за съвпадение. Моята референтна схема е Linux хост с клиент OpenVPN, който държи 10.8.0.10 на tun0, мост на Docker на 172.18.0.0/16 и частен адрес, който само страната на тунела може да достигне. Целта остава тясна: тази подмрежа на госта излиза през тунела, докато хостът запазва обичайния си канал.

Да пуснеш VPN клиента вътре в контейнера е различен дизайн, а не заобиколен път. Тогава контейнерът притежава своя тунел и идентификационни данни, а работата се премества в Dockerfile, вместо да остане в iptables.

Препращането избира маршрут по дестинация, после NAT пренаписва източника

Когато е зададено net.ipv4.ip_forward=1, препратеният пакет се маршрутизира само по дестинация в главната таблица. Източникът не участва в това решение. Едва след избора на маршрут ядрото изпълнява POSTROUTING и точно там стои собственото правило на Docker: -s 172.18.0.0/16 ! -o br-appnet -j MASQUERADE. Всичко, което напусне моста към друг интерфейс, се превежда, а адресът, който получава, зависи от интерфейса, към който маршрутът го е изпратил.

Това правило обяснява повечето симптоми, за които ме питат. Ако главната таблица няма маршрут за частната дестинация, пакетът излиза през канала с физическия адрес на хоста. Доставчикът ви изхвърля дестинацията или отдалеченият сайт изхвърля източник 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

Ако този отговор назовава канала, тунелът не е в картината и никаква настройка от страна на тунела няма да помогне.

Прехвърлете госта на собствения адрес на тунела

Най-малката работеща промяна е изрично пренаписване на източника при изход. Вмъкнете го, вместо да го добавяте накрая: 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, преди да обвините сървъра за застой.

Минете по списъка и спрете при първия отказ. Точно този слой трябва да се поправи; всичко, добавено по-нататък, само скрива границата, която се чупи.

Подобни статии