Policy routing для WireGuard: несколько таблиц маршрутизации без утечек | AdminWiki

Policy routing для WireGuard: несколько таблиц маршрутизации без утечек

11 сентября 2026 12 мин. чтения

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СелекторТаблица
90fwmark 0x2 (пакеты самого WireGuard)main
100fwmark 0x1 (трафик приложений)100 (vpn)
110from 10.8.0.2 (адрес внутри туннеля)100 (vpn)
120to 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 wg0Handshake, счётчики 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

  1. ip rule show: правила 90 и 100 на месте, их номера меньше 32766 и не перекрыты правилами Docker или kube-proxy.
  2. ip route show table 100: есть default dev wg0, лишних маршрутов через физический шлюз нет.
  3. ip route get 8.8.8.8 mark 0x1 возвращает dev wg0, а ip route get 8.8.8.8 без метки возвращает физический интерфейс.
  4. Счётчики в mangle растут, tcpdump на физическом интерфейсе показывает только трафик к endpoint WireGuard.
  5. Внешний адрес, полученный запросом к сервису определения IP, совпадает с адресом VPN-сервера.
  6. DNS отвечает из туннеля, tcpdump на физическом интерфейсе по порту 53 пуст.
  7. 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 и автоматизацию запуска.

Поделиться:
Сохранить гайд? В закладки браузера