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

Классические протоколы, такие как OpenVPN или IPsec, решали эту проблему добавлением статических маршрутов к публичному IP-адресу VPN-сервера через физический шлюз. Однако этот метод ненадежен при переключении между сетями Wi-Fi и сотовой связью, смене динамических адресов или использовании балансировщиков нагрузки Anycast.

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

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

Чтобы понять необходимость маршрутизации по правилам, рассмотрим путь сетевого пакета на рабочей станции Linux с активным интерфейсом WireGuard wg0.

Когда приложение отправляет TCP-сегмент удаленному серверу 203.0.113.50, сетевой стек ядра выбирает выходной интерфейс. Если маршрут по умолчанию указывает на wg0 (ip route add default dev 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-сокет WireGuard передает этот пакет сетевому стеку Linux, ядро повторно обращается к базе данных пересылки (Forwarding Information Base, 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));

Все исходящие UDP-датаграммы этого сокета получают числовую метку 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 гарантирует, что маршрут по умолчанию (/0) туннельной таблицы не перекрывает более специфичные локальные подсети в таблице main.

Мобильные песочницы и десктоп: Android VpnService и сетевые пространства имен

Использование параметра SO_MARK требует административных привилегий CAP_NET_ADMIN. На рабочих станциях и серверах Linux права суперпользователя позволяют напрямую манипулировать сокетами и таблицами RPDB. Однако в мобильных операционных системах строгая изоляция приложений не допускает прямого изменения таблиц ядра.

В Android клиентские приложения не могут вызывать setsockopt(SO_MARK). Для решения этой задачи платформа предоставляет интерфейс android.net.VpnService. При создании транспортного UDP-сокета приложение передает файловый дескриптор системе через вызов VpnService.protect(int socketFd). Системный демон Android привязывает этот сокет напрямую к физической сети, исключая попадание в виртуальный интерфейс tun.

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

В серверных решениях высокой надежности сетевые пространства имен (ip netns) предлагают альтернативный способ предотвращения петель. Перенос физического интерфейса eth0 в отдельный namespace при сохранении wg0 в основном окружении физически исключает рекурсию маршрутизации без необходимости маркировки пакетов.

Архитектура политик RiceGuard и защита от утечек DNS

В реальных условиях клиентские приложения VPN должны стабильно функционировать при переключении между точками Wi-Fi, смене параметров DHCP и работе локальных системных резолверов DNS, таких как systemd-resolved на 127.0.0.53.

RiceGuard применяет многоуровневый механизм маршрутизации по правилам:

  1. Приоритетные цепочки RPDB: Изоляция локальных адресов, подсетей LAN и многоадресного трафика до перенаправления общего потока в виртуальный туннель.
  2. Интеграция с брандмауэром: Синхронизация правил ядра с цепочками nftables. При падении туннеля пакеты без разрешенной метки fwmark не могут покинуть физический интерфейс в незашифрованном виде.
  3. Защита системных запросов DNS: Запросы к локальным службам разрешения имен изолируются и направляются исключительно через шифрованный канал.

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

Скачать VPNRice