MAATRIX / Блог / Split tunneling на Linux через network namespaces

Split tunneling на Linux через network namespaces

MAATRIX

Policy routing через ip rule и fwmark решает split tunneling, но у него есть слабое место: любая ошибка в правилах или забытый PostDown — и трафик молча идёт мимо VPN тем же default route, что и всё остальное. Network namespace устроен иначе: это отдельное сетевое пространство с собственной таблицей маршрутов, собственными интерфейсами и собственным /etc/resolv.conf. Процесс, запущенный внутри такого namespace, физически не видит host-интерфейсы — если внутри нет маршрута через VPN, у него нет маршрута вообще, и трафик просто не уйдёт. Ниже — рабочая схема: отдельный netns, veth-пара для связи с хостом, WireGuard внутри и автозапуск через systemd.

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

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

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

Зачем namespace, если есть policy routing

Разница не в результате (и там, и там часть трафика идёт через VPN, часть — мимо), а в модели изоляции.

Policy routing (ip rule + fwmark)Network namespace
Область действияОбщее сетевое пространство, правила применяются ко всем пакетам с нужной меткойОтдельное пространство: интерфейсы, таблицы маршрутов, iptables, DNS — свои
Утечка при сбое VPNВозможна, если правило откатилось не полностьюНевозможна структурно — нет маршрута, нет пакета наружу
ГранулярностьПо адресу назначения, метке процесса, UIDПо процессу: то, что запущено в netns, то и туннелируется
Сложность настройкиНиже для простых случаев (несколько подсетей)Выше на старте, но не «протекает» со временем
Несколько независимых VPN одновременноТребует аккуратной работы с таблицами и меткамиЕстественно — один netns на один VPN, без пересечений

Если нужно направить в туннель конкретные подсети для всей системы — проще policy routing, он разобран в статье про split tunneling на Linux и macOS. Если нужно гарантированно посадить *конкретное приложение или группу процессов* за отдельный VPN так, чтобы утечка была архитектурно невозможна — берите namespace.

Схема: netns, veth и NAT для хендшейка

Namespace изолирован полностью, поэтому у него по умолчанию нет выхода в интернет — а он нужен, чтобы установить сам WireGuard-туннель (UDP-хендшейк должен куда-то уйти). Решение — veth-пара: виртуальный «провод» между host-сетью и namespace, плюс NAT на хосте.

Создаём namespace и veth-пару:

sudo ip netns add vpnns

sudo ip link add veth-host type veth peer name veth-ns
sudo ip link set veth-ns netns vpnns

# адресация на «проводе»: 10.200.200.0/30
sudo ip addr add 10.200.200.1/30 dev veth-host
sudo ip link set veth-host up

sudo ip netns exec vpnns ip addr add 10.200.200.2/30 dev veth-ns
sudo ip netns exec vpnns ip link set veth-ns up
sudo ip netns exec vpnns ip link set lo up

# временный default route для namespace — через veth, наружу через хост
sudo ip netns exec vpnns ip route add default via 10.200.200.1

На хосте включаем форвардинг и NAT, чтобы пакеты из 10.200.200.0/30 выходили через реальный внешний интерфейс (замените eth0 на своё имя интерфейса, ip a подскажет):

sudo sysctl -w net.ipv4.ip_forward=1
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.d/99-vpnns.conf

sudo iptables -t nat -A POSTROUTING -s 10.200.200.0/30 -o eth0 -j MASQUERADE
sudo iptables -A FORWARD -i veth-host -o eth0 -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o veth-host -m state --state RELATED,ESTABLISHED -j ACCEPT

Проверка — из namespace должен открываться интернет по обычному маршруту, ещё без VPN:

sudo ip netns exec vpnns curl -s ifconfig.me

Если curl вернул внешний IP хоста — «провод» работает, можно поднимать WireGuard.

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

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

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

Поднимаем WireGuard внутри namespace

Интерфейс WireGuard создаём сразу в root-пространстве и переносим в namespace — так проще отлаживать, если что-то пошло не так:

sudo ip link add wg0 type wireguard
sudo ip link set wg0 netns vpnns

Конфиг wg0.conf — обычный, без особенностей под namespace:

[Interface]
PrivateKey = <приватный_ключ_клиента>
Address = 10.66.66.2/32

[Peer]
PublicKey = <публичный_ключ_сервера>
Endpoint = <IP_сервера>:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

Поднимаем его командой wg-quick, выполненной внутри namespace:

sudo ip netns exec vpnns wg-quick up /etc/wireguard/wg0.conf

