Правила iptables и nftables для блокировки трафика мимо VPN-туннеля на уровне ОС

Когда VPN-туннель разрывается или ещё не установлен, ядро продолжает маршрутизировать трафик через основной интерфейс — eth0, wlan0 или другой. Приложения не получают уведомления и работают с реальным IP-адресом. Kill switch на уровне файрвола решает проблему радикально: весь исходящий трафик, кроме явно разрешённого, отбрасывается. Ниже — рабочие наборы правил для iptables и nftables с разбором типичных ошибок.

Механика утечки​


Утечка возникает из-за того, как ядро Linux принимает решения о маршрутизации. VPN-клиент поднимает туннельный интерфейс (wg0, tun0, tap0) и добавляет маршрут по умолчанию через него. Если туннель падает, маршрут удаляется, и ядро возвращается к маршруту через физический интерфейс. Между моментом разрыва и восстановлением туннеля — или если клиент не переподключается вовсе — весь трафик идёт напрямую.

Вторая точка утечки — DNS. Даже при работающем туннеле резолвер может отправлять запросы на DNS-сервер провайдера, полученный через DHCP, минуя VPN. Это отдельная проблема, требующая дополнительных правил.

Третья — IPv6. Если в системе активен стек IPv6, а правила написаны только для iptables (IPv4), трафик уйдёт через IPv6-маршрут без ограничений.

Kill switch на iptables​


Логика цепочки OUTPUT строится по принципу «запрещено всё, что не разрешено». Порядок правил критичен: iptables обрабатывает их сверху вниз до первого совпадения.

Bash:
## Очистить цепочку OUTPUT (удалит все существующие правила в этой цепочке)
iptables -F OUTPUT

## Разрешить loopback — без него не работают локальные сервисы
iptables -A OUTPUT -o lo -j ACCEPT

## Разрешить подключение к VPN-серверу (замените на свои значения)
iptables -A OUTPUT -d 203.0.113.50 -p udp --dport 51820 -j ACCEPT

## Разрешить весь трафик через VPN-интерфейс
iptables -A OUTPUT -o wg0 -j ACCEPT

## Разрешить ответы на уже установленные соединения
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

## Отбросить всё остальное
iptables -A OUTPUT -j DROP

Замените 203.0.113.50 на IP вашего VPN-сервера, 51820 — на порт и протокол (для OpenVPN обычно UDP 1194 или TCP 443), wg0 — на имя туннельного интерфейса.

Для OpenVPN с tun0:

Bash:
iptables -A OUTPUT -d 203.0.113.50 -p udp --dport 1194 -j ACCEPT
iptables -A OUTPUT -o tun0 -j ACCEPT

В современных дистрибутивах iptables часто является обёрткой над nftables-бэкендом (iptables-nft). Правила при этом работают идентично. Проверить бэкенд можно командой iptables --version: вывод содержит (nf_tables) или (legacy).

Сохранение правил​


Правила iptables живут в памяти и сбрасываются при перезагрузке:

Bash:
## Debian/Ubuntu (пакет iptables-persistent)
iptables-save > /etc/iptables/rules.v4

## RHEL/CentOS/Fedora
iptables-save > /etc/sysconfig/iptables

Эквивалент в nftables​


nftables — стандартный бэкенд файрвола в современных дистрибутивах. Синтаксис компактнее, а замена всего набора правил происходит атомарно: нет окна, в котором часть правил уже удалена, а новые ещё не загружены.

Код:
table inet vpn_killswitch {
    chain output {
        type filter hook output priority 0; policy drop;

        ## Loopback
        oifname "lo" accept

        ## Подключение к VPN-серверу
        ip daddr 203.0.113.50 udp dport 51820 accept

        ## Трафик через туннель
        oifname "wg0" accept

        ## Установленные соединения
        ct state established,related accept
    }
}

Ключевое отличие от iptables: policy drop задаётся прямо в объявлении цепочки. Если ни одно правило не совпало, пакет отбрасывается без явного -j DROP в конце.

Таблица inet обрабатывает одновременно IPv4 и IPv6, что упрощает защиту от утечек обоих семейств.

Загрузка и сохранение:

Bash:
## Загрузить из файла
nft -f /etc/nftables.conf

## Сохранить текущий набор правил
nft list ruleset > /etc/nftables.conf

Начальное подключение к VPN-серверу​


Kill switch создаёт проблему курицы и яйца: если правила активны до установления туннеля, клиент не может подключиться к серверу. Разрешающее правило для IP сервера решает это, но есть нюанс с доменными именами.

