VPN-клиент подключается, браузер работает, а единственная команда, которая вам действительно нужна, по-прежнему выходит через домашнее соединение. Или наоборот: туннель забирает маршрут по умолчанию, и локальный файловый сервер исчезает с рабочего стола. Я столкнулся с этим на агенте сборки, которому нужно было попасть на внутреннее зеркало артефактов, пока остальная машина оставалась в офисной локальной сети, а клиент предлагал ровно два варианта: весь трафик или ничего. Запрос в обоих случаях один и тот же — это приложение, а не вся машина, — и у большинства клиентов для него нет галочки, потому что таблица маршрутов никогда не знала, что такое программа.

Почему таблица маршрутов не видит вашу программу

Когда процесс отправляет пакет, стек ищет адрес назначения в таблице маршрутизации. Ни имя исполняемого файла, ни порт в этом поиске не участвуют. Доменные имена терпят неудачу по той же причине: туннель доставляет трафик на адрес, а к тому моменту, когда имя разрешается в адрес, маршрутизация остаётся единственным рычагом ядра. Поэтому «VPN только для этого сайта» в итоге означает прокси, а «VPN только для этой программы» — один из небольшого набора механизмов.

МеханизмЧто выбираетОхватываетПри обрыве туннеля
Привязка сокетаадрес источникаклиент, которым вы управляетесоединения не проходят
Правило UID (Linux)владелец-пользовательвсе процессы одной учётной записисоединения не проходят
Профиль брандмауэра (Windows)профиль адаптераодна программаотказ в закрытую
Сетевое пространство имёнчленство в пространстве имёнвсё, что запущено внутрисоединения не проходят
Переход SOCKSнастройка прокси в приложенииодно приложение или один профиль браузерасоединения не проходят

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

Один адрес назначения — всё ещё маршрут, а не правило для программы. В macOS обычный рецепт — снять флажок «Весь трафик через VPN» и добавить маршрут к узлу: sudo route add -host internal-tuner.example -interface ppp0, удаляемый снова командой sudo route delete -host internal-tuner.example -interface ppp0. Прочитайте имя интерфейса из ifconfig сначала, поскольку ppp0 — лишь пример. Подстановочные знаки в маршруте к узлу не работают, поэтому служба, обращающаяся к нескольким именам, требует отдельной записи для каждого имени, а правило охватывает все процессы на машине, а не тот, который вы имели в виду.

Привяжите сокет к адресу туннеля

Когда клиент под вашим контролем, привязка — самый дешёвый селектор. Сокет, привязанный к локальному адресу туннеля, выходит через этот интерфейс, пока для пункта назначения там есть маршрут, а всё остальное на машине сохраняет обычный путь. Тонкость в том, что привязка выбирает адрес источника, а выход по-прежнему выбирает таблица маршрутов. Привязка сама по себе не заводит в туннель трафик, у которого нет маршрута, — в этом ошибка за большинством отчётов «я привязал, а он всё равно уходит не туда».

import socket
s = socket.socket()
s.bind(("10.8.0.2", 0))
s.connect(("build.internal.example", 443))

Инструменты более высокого уровня предлагают ту же идею: curl --interface 10.8.0.2, CURLOPT_INTERFACE в PHP и аргумент source_address в HTTP-клиентах Python. Адрес должен принадлежать туннелю, и опечатка здесь проявляется как обычная ошибка соединения, а не как жалоба маршрутизации. Привязка к имени устройства вместо адреса — привилегия Linux, и она не переносится на Windows или macOS, поэтому переносимые скрипты передают адрес.

Linux: сопоставляйте владельца, а не имя программы

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

ip rule add uidrange 1001-1001 lookup 100
ip route add default dev tun0 table 100

Это работает, только если туннель ещё не стал маршрутом по умолчанию. Запустите клиент с --route-nopull или отфильтруйте навязанный перенаправляющий маршрут с помощью --pull-filter ignore "redirect-gateway" в OpenVPN 2.4 и новее, а затем добавьте запись в таблицу сами. Иначе существуют оба маршрута по умолчанию, и побеждает тот, у которого приоритет ниже. Осознанная версия этого — направить маршрут по умолчанию на шлюз туннеля или сделать это из скрипта отключения: так трафик не уходит в интернет при обрыве туннеля, но это аварийный выключатель, а не селектор, и он действует, пока кто-нибудь его не уберёт.