wg-quick при AllowedIPs = 0.0.0.0/0 сам создаёт policy-routing трюк: отдельную таблицу маршрутов (по умолчанию с меткой fwmark), default route через wg0 в этой таблице и отдельный маршрут до Endpoint через прежний default (то есть через veth-host и NAT на хосте). Всё это происходит внутри пространства имён vpnns и не задевает host — таблицы маршрутов у каждого netns свои.

Проверяем:

sudo ip netns exec vpnns wg show
sudo ip netns exec vpnns ip route show table all
sudo ip netns exec vpnns curl -s ifconfig.me

Последняя команда должна вернуть уже IP VPN-сервера, а не хоста.

Здесь же встроенный kill switch: если wg0 упадёт (сервер недоступен, истёк handshake), маршрут по умолчанию внутри namespace пропадает вместе с интерфейсом. У процессов в vpnns не останется default route вообще — не «утечёт через veth», а просто перестанет ходить в сеть. Про этот же принцип для обычных VPN-клиентов — в статье про kill switch для VPN.

DNS для namespace

Общесистемный /etc/resolv.conf в netns не подключается — и это хорошо: DNS-запросы приложений внутри vpnns тоже должны идти через VPN, иначе получите классическую DNS-утечку. iproute2 поддерживает для этого отдельный файл:

sudo mkdir -p /etc/netns/vpnns
echo "nameserver 10.66.66.1" | sudo tee /etc/netns/vpnns/resolv.conf

При входе в namespace через ip netns exec этот файл автоматически подменяет /etc/resolv.conf для процессов внутри — без дополнительных монтирований и без правки systemd-resolved на хосте. Если VPN-сервер не отдаёт свой DNS, укажите публичный резолвер, который точно ходит через туннель (например, DNS самого VPN-провайдера или 1.1.1.1, если AllowedIPs покрывает весь трафик). Проверка на утечку — тем же приёмом, что и в общей статье про проверку и закрытие DNS leak, только команды нужно выполнять с префиксом ip netns exec vpnns.

Запуск процессов внутри namespace

Любая программа, запущенная через ip netns exec vpnns <команда>, наследует сеть этого namespace — свои интерфейсы, свою таблицу маршрутов, свой DNS.

Для консольных инструментов всё просто:

sudo ip netns exec vpnns curl -s ifconfig.me
sudo ip netns exec vpnns ssh user@internal-host
sudo ip netns exec vpnns su - deploy -c "python3 sync.py"

С GUI-приложениями сложнее: ip netns exec не передаёт X11/Wayland-сокет автоматически, namespace изолирует только сеть, а не сессию рабочего стола. Рабочий, хоть и не самый чистый вариант для X11:

xhost +local:
sudo ip netns exec vpnns sudo -u "$USER" env DISPLAY="$DISPLAY" XAUTHORITY="$HOME/.Xauthority" firefox

Для регулярного использования удобнее завернуть команду в скрипт-обёртку — тогда xhost/XAUTHORITY не нужно вспоминать каждый раз руками.

Проверить, что процесс действительно сидит в нужном namespace, можно по PID:

ls -l /proc/<PID>/ns/net
sudo ip netns identify <PID>

Автозапуск и systemd-юнит

Вручную поднимать namespace, veth и WireGuard после каждой перезагрузки неудобно — оформим это systemd-юнитом с двумя скриптами.

/usr/local/sbin/vpnns-up.sh:

#!/bin/bash
set -e
ip netns add vpnns
ip link add veth-host type veth peer name veth-ns
ip link set veth-ns netns vpnns
ip addr add 10.200.200.1/30 dev veth-host
ip link set veth-host up
ip netns exec vpnns ip addr add 10.200.200.2/30 dev veth-ns
ip netns exec vpnns ip link set veth-ns up
ip netns exec vpnns ip link set lo up
ip netns exec vpnns ip route add default via 10.200.200.1
iptables -t nat -A POSTROUTING -s 10.200.200.0/30 -o eth0 -j MASQUERADE
iptables -A FORWARD -i veth-host -o eth0 -j ACCEPT
iptables -A FORWARD -i eth0 -o veth-host -m state --state RELATED,ESTABLISHED -j ACCEPT
ip link add wg0 type wireguard
ip link set wg0 netns vpnns
ip netns exec vpnns wg-quick up /etc/wireguard/wg0.conf

/usr/local/sbin/vpnns-down.sh:

#!/bin/bash
ip netns exec vpnns wg-quick down /etc/wireguard/wg0.conf || true
iptables -t nat -D POSTROUTING -s 10.200.200.0/30 -o eth0 -j MASQUERADE || true
iptables -D FORWARD -i veth-host -o eth0 -j ACCEPT || true
iptables -D FORWARD -i eth0 -o veth-host -m state --state RELATED,ESTABLISHED -j ACCEPT || true
ip link del veth-host || true
ip netns del vpnns || true

Юнит /etc/systemd/system/vpnns.service:

[Unit]
Description=Isolated netns with WireGuard for split tunneling
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/vpnns-up.sh
ExecStop=/usr/local/sbin/vpnns-down.sh

[Install]
WantedBy=multi-user.target

Включаем и проверяем:

sudo chmod +x /usr/local/sbin/vpnns-up.sh /usr/local/sbin/vpnns-down.sh
sudo systemctl daemon-reload
sudo systemctl enable --now vpnns.service
sudo systemctl status vpnns.service

Приложения, которым нужен туннель, запускайте через ip netns exec vpnns — отдельным systemd-юнитом с NetworkNamespacePath=/var/run/netns/vpnns (для systemd 245+) или просто обёрткой в ExecStart. Как поднять сам WireGuard-сервер на удалённой стороне — в статье про установку WireGuard на VPS.

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

  • curl: (7) Couldn't connect внутри namespace сразу после создания veth. Забыт ip forward на хосте или NAT-правило смотрит не в тот интерфейс. Проверьте sysctl net.ipv4.ip_forward и что -o в MASQUERADE указывает на реальный внешний интерфейс, а не на veth-host.
  • wg-quick падает на RTNETLINK answers: File exists. Обычно предыдущий wg0 не был убран (vpnns-down.sh не отработал). Проверьте ip netns exec vpnns ip link show и удалите зависший интерфейс: ip netns exec vpnns ip link del wg0.
  • Хендшейк не идёт, wg show показывает latest handshake пустым. Скорее всего endpoint недоступен из namespace: ip netns exec vpnns ip route get <IP_сервера> — маршрут должен идти через veth-ns, а не через wg0 (иначе замкнутый круг).
  • Утечка DNS, несмотря на namespace. Проверьте, что процесс запущен именно через ip netns exec vpnns (частая ошибка — тестировать в другом терминале, забыв префикс). cat /proc/<PID>/net/route покажет реальную таблицу маршрутов процесса.
  • После systemctl stop vpnns iptables-правила остаются. Правило добавляли вручную поверх юнита, и счётчик не совпал. Держите vpnns-up.sh/vpnns-down.sh симметричными и не трогайте эти цепочки руками.
  • Нужно два независимых VPN одновременно. Заведите второй namespace с другим именем, второй veth-парой на другой подсети (10.200.201.0/30) и вторым wg1 — конфликтов не будет, у каждого netns свои таблицы.

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

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

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

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

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

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

Чем это лучше, чем просто ip netns exec без veth, если WireGuard сам умеет ходить наружу?

WireGuard-интерфейс — это виртуальное устройство поверх UDP-сокета; сокету всё равно нужен реальный сетевой путь наружу. Без veth у namespace нет вообще никакого маршрута, и хендшейк просто не уйдёт с хоста.

Можно ли добавить в этот namespace второй интерфейс, например OpenVPN как резерв?

Да, но лучше не смешивать оба в одном netns без явных приоритетов маршрутов — проще держать по одному активному VPN на namespace и переключаться между ними через systemd-юниты (vpnns-wg.service, vpnns-ovpn.service с одинаковым именем namespace).

Работает ли это в Docker-контейнере или потребуется privileged-режим?

Да, Docker сам использует network namespaces под капотом, но для ручного создания дополнительных netns и переноса интерфейсов контейнеру нужны --cap-add=NET_ADMIN и --network=host (или запуск скриптов на хосте, а не внутри контейнера) — без этого ip link set ... netns не сработает.

Что будет, если процесс из namespace попробует достучаться до локальной сети (192.168.x.x)?

Ничего не будет — namespace не видит host-интерфейсы вообще, включая локальную сеть, если явно не пробросить туда ещё один маршрут через второй veth. Это плюс для изоляции, но учтите при работе с внутренними ресурсами.

Нужен ли отдельный netns, если VPN нужен всего одному процессу и на постоянной основе?

Не обязательно — для одного процесса иногда проще ip vrf (VRF-таблица без полной сетевой изоляции) или простой fwmark по UID процесса. Namespace оправдан, когда важна гарантия изоляции, а не только маршрутизация.

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

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

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