MAATRIX / Блог / Split tunneling на Linux и macOS: настройка через iptables/pf

Split tunneling на Linux и macOS: настройка через iptables/pf

MAATRIX

На Windows и на роутерах split tunneling — это пара галочек в клиенте или список подсетей в веб-морде. На Linux и macOS готового переключателя чаще всего нет: нужно руками объяснить системе, какой трафик идёт через туннель, а какой — мимо него, напрямую в провайдера. Разберём оба случая: policy routing через ip rule/iptables на Linux и фильтрацию с route-to в pf на macOS — с рабочими конфигами и типичными граблями.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Зачем вообще делить трафик

Полный туннель (default route через VPN) удобен, но не всегда нужен. Частые причины настроить split tunneling:

  • нужен доступ к серверу или облаку через VPN, а остальной трафик (стриминг, локальная сеть, обновления ОС) не должен упираться в пропускную способность туннеля;
  • на клиенте одновременно работают инструменты, которым важен именно локальный IP (например, банк-клиент с привязкой к адресу), и сервисы, которым нужен IP сервера;
  • сервер стоит далеко, и гнать через него весь трафик — это лишняя задержка там, где VPN не нужен;
  • на Linux-сервере или macOS-машине несколько сетевых задач: часть процессов должна ходить через один туннель, часть — через другой или напрямую.

Идея одна и та же на обеих системах: пакет должен быть промаркирован или классифицирован до того, как ядро выберет для него маршрут. Механизмы разные, потому что у Linux и macOS разное сетевое ядро.

Linux: policy routing — ip rule, fwmark и таблицы маршрутов

В Linux обычная таблица маршрутов одна (main), но ядро поддерживает несколько таблиц и правила выбора между ними — это и есть policy routing. Три кита:

  • fwmark — метка на пакете, которую ставит iptables/nftables (правило в цепочке mangle);
  • ip rule — правило вида «если у пакета такая-то метка/источник — искать маршрут в такой-то таблице», а не в main;
  • отдельная таблица маршрутов — в ней прописан маршрут по умолчанию через VPN-интерфейс, а main при этом остаётся с обычным шлюзом провайдера.

Смотрим текущие правила и таблицы:

ip rule list
ip route show table main
cat /etc/iproute2/rt_tables

Заводим свою таблицу (номер и имя — на ваш выбор, главное не занять существующий):

echo "200 vpnonly" | sudo tee -a /etc/iproute2/rt_tables

Дальше — два рабочих сценария: маршрутизация по адресату (проще, без меток) и маршрутизация по владельцу процесса/подсети (нужны метки).

Арендуйте сервер под свои задачи!

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Практика: WireGuard + fwmark на Linux

Если туннель поднят через wg-quick, по умолчанию он забирает себе весь default route (AllowedIPs = 0.0.0.0/0). Для split tunneling это отключают и добавляют маршруты вручную через хуки PostUp/PreDown.

Вариант А — split по подсетям назначения (самый простой, без fwmark). В AllowedIPs указываем не 0.0.0.0/0, а конкретные подсети/хосты, которые должны идти через туннель:

[Interface]
PrivateKey = <ключ_клиента>
Address = 10.66.0.2/32
DNS = 10.66.0.1

[Peer]
PublicKey = <ключ_сервера>
Endpoint = <IP_сервера>:51820
AllowedIPs = 10.20.0.0/24, 192.0.2.10/32
PersistentKeepalive = 25

WireGuard сам добавит маршруты только для этих подсетей — остальной трафик пойдёт через обычный шлюз. Это покрывает 80% случаев («мне нужен только доступ к рабочей сети/серверу через VPN»).

Вариант Б — split по отправителю (конкретный пользователь, процесс, локальная подсеть) через fwmark. Тут AllowedIPs = 0.0.0.0/0 оставляем, но отключаем автоматическую подмену default route и строим её вручную:

[Interface]
PrivateKey = <ключ_клиента>
Address = 10.66.0.2/32
Table = off
PostUp = ip rule add fwmark 51820 table vpnonly
PostUp = ip route add default dev %i table vpnonly
PostUp = iptables -t mangle -A OUTPUT -m owner --uid-owner vpnuser -j MARK --set-mark 51820
PreDown = ip rule del fwmark 51820 table vpnonly
PreDown = iptables -t mangle -D OUTPUT -m owner --uid-owner vpnuser -j MARK --set-mark 51820

