Добавление второго клиента OpenVPN на один хост Linux срывается в одном из двух мест. Либо внутренний клиент не может достучаться до своего сервера, потому что внешний туннель захватил этот транспорт, либо второй клиент переписывает таблицу маршрутов и первый туннель перестаёт работать. Обе проблемы лежат в маршрутизации, а не в шифровании, и обе решаются до того, как новый туннель понесёт хоть какой-то прикладной трафик. Адреса здесь иллюстративны: tun0 — внешний туннель, 203.0.113.9 — внутренний сервер, а TCP 9999 — поток, который должен идти внутренним путём.

Два режима отказа, одна таблица маршрутов

Внутренний клиент открывает UDP-сокет к своему серверу, и этот сокет следует текущей таблице маршрутов, как любой другой. Если внешний туннель уже владеет маршрутом по умолчанию, пакеты рукопожатия уходят во внешний туннель. Это работает только когда внешний сервер может достичь внутреннего и противоположная сторона согласна нести поток; иначе рукопожатие застревает и клиент повторяет попытку.

Второй отказ приходит из push сервера. Обычный push — redirect-gateway def1, который устанавливает 0.0.0.0/1 и 128.0.0.0/1 вместо замены маршрута по умолчанию. Поэтому два клиента, каждый из которых ставит свою пару /1, сталкиваются, и самая новая пара выигрывает почти для любого адресата. Ищите эти два префикса в таблице, когда туннель падает после подключения второго, потому что сам маршрут по умолчанию мог вообще не меняться.

Пометьте поток и дайте ему таблицу

Направьте выбранный трафик, пометив его в таблице mangle и сопоставив метку в правиле политики. Правило указывает на выделенную таблицу, маршрут по умолчанию которой — внутреннее устройство, поэтому только помеченные пакеты идут внутренним путём.

iptables -t mangle -A PREROUTING -p tcp -m multiport --dport 9999 -j MARK --set-mark 0x9999
ip rule add fwmark 0x9999 table 200 pref 1000
ip route add default dev tun1 table 200

Большинство отказов в этом наборе правил происходит из двух ошибок. Значение метки, которое задаёт межсетевой экран, и значение, с которым сопоставляет правило, должны совпадать, потому что расхождение не даёт ни ошибки, ни эффекта. А PREROUTING видит только пакеты, приходящие извне, поэтому соединению, начатому на этом хосте, нужно правило OUTPUT с тем же сопоставлением, прежде чем его вообще помечат.

Отберите владение маршрутами у второго клиента

Два варианта не дают второму клиенту править таблицу, и они действуют в разных точках. route-nopull игнорирует присланные опции маршрутов, поэтому ничего не устанавливается. route-noexec принимает присланные опции в окружение клиента, но изменение таблицы оставляет хуку, и это именно то, что нужно, когда требуется увидеть, что предложил сервер.

Хук запускается с присланными значениями в своём окружении, и он должен добавлять только одобренные вами записи. Сделайте его идемпотентным: очищайте таблицу, которой он владеет, перед перестройкой, и не предполагайте, что прошлый запуск оставил чистое состояние. Переподключение снова вызывает хук, а хук, который добавляет без очистки, накапливает дубликаты маршрутов.

Выберите одного владельца и запишите его. Профиль, задающий оба варианта, или хук, конкурирующий с ручным скриптом, порождает таблицу, которую потом никто не объяснит.

Закрепите оба адреса серверов

Внутреннему серверу нужен host-маршрут через внешнее устройство до подключения внутреннего клиента. Именно эта запись удерживает внутренний транспорт внутри внешнего туннеля, вместо того чтобы позволить ему следовать маршруту по умолчанию.

ip route add 203.0.113.9/32 dev tun0
ip route get 203.0.113.9

Добавьте также соответствующий host-маршрут для внешнего сервера в главную таблицу, иначе переподключение может направить транспорт самого внешнего клиента в его собственный туннель. Порядок не факультативен: добавляйте эти записи после того, как появится нужный интерфейс, то есть в хуке или скрипте, запускаемом после подключения. Маршрут, указывающий на выключенное устройство, удаляется или игнорируется, поэтому запись, сделанная при загрузке, не переживает предполагаемую последовательность.

Проверяйте поиск, а не читайте всю таблицу. Вывод поиска маршрута называет устройство и адрес источника, которые будет использовать ядро, и это ровно то, что получает клиент, открывая свой сокет.

Проверьте цепочку и сделайте отказ видимым

Снимайте трафик на внешнем интерфейсе, пока внутренний клиент подключается. Внутреннее рукопожатие должно появиться как зашифрованная полезная нагрузка внутри внешнего туннеля; если оно видно как обычный UDP к 203.0.113.9, host-маршрута нет и цепочка не строилась.

Худший исход — цепочка, которая тихо возвращается на физический uplink: состояние интерфейса ничего не объявляет, и внутренний трафик уходит с неверного адреса источника. Если второй клиент может подключиться напрямую, когда внешний туннель падает, отключите этот путь или привяжите его к внешнему интерфейсу, чтобы отказ был виден.

Закончите двумя потоками и одним дампом правил: помеченное и непомеченное соединения должны идти разными путями, а ip rule show должен перечислять задуманный приоритет. Если оба потока идут одним путём, метка применяется не там, где вы думаете.

Похожие статьи