Если VPN-сервер указан доменом, клиенту сначала нужен DNS-запрос, который будет заблокирован. Варианты:

  • Указать в правиле конкретный IP-адрес сервера. Для WireGuard это стандартная практика: конфигурация содержит IP endpoint напрямую.
  • Использовать скрипт, который сначала резолвит имя, подставляет IP в правило и только потом включает блокировку.
  • Временно разрешить DNS до подключения, затем активировать полный kill switch.

Утечка IPv6​


Если в системе активен IPv6, правила iptables его не покрывают — нужен отдельный набор через ip6tables:

Bash:
ip6tables -F OUTPUT
ip6tables -A OUTPUT -o lo -j ACCEPT
ip6tables -A OUTPUT -o wg0 -j ACCEPT
ip6tables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
ip6tables -A OUTPUT -j DROP

Если IPv6 не используется и не нужен через VPN, проще заблокировать его целиком:

Bash:
ip6tables -A OUTPUT -j DROP

В nftables таблица inet покрывает оба семейства, но только если правила не ограничены явно ip daddr (IPv4) или ip6 daddr (IPv6). Правило oifname "wg0" accept в таблице inet работает для обоих.

Подробнее о механизмах утечки IPv6 и способах диагностики маршрутизации — в материале Утечка IPv6 через VPN: почему она возникает и как проверить маршрутизацию.

Утечка DNS​


DNS-запросы могут уходить мимо туннеля, даже если остальной трафик заблокирован. Два подхода:

Блокировка DNS вне туннеля. Запретить исходящие запросы на порт 53 для всех интерфейсов, кроме VPN:

Bash:
## iptables: вставка в начало цепочки OUTPUT
iptables -I OUTPUT -p udp --dport 53 ! -o wg0 -j DROP
iptables -I OUTPUT -p tcp --dport 53 ! -o wg0 -j DROP

В nftables (внутри цепочки output, до правила oifname "wg0" accept):

Код:
udp dport 53 oifname != "wg0" drop
tcp dport 53 oifname != "wg0" drop

Принудительный DNS через туннель. Указать в /etc/resolv.conf или в настройках NetworkManager / systemd-resolved DNS-сервер VPN-провайдера. Это не заменяет блокировку, но гарантирует, что легитимные запросы идут через туннель.

Альтернатива: сетевые пространства имён​


WireGuard привязан к тому сетевому пространству имён, в котором был создан интерфейс. Это позволяет изолировать контейнер или процесс: интерфейс создаётся в основном namespace с доступом в интернет, затем перемещается в namespace контейнера как единственный интерфейс. Контейнер физически не может отправить пакет мимо туннеля — у него нет другого интерфейса.

Этот подход надёжнее правил файрвола, потому что исключает саму возможность утечки на уровне маршрутизации, а не фильтрует пакеты постфактум.

Проверка результата​


Активные правила:

Bash:
iptables -L OUTPUT -n -v --line-numbers
## или
nft list chain inet vpn_killswitch output

Тест утечки. Остановите VPN-клиент и попробуйте открыть любой сайт или выполнить curl. Если kill switch работает, соединение не установится. Поднимите VPN и повторите — трафик должен пойти.

Проверка внешнего IP:

Bash:
curl -4 ifconfig.me

Если возвращаемый адрес совпадает с адресом VPN-сервера, а не провайдера — утечки нет.

Проверка DNS. resolvectl status (systemd-resolved) или cat /etc/resolv.conf покажет, какой DNS-сервер используется. Запрос должен идти через туннельный интерфейс.

Типичные ошибки​


Правило для VPN-сервера слишком широкое. Если разрешить весь трафик на IP сервера без ограничения порта и протокола, при компрометации сервера или подмене маршрута трафик уйдёт мимо туннеля. Указывайте конкретный порт и протокол.

Забыт IPv6. Правила iptables не влияют на ip6tables. Если IPv6 активен, трафик уйдёт через него. В nftables таблица inet покрывает оба семейства, но только при корректных правилах.

Kill switch не переживает перезагрузку. Без сохранения правил в конфигурационный файл они теряются. Убедитесь, что правила загружаются при старте — через iptables-persistent, nftables.service или unit-файл.

Порядок правил в iptables. Если -j DROP стоит раньше разрешающих правил, они никогда не сработают. Проверяйте порядок через --line-numbers.

Конфликт с другими правилами. Docker, libvirt и другие сервисы создают собственные цепочки и таблицы. В nftables используйте отдельную таблицу с собственным приоритетом, чтобы изолировать kill switch от остального файрвола.

Источники​


 
Назад
Верх Низ