Під час налаштування віртуальної приватної мережі для перенаправлення всього трафіку пристрою (0.0.0.0/0 та ::/0) кожен вихідний пакет повинен прямувати через зашифрований тунель. Така архітектура породжує фундаментальну проблему, відому як рекурсивна маршрутизація (routing recursion). Зашифроване корисне навантаження повинно надсилатися у фізичну мережу через зовнішній UDP-сокет. Якщо операційна система спрямовує всі IP-пакети у віртуальний тунельний інтерфейс, зовнішній UDP-пакет знову потрапляє до драйвера тунелю, створюючи нескінченний цикл інкапсуляції.

Традиційні протоколи, такі як OpenVPN або класичний IPsec, намагалися вирішити цей конфлікт додаванням статичних маршрутів до публічної IP-адреси сервера через фізичний шлюз. Проте такий підхід стає ненадійним під час перемикання між мережами Wi-Fi та стільниковим зв'язком, динамічної зміни адрес або використання балансувальників Anycast.

WireGuard усуває ризик виникнення петель у Linux завдяки поєднанню міток брандмауера (fwmark), параметра сокета SO_MARK та бази правил маршрутизації (Routing Policy Database, RPDB).

Дилема рекурсивної маршрутизації в зашифрованих тунелях

Щоб зрозуміти необхідність маршрутизації за політиками, розглянемо шлях мережевого пакета на комп'ютері з операційною системою Linux та активним інтерфейсом WireGuard wg0.

Коли застосунок надсилає TCP-сегмент віддаленому серверу за адресою 203.0.113.50, мережевий стек ядра обирає вихідний інтерфейс. Якщо застосовано звичайне налаштування на зразок ip route add default dev wg0, ядро обирає wg0 як наступний вузол. Драйвер WireGuard отримує пакет, звіряє його з таблицею Cryptokey Routing, шифрує дані за допомогою ChaCha20-Poly1305 і пакує їх у зовнішній UDP-пакет, адресований серверу 198.51.100.10:51820.

Пакет програми (Призначення: 203.0.113.50)
        │
        ▼
   Таблиця FIB ядра: default dev wg0
        │
        ▼
Драйвер WireGuard (wg0): Інкапсуляція в UDP (Призначення: 198.51.100.10:51820)
        │
        ▼
   Таблиця FIB ядра: Знову збіг із default dev wg0! ──> ПЕТЛЯ МАРШРУТИЗАЦІЇ

Щойно UDP-сокет передає цей пакет мережевому стеку, ядро вдруге звертається до бази даних пересилання (FIB). Оскільки маршрут за замовчуванням вказує на wg0, зовнішній UDP-пакет повертається назад у wg0. Драйвер намагається повторно зашифрувати вже зашифрований пакет, черга сокета переповнюється, розмір перевищує ліміт MTU, і ядро відкидає дані. Зв'язок миттєво обривається.

База правил RPDB та архітектура двох таблиць

Ядро Linux підтримує гнучку маршрутизацію через базу правил маршрутизації (RPDB), керовану набором команд ip rule. Замість єдиної статичної таблиці RPDB дозволяє спрямовувати трафік у різні таблиці залежно від адреси джерела, інтерфейсу, типу трафіку чи додаткових метаданих.

Стандартна конфігурація Linux містить три системні таблиці:

  1. local (таблиця 255): локальні адреси та широкомовний трафік.
  2. main (таблиця 254): стандартні маршрути фізичних інтерфейсів та шлюзів DHCP.
  3. default (таблиця 253): резервна таблиця для додаткових політик.

Інструменти керування WireGuard (наприклад, wg-quick) не змінюють шлюз за замовчуванням у таблиці main. Натомість створюється окрема таблиця маршрутизації — зазвичай з номером 51820 — куди додається маршрут за замовчуванням через wg0:

# Додавання маршруту за замовчуванням до окремої таблиці
ip route add default dev wg0 table 51820
ip -6 route add default dev wg0 table 51820

Відокремлення маршруту тунелю від фізичного шлюзу дозволяє операційній системі підтримувати два незалежних простори маршрутизації. Головне завдання полягає у спрямуванні трафіку застосунків до таблиці 51820 та наданні сокетам WireGuard доступу до таблиці main.

Маркування сокетів через SO_MARK та перевірка fwmark

Ключовим елементом зв'язку між сокетом тунелю та фізичним інтерфейсом є мітка брандмауера (fwmark). У ядрі Linux структура буфера пакета (struct sk_buff) містить 32-бітне поле skb->mark. Ця мітка існує виключно в пам'яті ядра, не передається лініями зв'язку і не змінює IP-заголовки.

Під час створення зовнішнього UDP-сокета драйвер WireGuard активує параметр SO_MARK через системний виклик POSIX:

int fwmark = 51820;
setsockopt(sock_fd, SOL_SOCKET, SO_MARK, &fwmark, sizeof(fwmark));

Усі вихідні пакети цього сокета отримують числову мітку 51820. Для керування рухом пакетів додаються правила RPDB:

# Правила для маркованого та немаркованого трафіку
ip rule add not fwmark 51820 table 51820
ip rule add table main suppress_prefixlength 0