Более сильный вариант — сетевое пространство имён. Создайте его, запустите клиент внутри и стартуйте приложение через ip netns exec. Ничто вне пространства имён не видит интерфейс туннеля, поэтому приложение не может случайно вернуться на маршрут хоста. Цена реальна: пространству имён нужна пара veth для выхода в физическую сеть и собственный файл резолвера, потому что пространство имён монтирования не разделяет настройки разрешения имён хоста.

Две причины отказа объясняют большинство утечек. Разрешение имён по-прежнему происходит в резолвере хоста, поэтому запрос уходит мимо туннеля, даже когда TCP-соединение идёт внутрь. А IPv6 нужен собственный ip -6 rule, потому что правило v4 его не покрывает.

Windows и macOS: ограничьте брандмауэр или отдайте прокси

В Windows тоже нет селектора маршрута для отдельного приложения, но правила брандмауэра можно ограничить профилем, а VPN-адаптер можно отнести к профилю «Общий», пока домашняя локальная сеть остаётся «Частной». Создайте правила блокировки исходящих для программы в профилях «Частный» и «Домен», оставьте «Общий» открытым, и приложение сможет выходить в сеть только через туннель. Сначала задайте категорию сети для подключения, затем убедитесь в панели мониторинга брандмауэра, что правило сработало. Это блокировка, а не маршрут: при обрыве туннеля она отказывает в закрытую и не может остановить приложение, игнорирующее брандмауэр.

macOS следует той же логике другими инструментами. SOCKS-слушатель, доступный через SSH, — наименее инвазивный вариант, поскольку ssh -D 1080 user@host создаёт один локальный порт, и этот путь используют только приложения, настроенные на него. Правило pf может блокировать программу по пользователю или группе, но всё равно не может выбрать туннель только для этой программы. Tunnelblick вообще не умеет фильтровать по домену, потому что маршруты работают с адресами; пользователи OpenConnect прибегают к vpn-slice, когда сервер присылает слишком широкий список префиксов.

Два обходных пути покрывают то, чего не умеют инструменты платформы. Расширения браузера, переключающие прокси по домену, — практичный ответ для «только этого сайта», потому что они действуют на уровне приложения, где имена ещё существуют; они охватывают один браузер и ничего больше. Грубый путь — запустить клиент поставщика в виртуальной машине, чтобы гость владел изменением маршрута, а хост сохранял свой путь, ценой двух сетевых стеков в обслуживании.

Переход SOCKS для инструментов без привязки

Большинство приложений можно направить на HTTP- или SOCKS-прокси, даже если они ничего не знают об интерфейсах. SSH-клиент предоставляет такой прокси без изменений на стороне сервера:

ssh -N -D 127.0.0.1:9999 [email protected]

Всё, что настроено на этот порт, выходит через бастион, пока остальной рабочий стол сохраняет обычное соединение. Локальный экземпляр 3proxy, привязанный к адресу туннеля, делает то же самое для одного порта без SSH, а stunnel закрывает один порт, обёрнутый в TLS. Ограничения практические: прокси несёт только протоколы, умеющие им пользоваться; UDP требует UDP-ассоциации SOCKS5, которую настройка «SOCKS5» у многих клиентов не включает; а DNS должен разрешаться прокси (socks5h в curl), иначе имя утекает к локальному резолверу.

Держите слушатель на loopback, если он не нужен другой машине.

Проверяйте путь захватом трафика, а не сообщением о статусе

Окна состояния приложения описывают настройку, а не пакет. Захватите трафик на обоих интерфейсах и сравните:

tcpdump -i any -n port 443
ss -tnp | grep 443

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

Затем выберите границу, соответствующую требованию: прокси достаточно для одного сайта, правила UID достаточно для одной учётной записи, профиля брандмауэра достаточно, когда соединение должно отказывать в закрытую, а пространство имён — единственный вариант, скрывающий туннель от всего, что снаружи.

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