[Peer]
PublicKey = <ключ_сервера>
Endpoint = <IP_сервера>:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

Table = off запрещает wg-quick трогать таблицу main. Правило iptables -m owner --uid-owner vpnuser маркирует пакеты конкретного пользователя (создайте его отдельно: sudo useradd -M -s /usr/sbin/nologin vpnuser), а ip rule отправляет промаркированные пакеты в таблицу vpnonly, где единственный маршрут — через wg0. Запускать нужный процесс от этого пользователя:

sudo -u vpnuser <команда>

Модуль owner умеет матчить и по --gid-owner, что удобнее для группы процессов, чем заводить пользователя на каждый сервис.

Практика на Linux: split без WireGuard и по cgroup

Для OpenVPN идея та же: клиент по умолчанию тоже пытается забрать default route (директивы redirect-gateway). Если её убрать из конфига (или клиент запущен с --route-nopull), маршруты через туннель нужно добавлять руками:

sudo ip route add 10.20.0.0/24 dev tun0

Это split по адресату — без меток, без правил, работает сразу.

Если нужно развести трафик не по пользователю, а по конкретному процессу/сервису (например, только у одного systemd-юнита), удобнее cgroup v2 вместо отдельного uid:

iptables -t mangle -A OUTPUT -m cgroup --path system.slice/myservice.service -j MARK --set-mark 51820

Дальше — тот же ip rule add fwmark 51820 table vpnonly, что и в примере с WireGuard. Практический плюс: не нужно возиться с правами и отдельным пользователем ради одного демона.

Грабля с персистентностью. ip rule/ip route add без хуков PostUp/PreDown не переживают перезагрузку и не переживают systemctl restart. Если туннель поднимается не через wg-quick, а через systemd-networkd или NetworkManager, правила policy routing лучше вынести в dispatcher-скрипт (/etc/NetworkManager/dispatcher.d/) или в отдельный systemd-юнит с ExecStart/ExecStop, а не полагаться на то, что кто-то вспомнит вбить их руками после ребута.

macOS: почему тут не iproute2, а pf

В macOS нет ip rule и нескольких пользовательских таблиц маршрутов в том виде, как в Linux — сетевой стек унаследован от BSD, и инструмент policy routing там один: pf (Packet Filter) с ключевым словом route-to. Разница принципиальная: вместо «промаркировать пакет и отправить его в свою таблицу» в pf вы прямо в правиле фильтра указываете, через какой интерфейс и шлюз пакет должен уйти, если он подошёл под условие — по адресу назначения, по порту, по пользователю (uid) или группе.

Это менее гибко, чем связка iptables + ip rule (нет отдельных таблиц, которые можно переиспользовать в нескольких правилах), но для типовой задачи split tunneling хватает с запасом.

Перед началом — включён ли pf вообще:

sudo pfctl -s info | head -3

Если Status: Disabled — включаем и одновременно правим правила: pf в macOS уже используется системой (например, для NAT в режиме раздачи интернета), поэтому свой конфиг подключают через anchor, а не подменяют весь /etc/pf.conf.

Практика на macOS: pf.conf для split tunneling через WireGuard

WireGuard на macOS (через wireguard-tools или официальное приложение) поднимает интерфейс utunN — номер плавает, лучше смотреть его после подключения:

ifconfig | grep -A1 utun
wg show

Дальше два рабочих подхода.

Split по адресату — как и на Linux, самый простой: в конфиге WireGuard AllowedIPs указываете не 0.0.0.0/0, а нужные подсети. wireguard-go/wg-quick на macOS так же сам добавит маршруты только для них — отдельный pf.conf тут даже не обязателен.

Split по пользователю/группе процессов — когда нужно, чтобы через VPN шёл конкретный инструмент, а не подсеть. Заводим отдельного пользователя для процессов, которым нужен туннель:

sudo dscl . -create /Users/_vpnuser
sudo dscl . -create /Users/_vpnuser UniqueID 599
sudo dscl . -create /Users/_vpnuser UserShell /usr/bin/false

В /etc/pf.anchors/split-tunnel (свой anchor, отдельно от системного конфига):

wg_if = "utun9"
wg_gw = "10.66.0.1"

pass out on $wg_if all
pass out route-to ($wg_if $wg_gw) from any to any user _vpnuser

В /etc/pf.conf подключаем anchor, не трогая системные правила Apple:

anchor "split-tunnel"
load anchor "split-tunnel" from "/etc/pf.anchors/split-tunnel"

