Як прив'язати одну програму Linux до VPN за допомогою правил UID
Правило UID бачить лише локальні сокети, а звичайний виняток для loopback вбиває тунель. Спершу спрямуйте обліковий запис, потім фільтруйте й перевіряйте кожен крок.
Прив'язати одну програму до 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 указують в один бік; приберіть будь-що з них, і обліковий запис або протікає, або втрачає зв'язок.