Процес оцінки пакетів відбувається послідовно:

  1. Трафік застосунків: Браузер створює пакет до 203.0.113.50. Сокет браузера не має мітки, тому skb->mark дорівнює 0. Ядро оцінює правило not fwmark 51820. Оскільки 0 != 51820, правило спрацьовує. Пакет скеровується до таблиці 51820 і надходить в інтерфейс wg0.
  2. Транспортний трафік WireGuard: Після шифрування WireGuard надсилає зовнішній UDP-пакет через маркований сокет (skb->mark = 51820). Ядро перевіряє правило not fwmark 51820. Оскільки мітка збігається з 51820, інверсне правило не спрацьовує. Ядро переходить до наступного правила в RPDB — таблиці main (254). У таблиці main фізичний шлюз через eth0 або wlan0 відправляє пакет безпосередньо до локального маршрутизатора.

Цей механізм гарантує вихід зашифрованих пакетів через фізичний інтерфейс без потрапляння назад у wg0.

Фільтрація зворотного шляху та параметр rp_filter

Навіть за правильної вихідної маршрутизації робота тунелю може порушитися, якщо засоби безпеки вхідного трафіку блокуватимуть законні відповіді. Головною причиною є фільтрація зворотного шляху (Reverse Path Filtering, RPF), регламентована стандартом RFC 3704 і керована змінною rp_filter.

RPF захищає вузол від підміни IP-адрес. Отримуючи пакет, ядро перевіряє, чи використовувався б той самий інтерфейс для відправлення відповіді на адресу джерела.

Linux підтримує три режими RPF:

  • 0 (Вимкнено): перевірка адреси відправника не виконується.
  • 1 (Суворий режим): вхідний пакет повинен надійти виключно через той інтерфейс, який обрано таблицею маршрутизації для зворотної відповіді.
  • 2 (М'який режим): адреса відправника повинна мати валідний маршрут у будь-якій таблиці, незалежно від інтерфейсу.

У суворому режимі (rp_filter = 1) несиметричність маршрутів спричиняє відкидання пакетів. Коли відповідь надходить з інтернету на інтерфейс eth0, ядро перевіряє зворотний шлях. Оскільки запит не містить мітки fwmark, RPDB спрямовує його до таблиці 51820, де зазначено, що зовнішні адреси належать wg0. Несумісність інтерфейсів змушує ядро вважати пакет підробленим і відкидати його.

Щоб уникнути втрати даних, у конфігураціях WireGuard вмикається м'який режим RPF:

sysctl -w net.ipv4.conf.all.rp_filter=2
sysctl -w net.ipv4.conf.eth0.rp_filter=2
sysctl -w net.ipv4.conf.wg0.rp_filter=2

Крім того, параметр suppress_prefixlength 0 у правилах RPDB гарантує, що локальні підмережі таблиці main мають перевагу перед загальним тунельним маршрутом /0.

Мобільні ізольовані середовища та робочі станції: Android VpnService і простори імен

Застосування SO_MARK вимагає прав адміністратора CAP_NET_ADMIN. На робочих станціях і серверах привілеї користувача дозволяють керувати таблицями безпосередньо. Однак у мобільних операційних системах сувора ізоляція середовищ забороняє застосункам доступ до таблиць ядра.

В Android клієнти не можуть викликати setsockopt(SO_MARK). Для подолання цього обмеження створено інтерфейс android.net.VpnService. Під час створення сокета програма передає дескриптор системі через VpnService.protect(int socketFd). Системна служба Android прив'язує сокет безпосередньо до фізичної мережі, повністю оминаючи віртуальний пристрій tun.

Linux Desktop:       Сокет -> SO_MARK (51820) -> Правило RPDB (not fwmark) -> Таблиця main
Android Mobile:      Сокет -> VpnService.protect() -> Пряма прив'язка до фізичної мережі
Простори імен:       Інтерфейс eth0 у netns0 <-> wg0 у init_net (повне фізичне розділення)

На серверах високої надійності мережеві простори імен (ip netns) забезпечують розділення трафіку без використання міток. Перенесення інтерфейсу eth0 в ізольований простір при збереженні wg0 в основному унеможливлює виникнення петель на рівні архітектури.

Архітектура політик RiceGuard та захист від витоків DNS

У реальних умовах клієнтські застосунки повинні швидко адаптуватися до зміни мереж Wi-Fi, оновлення параметрів DHCP та взаємодії з локальними службами резолвінгу, такими як systemd-resolved на 127.0.0.53.

RiceGuard використовує розширену модель маршрутизації за правилами:

  1. Пріоритетні ланцюжки RPDB: Пакетна ізоляція локальних адрес, внутрішніх підмереж та багатоадресного трафіку перед передачею даних у тунель.
  2. Автоматичний захист брандмауера: Синхронізація правил із таблицями nftables. У разі втрати зв'язку пакети без дозволеної мітки не можуть вийти через фізичний інтерфейс у нешифрованому вигляді.
  3. Ізоляція системних запитів DNS: Запити до внутрішніх служб резолвінгу направляються виключно в зашифрований канал, усуваючи ризик витоку IP-адрес.

Злагоджена робота міток, таблиць правил та механізмів фільтрації гарантує стійку передачу даних, високу швидкість та відсутність рекурсивних петель.

Завантажити VPNRice