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 достатньо для одного облікового запису, профілю брандмауера достатньо, коли з'єднання має відмовляти в закритому стані, а простір імен — єдиний варіант, який ховає тунель від усього зовні.

Схожі статті