Policy routing для WireGuard нужен, когда в туннель должны уходить не все пакеты, а только часть: трафик конкретного пользователя, порта, подсети или приложения. Схема собирается из четырёх шагов: своя таблица маршрутизации, правило ip rule по метке fwmark, маркировка пакетов в mangle, отдельная метка для самого WireGuard. Таблица main при этом остаётся нетронутой, а маршрут по умолчанию через wg0 живёт только в вашей таблице.
Рабочий каркас выглядит так:
echo '100 vpn' > /etc/iproute2/rt_tables.d/vpn.conf ip rule add fwmark 0x1 table 100 pref 100 ip route add default dev wg0 table 100 iptables -t mangle -A OUTPUT -m owner --uid-owner 1000 -j MARK --set-mark 0x1
После этих команд трафик процесса с UID 1000 уходит через wg0, остальные процессы работают через провайдера. Метка 0x1 обязана отличаться от метки WireGuard. Если поставить одинаковые значения, зашифрованные UDP-пакеты туннеля тоже попадут в таблицу 100 и уйдут в туннель, то есть в петлю маршрутизации.
Дальше разберём порядок правил, kill switch, DNS без утечек, диагностику асимметрии и способ сохранить конфигурацию после перезагрузки.
Зачем нужен policy routing для WireGuard
Проблемы стандартной маршрутизации с WireGuard
Классическая маршрутизация Linux принимает решение по адресу назначения и таблице main. У таблицы один маршрут по умолчанию. Два default route уживаются только через метрику: побеждает меньшая, и выбор не зависит от того, какой процесс или порт создал пакет.
wg-quick с настройкой по умолчанию (Table = auto) делает так: добавляет default route в таблицу 51820, затем ставит два правила ip rule, not fwmark 51820 table 51820 и table main suppress_prefixlength 0. Второе правило отменяет маршрут по умолчанию из main, но оставляет все более специфичные маршруты. Так получается простейший split tunneling: явно прописанные подсети идут мимо туннеля, всё остальное в туннель.
Схема закрывает сценарий "весь трафик плюс локальные сети". Три задачи она не решает: разделение по процессу (один браузер в VPN, второй напрямую), два туннеля одновременно для разных подсетей и изоляция транзитного трафика на шлюзе. Выбор по источнику или по метке требует отдельных правил; базу по ним удобно держать под рукой, например руководство по ip rule, таблицам и политикам.
Что такое policy routing и как он работает
Ядро Linux хранит базу политик маршрутизации (RPDB). При выборе маршрута пакет проходит список правил ip rule в порядке возрастания priority. Каждое правило содержит селектор и действие lookup в таблицу. Селекторы: from (исходный адрес), to (адрес назначения), iif и oif (интерфейсы), fwmark (метка пакета), tos, uidrange.
Практическая логика проста. Правило с priority 90 и селектором fwmark 0x2 отправляет пакеты WireGuard в main. Правило с priority 100 и селектором fwmark 0x1 отправляет трафик приложений в таблицу 100, где лежит default dev wg0. Пакет без метки до этих правил не доходит по селектору и попадает в системное правило 32766 table main.
Готовые приоритеты в системе: 0 local, 32766 main, 32767 default. Ваши правила ставятся между нулём и 32766. Селектор uidrange работает на ядре 4.10 и новее, маркировка через mangle даёт больше контроля и не зависит от версии ядра, поэтому основной путь в статье идёт через fwmark. Пример привязки к источнику и подсети разобран в материале про source-based маршрутизацию (PBR) на Linux.
Подготовка: создание таблиц маршрутизации и правил ip rule
Создание отдельной routing table
Номера 0, 253, 254 и 255 заняты ядром (unspec, default, main, local). Для своих таблиц берите диапазон 1-252. Имена читаются глазами, номером пользуется ядро, поэтому таблице дают и то, и другое.
mkdir -p /etc/iproute2/rt_tables.d echo '100 vpn' > /etc/iproute2/rt_tables.d/vpn.conf ip route show table vpn
Каталог rt_tables.d поддерживают свежие сборки iproute2 и раскладывает имена по отдельным файлам, так проще удалять конфигурацию целиком. Если каталога нет, допишите строку в /etc/iproute2/rt_tables и проверьте результат командой ip route show table 100.
Добавление правил ip rule с fwmark
ip rule add fwmark 0x2 table main pref 90 ip rule add fwmark 0x1 table 100 pref 100 ip rule show
Синтаксис fwmark 0x1/0xff сравнивает только часть разрядов метки, что удобно, когда младшие биты занимают другие подсистемы. Удаление делается по селектору или по приоритету: ip rule del pref 100.
Порядок приоритетов для типового клиента:
| Priority | Селектор | Таблица |
|---|---|---|
| 90 | fwmark 0x2 (пакеты самого WireGuard) | main |
| 100 | fwmark 0x1 (трафик приложений) | 100 (vpn) |
| 110 | from 10.8.0.2 (адрес внутри туннеля) | 100 (vpn) |
| 120 | to 10.0.0.0/8 (внутренние сети) | 100 (vpn) |
| 32766 | всё остальное | main |
Перед правками снимите текущее состояние: ip rule show. Docker, kube-proxy и libvirt добавляют свои правила в диапазон 0-32765. Когда вы знаете их номера заранее, потом легче понять, чьё правило перехватило пакет раньше вашего.
Маркировка пакетов через fwmark и iptables/nftables
Маркировка по UID/GID
Модуль owner работает в цепочках OUTPUT и POSTROUTING. Для локально созданного трафика этого достаточно.
iptables -t mangle -A OUTPUT -m owner --uid-owner 1000 -j MARK --set-mark 0x1 iptables -t mangle -A OUTPUT -m owner --gid-owner 1001 -j MARK --set-mark 0x1
Аналог на nftables, цепочка с хуком output и приоритетом mangle:
nft add table ip mangle
nft 'add chain ip mangle output { type route hook output priority mangle; }'
nft add rule ip mangle output meta skuid 1000 meta mark set 0x1Маркировка по приложению или порту
iptables -t mangle -A OUTPUT -d 203.0.113.0/24 -j MARK --set-mark 0x1
iptables -t mangle -A OUTPUT -p tcp -m multiport --dports 443,8443 -j MARK --set-mark 0x1
nft add rule ip mangle output tcp dport { 443, 8443 } meta mark set 0x1Маркировка по порту 443 отправляет в туннель почти весь веб-трафик, поэтому на практике чаще выбирают адрес назначения или владельца сокета. Метка ставится до выбора маршрута: для локальных пакетов финальное решение принимается после прохождения mangle OUTPUT, и правило в этой цепочке успевает повлиять на маршрут.
Ответные пакеты должны идти тем же путём, иначе соединение развалится. Для этого метку запоминают в conntrack и восстанавливают при входе:
iptables -t mangle -A OUTPUT -m owner --uid-owner 1000 -j MARK --set-mark 0x1 iptables -t mangle -A OUTPUT -m mark --mark 0x1 -j CONNMARK --save-mark iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark
Правило restore-mark в PREROUTING перезаписывает метки, которые выставили другие подсистемы. На узле Kubernetes это ломает маршрутизацию сервисов, там восстановление метки ограничивают по интерфейсу или по признаку ctstate NEW, а сами метки разводят по маске.
Обратная схема тоже рабочая: часть сервисов оставляют вне туннеля, чтобы не гонять через VPN лишний объём. Для доступа к API нейросетей без VPN есть агрегатор AiTunnel с единым интерфейсом к более чем 200 моделям, управлением ключами и бюджетом, оплатой в рублях. Готовые схемы разделения трафика по приложениям и подсетям разобраны в статье Маршрутизация через VPN: как направить нужный трафик в туннель.
Проверка маркировки: iptables -t mangle -L OUTPUT -v -n или nft list chain ip mangle output. Счётчики у нужного правила растут, если трафик попадает под условие.
Настройка WireGuard с policy routing: пошаговый пример
Конфигурация WireGuard с FwMark
Опция FwMark в интерфейсе WireGuard ставит метку на зашифрованные UDP-пакеты. Она нужна, чтобы эти пакеты не попали в таблицу с default dev wg0. Возьмите значение, не совпадающее с меткой приложений.
[Interface] Address = 10.8.0.2/24 PrivateKey = <ключ клиента> FwMark = 0x2 Table = off PostUp = ip -4 rule add fwmark 0x2 table main pref 90; ip -4 rule add fwmark 0x1 table 100 pref 100 PreDown = ip -4 rule del fwmark 0x2 table main pref 90; ip -4 rule del fwmark 0x1 table 100 pref 100 [Peer] PublicKey = <ключ сервера> Endpoint = 203.0.113.10:51820 AllowedIPs = 0.0.0.0/0, ::/0 PersistentKeepalive = 25
Table = off отключает автоматические маршруты и правила wg-quick: всё, что касается путей, вы задаёте сами. Если оставить Table = auto и указать номер таблицы, wg-quick добавит своё правило not fwmark со значением номера таблицы и правило suppress_prefixlength 0. Проверяйте ip rule show после подъёма интерфейса: чужое правило с меньшим номером перехватит пакет раньше вашего.
Добавление маршрутов в отдельную таблицу
ip route add default dev wg0 table 100 ip route add 10.0.0.0/8 dev wg0 table 100 ip route show table 100 ip -4 route get 8.8.8.8 mark 0x1
wg0 это point-to-point интерфейс, поэтому маршрут указывает на устройство без via. MTU 1420 выставляет wg-quick, менять его вручную стоит только при проблемах с фрагментацией. Хост, который раздаёт туннель другим машинам, дополнительно требует net.ipv4.ip_forward=1 и правил форвардинга в firewall.
Стенд для проверки удобно поднять на виртуальной машине с публичным адресом: Timeweb Cloud даёт серверы, VDS и возможность менять ресурсы на ходу, что помогает прогнать маршрутизацию на реальном интерфейсе, а не в лабораторной заглушке.
Kill switch и защита от утечек трафика
Правила firewall для kill switch
Kill switch в этой схеме означает простое условие: наружу уходит трафик туннеля и служебные пакеты WireGuard, всё остальное отбрасывается на выходе. Набор для nftables:
nft add table inet ks
nft 'add chain inet ks out { type filter hook output priority 0; policy drop; }'
nft add rule inet ks out oifname "lo" accept
nft add rule inet ks out oifname "wg0" accept
nft add rule inet ks out meta mark 0x2 accept
nft add rule inet ks out udp dport 51820 acceptПервое правило оставляет работу localhost, второе пропускает трафик внутри туннеля, третье выпускает зашифрованные пакеты WireGuard по их метке, четвёртое служит запасным путём для handshake. Политика drop сработает и на вашей SSH-сессии, если она идёт не через wg0. Не применяйте набор вслепую на удалённом хосте: держите рядом команду сброса правил или ставьте возврат конфигурации по таймеру.
Вариант для систем на iptables, порядок правил снизу вверх по смыслу, DROP последним:
iptables -A OUTPUT -o lo -j ACCEPT iptables -A OUTPUT -o wg0 -j ACCEPT iptables -A OUTPUT -m mark --mark 0x2 -j ACCEPT iptables -A OUTPUT -p udp --dport 51820 -j ACCEPT iptables -A OUTPUT -j DROP
Строка вида iptables -A OUTPUT ! -o wg0 -m mark ! --mark 0x2 -j DROP из старых инструкций вырезает и DNS, и служебный трафик. Ставьте её после разрешающих правил. Разницу между iptables, nftables и eBPF на шлюзе разбирает руководство по VPN-маршрутизации на Linux.
Настройка DNS без утечек
DNS-запросы уходят по адресу из resolv.conf и легко обходят туннель. Рабочая схема: резолвер доступен только через wg0, а systemd-resolved направляет туда нужные имена.
resolvectl dns wg0 10.8.0.1 resolvectl domain wg0 '~corp.internal' resolvectl status wg0 dig +short internal.corp @10.8.0.1
Запись ~corp.internal задаёт домен маршрутизации: только эти имена уходят на 10.8.0.1. Когда kill switch блокирует весь трафик вне wg0, единственный рабочий резолвер тот, что доступен через туннель, тогда проще указать DNS = 10.8.0.1 прямо в конфиге WireGuard. Утечку ловят так: на физическом интерфейсе запускают tcpdump -i eth0 -n udp port 53 и одновременно запрашивают внешнее имя. Пустой вывод означает, что запросы уходят в туннель.
Диагностика асимметричной маршрутизации и ошибок
Асимметрия появляется, когда запрос уходит в туннель, а ответ ищет путь по main и уходит через физический интерфейс. Вторая частая причина отказа это rp_filter. Ядро проверяет, что обратный путь для входящего пакета ведёт к тому же интерфейсу, откуда пакет пришёл. Значение берётся как максимум из net.ipv4.conf.all.rp_filter и net.ipv4.conf.<интерфейс>.rp_filter. При 1 (strict) пакеты из wg0, для которых main указывает другой интерфейс, молча отбрасываются.
sysctl net.ipv4.conf.all.rp_filter sysctl net.ipv4.conf.wg0.rp_filter sysctl -w net.ipv4.conf.all.rp_filter=2 sysctl -w net.ipv4.conf.wg0.rp_filter=2
Режим 2 (loose) ослабляет проверку, режим 0 отключает её. Такие настройки уместны на управляемом шлюзе, где маршрутизация описана правилами. Полный разбор привязки таблиц к интерфейсам и диагностики асимметрии есть в статье про профиль маршрутизации для нескольких интерфейсов в Linux.
Команды для диагностики маршрутизации
| Команда | Что показывает | На что смотреть |
|---|---|---|
| ip rule show | Список правил RPDB с приоритетами | Чужое правило с меньшим номером перехватывает пакет раньше вашего |
| ip route show table 100 | Маршруты вашей таблицы | Наличие default dev wg0 и отсутствие лишних via |
| ip route get 8.8.8.8 mark 0x1 | Фактический путь для помеченного пакета | Ответ должен содержать dev wg0, а не физический интерфейс |
| iptables -t mangle -L OUTPUT -v -n | Счётчики правил маркировки | Нулевые счётчики означают, что трафик не попал под условие |
| tcpdump -i eth0 -n udp port 51820 | Зашифрованный трафик WireGuard на физическом интерфейсе | Пакеты идут только к endpoint, кольца трафика внутрь туннеля нет |
| conntrack -L -m 1 | Соединения с меткой 0x1 | Метка должна совпадать у запроса и у ответа |
| wg show wg0 | Handshake, счётчики rx и tx, endpoint peer | Свежий handshake и растущие счётчики после маркировки |
| sysctl net.ipv4.conf.wg0.rp_filter | Режим проверки обратного пути | Значение 1 ломает асимметричную схему, нужен 2 или 0 |
Типовые ошибки и их решения
| Симптом | Причина | Решение |
|---|---|---|
| Пакеты уходят в туннель, ответы не возвращаются | Метка ставится только в OUTPUT, ответный пакет идёт по main | Сохранить метку через CONNMARK в OUTPUT и восстанавливать её в PREROUTING |
| После ip link set wg0 up связь пропадает полностью | Зашифрованный трафик WireGuard попал в таблицу 100 и ушёл в туннель | Развести метки: FwMark 0x2 для WireGuard, 0x1 для приложений, правило 0x2 в main выше остальных |
| Пропал доступ к локальной подсети | wg-quick добавил default route и собственные drop-правила | Table = off и ручные маршруты только для нужных сетей |
| Счётчики правила маркировки нулевые | Трафик создаёт другой пользователь или цепочка обходится сторонним правилом | Уточнить UID через ss -tunap, проверить порядок цепочек в mangle |
| Контейнеры потеряли интернет | Пакеты docker-сетей попали в таблицу 100 без маршрута к мосту | Добавить правило from 172.17.0.0/16 table main с приоритетом ниже вашего или правило с suppress_prefixlength |
| Handshake проходит, данные не идут | rp_filter strict отбрасывает ответы, пришедшие из wg0 | Выставить rp_filter 2 для wg0 и для all, проверить ip route get для ответного адреса |
Проверка результата и автоматизация
Чек-лист проверки policy routing
- ip rule show: правила 90 и 100 на месте, их номера меньше 32766 и не перекрыты правилами Docker или kube-proxy.
- ip route show table 100: есть default dev wg0, лишних маршрутов через физический шлюз нет.
- ip route get 8.8.8.8 mark 0x1 возвращает dev wg0, а ip route get 8.8.8.8 без метки возвращает физический интерфейс.
- Счётчики в mangle растут, tcpdump на физическом интерфейсе показывает только трафик к endpoint WireGuard.
- Внешний адрес, полученный запросом к сервису определения IP, совпадает с адресом VPN-сервера.
- DNS отвечает из туннеля, tcpdump на физическом интерфейсе по порту 53 пуст.
- wg show wg0 показывает свежий handshake и растущие счётчики rx и tx.
Автоматизация при старте системы
Самый простой способ это PostUp и PreDown в wg0.conf: правила живут ровно столько, сколько интерфейс. Если интерфейс поднимает systemd-networkd или скрипт развёртывания, правила выносят в отдельный unit.
[Unit] Description=Policy routing rules for wg0 After=network-online.target wg-quick@wg0.service Requires=wg-quick@wg0.service [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/local/sbin/wg-policy-up.sh ExecStop=/usr/local/sbin/wg-policy-down.sh [Install] WantedBy=multi-user.target
Скрипт wg-policy-up.sh содержит те же команды ip rule и ip route, что и выше, плюс защиту от дублей: перед добавлением выполните ip rule del pref 100 2>/dev/null; ip rule del pref 90 2>/dev/null; и только потом add. Без этой проверки после каждого рестарта сервиса в списке копятся одинаковые правила, и разобраться в ip rule show становится тяжело.
Проверку начинайте с двух команд: ip rule show и ip route get нужного адреса с меткой. Если маршрут возвращает dev wg0, остаётся настроить kill switch, DNS и автоматизацию запуска.