Применяем и включаем:

sudo pfctl -f /etc/pf.conf
sudo pfctl -e

Проверяем, что anchor загрузился:

sudo pfctl -a split-tunnel -s rules

Запускать нужное приложение через VPN — от имени этого пользователя:

sudo -u _vpnuser open -a "Имя.app"

Для CLI-инструментов проще: sudo -u _vpnuser <команда>. GUI-приложения через sudo -u иногда не подхватывают графическую сессию корректно — в таких случаях чаще делают наоборот: весь трафик обычного пользователя пускают через route-to, а системные процессы/конкретные приложения (Apple-сервисы, локальную сеть) явно исключают отдельным правилом pass out on en0 from any to <локальная_подсеть> выше по списку — pf применяет первое совпавшее правило по порядку сверху вниз с учётом quick.

Диагностика, утечки DNS и типичные грабли

Порядок правил в pf решает всё. Без quick последнее совпавшее правило перекрывает предыдущие; с quick — останавливается на первом совпадении. Если route-to не срабатывает, почти всегда причина в порядке: более общее правило pass out all идёт после специфичного и перебивает его.

DNS утекает мимо туннеля. И на Linux, и на macOS split tunneling обычно затрагивает только IP-трафик, а DNS-запросы могут продолжать идти через резолвер провайдера, если явно не прописать DNS-сервер туннеля и не завернуть 53-й порт в те же правила. На Linux добавьте отдельный iptables-маркер для udp --dport 53 от нужного пользователя; на macOS — отдельное правило route-to для порта 53 либо DNS = в конфиге WireGuard плюс проверка через scutil --dns, что резолвер действительно указывает на VPN.

ip rule/pf-anchor не переживают перезагрузку. На Linux — см. выше про PostUp/PreDown и dispatcher-скрипты. На macOS правила pf тоже не персистентны по умолчанию: система на каждой загрузке применяет свой /etc/pf.conf, но включение (pfctl -e) и загрузка кастомного anchor должны быть прописаны в LaunchDaemon (/Library/LaunchDaemons/), иначе после ребута VPN снова поднимется полным туннелем без split.

Проверка, что split действительно работает. Не полагайтесь на ощущения — смотрите фактический маршрут и внешний IP до и после правила:

# Linux
ip route get 8.8.8.8
ip route get 8.8.8.8 mark 51820

# macOS
route get 8.8.8.8

А чтобы убедиться, что нужный трафик реально выходит через IP сервера, а не мимо туннеля — сверьте внешний адрес и геолокацию, как описано в статье Как проверить IP и страну после туннеля.

MTU и фрагментация. Split tunneling иногда обнажает проблему с MTU: часть трафика идёт напрямую с обычным MTU 1500, а часть — через WireGuard с MTU 1420 (или ниже, если есть двойная инкапсуляция). Если конкретные сайты через туннель зависают на загрузке, а пинг проходит — проверьте MTU в конфиге интерфейса, это частая причина «то работает, то нет» именно при частичном туннелировании.

Арендуйте сервер под свои задачи!

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли сделать split tunneling без root/sudo?

Нет — и iptables/ip rule на Linux, и pfctl на macOS требуют прав администратора, потому что меняют системную маршрутизацию, а не настройки конкретного приложения.

Что проще — split по адресату или по пользователю?

По адресату (список нужных подсетей в AllowedIPs) — значительно проще и не требует ни iptables, ни pf, только конфиг VPN-клиента. Split по пользователю/процессу нужен только тогда, когда список адресов заранее неизвестен или динамический.

pf в macOS не мешает системным функциям вроде интернет-шаринга?

Если добавлять правила через anchor и не переписывать системный /etc/pf.conf целиком, штатные правила Apple (NAT для раздачи интернета, файрвол приложений) продолжают работать — конфликты обычно возникают именно от полной перезаписи системного конфига.

Как проверить, что VPN-сервер вообще настроен правильно, до того как городить split tunneling?

Сначала убедитесь, что базовое подключение работает: для WireGuard — по инструкции как установить и настроить WireGuard на VPS, для клиента на Mac — подключение к WireGuard с macOS. Split tunneling — это надстройка поверх рабочего туннеля, а не замена ему.

На Windows та же логика работает?

Нет, там другой механизм — через метрики маршрутов и route add/настройки клиента, без iptables и pf. Разбор — в статье split tunneling на Windows.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →