Привязать одну программу к VPN в Linux — это прежде всего задача маршрутизации, а правило брандмауэра решает только то, что произойдёт после выбора маршрута. Если сопоставить UID программы в OUTPUT раньше, чем туннель получит маршрут по умолчанию, правило отбросит именно тот трафик, который вы хотели защитить. То же правило не видит и процесс резолвера, который отвечает на DNS-запросы программы. Настройте порядок правильно, и одна учётная запись без привилегий станет надёжным селектором разделённого туннеля.

Дайте учётной записи маршрут до приговора

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

Сначала проведите учётную запись через её собственную таблицу, а затем позвольте брандмауэру подтвердить результат. Это работает на хосте, чей iproute2 понимает uidrange, о чём ip rule help сообщает в одной строке. Таблица 100 держит маршрут по умолчанию через туннель, и правило отправляет только UID 1000 в эту таблицу. Всё остальное остаётся в главной таблице и продолжает использовать физический канал.

ip rule add uidrange 1000-1000 table 100
ip route add default dev tun0 table 100

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

Почему исключение для loopback убивает туннель

Большинство скопированных версий правила отбрасывают каждый пакет, который не выходит через loopback. Туннель — не loopback, поэтому такая форма отбрасывает и трафик туннеля, и результат выглядит как мёртвый VPN, а не как ошибка брандмауэра.

К моменту запуска OUTPUT ядро уже выбрало исходящий интерфейс, поэтому -o сравнивается с ответом маршрута, а не с заголовком пакета. Сопоставление владельца проверяется только в OUTPUT и POSTROUTING, а пересылаемые пакеты не несут сокета, который оно могло бы прочитать. Такое сочетание означает, что исключение должно назвать интерфейс туннеля и отменить это сопоставление, а не отменять loopback.

Loopback по-прежнему заслуживает собственного ACCEPT над DROP, потому что первый DNS-пакет от программы идёт к локальному stub через lo. Поставьте два правила в этом порядке, и учётная запись доходит до stub, затем до туннеля, и никуда больше.

iptables -I OUTPUT 1 -o lo -m owner --uid-owner vpnapp -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner vpnapp ! -o tun0 -j DROP

IPv6 — отдельный набор правил, и пара только для v4 оставляет ip6tables нетронутым. Программа, которая предпочитает записи AAAA, тогда вышла бы через физический канал, так и не достигнув DROP. Отразите оба правила в ip6tables, или отключите IPv6 на хосте, когда туннель не несёт адреса v6. Правила, добавленные вручную, тоже исчезают при перезагрузке, поэтому сохраните их с помощью netfilter-persistent save или пересоберите ту же логику как таблицу nftables, которую загружает загрузочный модуль.

DNS-ответы приходят из другого процесса

Запрос программы к 127.0.0.53 проходит ACCEPT для loopback, и именно там сопоставление UID прекращается. Stub-резолвер пересылает запрос выше из своей служебной учётной записи, поэтому правило, привязанное к vpnapp никогда не видит второй запрос. Если резолвер выбирает LAN-сервер для канала, ответы возвращаются через физический NIC, пока собственные пакеты программы остаются в туннеле. Видимый результат — работающий туннель со сломанными именами или имена, на которые отвечает сервер, которого вы не выбирали.

Исправьте разрешение имён на канале туннеля, а не в приложении. Дайте профилю туннеля DNS-сервер и домен маршрутизации, затем подтвердите с помощью resolvectl query и resolvectl status tun0 что резолвер использует его для важных имён. Внутри сетевого пространства имён проблема исчезает, потому что резолвер пространства знает только туннель.

Проверьте правило из самой учётной записи

Спросите ядро, какой маршрут получает UID, затем выполните запрос от имени этой учётной записи и сравните адрес выхода с адресом своей собственной. Если оба сообщают один и тот же адрес, правило сопоставляет что-то иное, а не проверенный трафик. Первая команда ниже показывает поиск маршрута без отправки пакета, что отделяет ошибку маршрутизации от ошибки брандмауэра.

ip route get 1.1.1.1 uid 1000
sudo -u vpnapp curl -s https://ifconfig.co

Счётчики показывают, срабатывало ли правило когда-либо. Запустите iptables -vnL OUTPUT, прочитайте число пакетов рядом с каждым правилом и считайте ноль рядом с DROP после реального трафика ошибкой сопоставления.

Пересылаемый трафик остаётся вне этого проекта. У контейнера или VM за мостом нет локального сокета, принадлежащего vpnapp, поэтому сопоставление владельца никогда не срабатывает, и такие пакеты следуют политике FORWARD. Если эту нагрузку нужно покрыть, проверьте её отдельно и напишите политику, привязанную к мосту или адресу контейнера. Проект держится, когда таблица, исключение интерфейса, резолвер и правила v6 указывают в одну сторону; уберите любое из них, и учётная запись либо утекает, либо теряет связь.

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