Маршрутизация и сетевые пространства имён

Интеграция WireGuard с сетевыми пространствами имён Linux для контейнеризации и маршрутизации трафика

Интеграция с маршрутизацией и сетевыми пространствами имён

Как и все сетевые интерфейсы Linux, WireGuard интегрируется в инфраструктуру сетевых пространств имён (network namespaces). Это означает, что администратор может иметь несколько полностью разных сетевых подсистем и выбирать, какие интерфейсы находятся в каждом из них.

Что такое сетевые пространства имён? Это механизм ядра Linux, который позволяет изолировать сетевые стеки. Каждое пространство имён имеет свои собственные сетевые интерфейсы, таблицы маршрутизации, правила файервола и сокеты. Это основа для контейнеризации (Docker, LXC и другие).

WireGuard делает кое-что довольно интересное. Когда интерфейс WireGuard создаётся (с помощью ip link add wg0 type wireguard), он запоминает пространство имён, в котором был создан: «Я был создан в пространстве имён A». Позже WireGuard может быть перемещён в новые пространства имён («Я перемещаюсь в пространство имён B»), но он всё равно будет помнить, что произошёл из пространства имён A.

Почему это важно? WireGuard использует UDP-сокет для фактической отправки и получения зашифрованных пакетов. Этот сокет всегда живёт в пространстве имён A — исходном пространстве имён рождения. Это обеспечивает очень интересные возможности. А именно, вы можете создать интерфейс WireGuard в одном пространстве имён (A), переместить его в другое (B) и отправлять незашифрованные пакеты из пространства имён B, которые будут отправляться зашифрованными через UDP-сокет в пространстве имён A.

Это открывает очень интересные возможности.

Обычная контейнеризация

Наиболее очевидное использование этого — предоставить контейнерам (например, Docker-контейнерам) интерфейс WireGuard в качестве единственного интерфейса.

Схема контейнерной организации

Пример: Внутри контейнера видно только loopback-интерфейс и интерфейс WireGuard:

container # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
17: wg0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1423 qdisc noqueue state UNKNOWN group default qlen 1
    link/none 
    inet 192.168.4.33/32 scope global wg0
       valid_lft forever preferred_lft forever

Что это значит? Единственный способ доступа к сети — через wg0, интерфейс WireGuard. Весь трафик из контейнера автоматически шифруется и отправляется через VPN. Контейнер даже не знает о существовании физических сетевых интерфейсов хоста.

Настройка контейнера с WireGuard

Шаг 1: Создание сетевого пространства имён

Сначала создаём сетевое пространство имён с именем «container»:

# ip netns add container

Шаг 2: Создание интерфейса WireGuard

Создаём интерфейс WireGuard в исходном пространстве имён (init):

# ip link add wg0 type wireguard

Шаг 3: Перемещение интерфейса

Перемещаем интерфейс в новое пространство имён:

# ip link set wg0 netns container

Шаг 4: Настройка интерфейса

Теперь настраиваем wg0 как обычно, но указываем его новое пространство имён:

# ip -n container addr add 192.168.4.33/32 dev wg0
# ip netns exec container wg setconf wg0 /etc/wireguard/wg0.conf
# ip -n container link set wg0 up
# ip -n container route add default dev wg0

Результат: Теперь единственный способ доступа к сетевым ресурсам для «container» — через интерфейс WireGuard.

Маршрутизация всего трафика

Менее очевидное, но чрезвычайно мощное использование — применение этой характеристики WireGuard для перенаправления всего вашего обычного интернет-трафика через WireGuard.

Проблема: Как заставить весь трафик идти через VPN, но при этом позволить самому VPN-соединению использовать обычный интернет?

Классические решения

Классические решения основаны на различных конфигурациях таблиц маршрутизации. Для всех них нам нужно установить явный маршрут для фактической конечной точки WireGuard.

Пример: Предположим, конечная точка WireGuard — demo.wireguard.com, которая на момент написания резолвится в 163.172.161.0. Предположим также, что мы обычно подключаемся к интернету через eth0 со шлюзом 192.168.1.1.

Замена маршрута по умолчанию

Самая простая техника — просто заменить маршрут по умолчанию, но добавить явное правило для конечной точки WireGuard:

# ip route del default
# ip route add default dev wg0
# ip route add 163.172.161.0/32 via 192.168.1.1 dev eth0

Преимущества: Просто и понятно.

Недостатки: DHCP-демоны и другие программы любят отменять то, что мы только что сделали.

Переопределение маршрута по умолчанию

Вместо замены маршрута по умолчанию мы можем переопределить его двумя более специфичными правилами, которые в сумме дают маршрут по умолчанию, но совпадают раньше:

# ip route add 0.0.0.0/1 dev wg0
# ip route add 128.0.0.0/1 dev wg0
# ip route add 163.172.161.0/32 via 192.168.1.1 dev eth0

Объяснение: 0.0.0.0/1 + 128.0.0.0/1 = 0.0.0.0/0 (весь IPv4-диапазон). Эти маршруты имеют более высокий приоритет, чем маршрут по умолчанию, поэтому они используются для всего трафика, кроме явно исключённого.

Недостатки: Когда eth0 поднимается и опускается, явный маршрут для demo.wireguard.com забывается.

Маршрутизация на основе правил

Некоторые предпочитают использовать маршрутизацию на основе правил и несколько таблиц маршрутизации:

# ip rule add to 163.172.161.0 lookup main pref 30
# ip rule add to all lookup 80 pref 40
# ip route add default dev wg0 table 80

Объяснение: Правила определяют, какую таблицу маршрутизации использовать для каждого пакета. Для IP-адреса конечной точки используется основная таблица, для всего остального — таблица 80.

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

Улучшенная маршрутизация на основе правил

Предыдущее решение требует знания явного IP-адреса конечной точки, который должен быть исключён из туннеля. Но конечные точки WireGuard могут роумить, что означает, что это правило может устареть.

Решение: Устанавливаем fwmark на все пакеты, выходящие из UDP-сокета WireGuard, которые затем будут исключены из туннеля:

# wg set wg0 fwmark 1234
# ip route add default dev wg0 table 2468
# ip rule add not fwmark 1234 table 2468
# ip rule add table main suppress_prefixlength 0

Объяснение:

  1. Устанавливаем fwmark на интерфейсе
  2. Добавляем маршрут по умолчанию в альтернативную таблицу маршрутизации
  3. Указываем, что пакеты без fwmark должны использовать эту альтернативную таблицу
  4. Добавляем удобство для доступа к локальной сети

Это техника, используемая инструментом wg-quick(8).

Новое решение с пространствами имён

Оказывается, мы можем маршрутизировать весь интернет-трафик через WireGuard, используя сетевые пространства имён, а не классические хаки с таблицами маршрутизации.

Основная идея: Перемещаем интерфейсы, подключающиеся к интернету (например, eth0 или wlan0), в пространство имён «physical», а интерфейс WireGuard делаем единственным интерфейсом в пространстве имён «init».

Схема физического сетевого пространства имён

Пошаговая настройка

Шаг 1: Создание физического пространства имён

# ip netns add physical

Шаг 2: Перемещение физических интерфейсов

Перемещаем eth0 и wlan0 в пространство имён «physical»:

# ip link set eth0 netns physical
# iw phy phy0 set netns name physical

Теперь эти интерфейсы находятся в пространстве имён «physical», а в пространстве имён «init» интерфейсов нет:

# ip -n physical link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000
    link/ether ab:cd:ef:g1:23:45 brd ff:ff:ff:ff:ff:ff
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DORMANT group default qlen 1000
    link/ether 01:23:45:67:89:ab brd ff:ff:ff:ff:ff:ff

# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

Шаг 3: Создание интерфейса WireGuard в физическом пространстве

# ip -n physical link add wg0 type wireguard

Пространство имён рождения wg0 теперь — «physical», что означает, что UDP-сокеты с зашифрованными данными будут привязаны к устройствам eth0 и wlan0.

Шаг 4: Перемещение интерфейса в init

Перемещаем wg0 в пространство имён «init». Он всё равно будет помнить своё место рождения для сокетов:

# ip -n physical link set wg0 netns 1

Теперь в пространстве имён «init» есть устройство wg0:

# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
17: wg0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1423 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/none

Шаг 5: Настройка физических устройств

Настраиваем физические устройства с помощью обычных инструментов, но запускаем их внутри пространства имён «physical»:

# ip netns exec physical dhcpcd wlan0
# ip netns exec physical wpa_supplicant -iwlan0 -c/etc/wpa_supplicant/wpa_supplicant.conf
# ip -n physical addr add 192.168.12.52/24 dev eth0

Шаг 6: Настройка WireGuard

Настраиваем интерфейс wg0 как обычно и устанавливаем его как маршрут по умолчанию:

# wg setconf wg0 /etc/wireguard/wg0.conf
# ip addr add 10.2.4.5/32 dev wg0
# ip link set wg0 up
# ip route add default dev wg0

Результат: Все обычные процессы в системе маршрутизируют свои пакеты через пространство имён «init», которое содержит только интерфейс wg0 и его маршруты. Однако wg0 имеет UDP-сокет, живущий в пространстве имён «physical», что означает, что он будет отправлять трафик через eth0 или wlan0. Обычные процессы даже не знают о существовании eth0 или wlan0, за исключением dhcpcd и wpa_supplicant, которые были запущены внутри пространства имён «physical».

Выполнение команд в физическом пространстве

Иногда вам может понадобиться открыть веб-страницу или быстро сделать что-то, используя пространство имён «physical». Например, вы можете планировать маршрутизировать весь трафик через WireGuard, но кофейня, в которой вы сидите, требует аутентификации через веб-сайт перед предоставлением реального интернет-соединения.

Выполнение процесса в физическом пространстве:

$ sudo -E ip netns exec physical sudo -E -u \#$(id -u) -g \#$(id -g) chromium

Создание удобной функции для .bashrc:

physexec() { sudo -E ip netns exec physical sudo -E -u \#$(id -u) -g \#$(id -g) "$@"; }

Использование:

$ physexec chromium  # Открыть Chromium в физическом пространстве для аутентификации

После входа в сеть кофейни запускайте браузер как обычно:

$ chromium  # Весь трафик защищён WireGuard

Пример скрипта

Следующий пример скрипта можно сохранить как /usr/local/bin/wgphys и использовать для команд wgphys up, wgphys down и wgphys exec:

#!/bin/bash
set -ex

[[ $UID != 0 ]] && exec sudo -E "$(readlink -f "$0")" "$@"

up() {
    killall wpa_supplicant dhcpcd || true
    ip netns add physical
    ip -n physical link add wgvpn0 type wireguard
    ip -n physical link set wgvpn0 netns 1
    wg setconf wgvpn0 /etc/wireguard/wgvpn0.conf
    ip addr add 192.168.4.33/32 dev wgvpn0
    ip link set eth0 down
    ip link set wlan0 down
    ip link set eth0 netns physical
    iw phy phy0 set netns name physical
    ip netns exec physical dhcpcd -b eth0
    ip netns exec physical dhcpcd -b wlan0
    ip netns exec physical wpa_supplicant -B -c/etc/wpa_supplicant/wpa_supplicant-wlan0.conf -iwlan0
    ip link set wgvpn0 up
    ip route add default dev wgvpn0
}

down() {
    killall wpa_supplicant dhcpcd || true
    ip -n physical link set eth0 down
    ip -n physical link set wlan0 down
    ip -n physical link set eth0 netns 1
    ip netns exec physical iw phy phy0 set netns 1
    ip link del wgvpn0
    ip netns del physical
    dhcpcd -b eth0
    dhcpcd -b wlan0
    wpa_supplicant -B -c/etc/wpa_supplicant/wpa_supplicant-wlan0.conf -iwlan0
}

execi() {
    exec ip netns exec physical sudo -E -u \#${SUDO_UID:-$(id -u)} -g \#${SUDO_GID:-$(id -g)} -- "$@"
}

command="$1"
shift

case "$command" in
    up) up "$@" ;;
    down) down "$@" ;;
    exec) execi "$@" ;;
    *) echo "Usage: $0 up|down|exec" >&2; exit 1 ;;
esac

Демонстрация работы скрипта:

Демонстрация команды wgphys

Сравнение решений

РешениеПреимуществаНедостатки
Замена defaultПростотаDHCP отменяет изменения
Переопределение defaultНе заменяет defaultМаршрут забывается при переподключении
Правила (rule-based)Разделение таблицНужно знать IP конечной точки
Улучшенные правила (fwmark)Устойчив к роумингуСложнее для понимания
Пространства имёнПолная изоляция, чистотаТребует настройки пространств имён

Преимущества решения с пространствами имён

  1. Полная изоляция — обычные процессы не видят физические интерфейсы
  2. Автоматическая маршрутизация — весь трафик идёт через WireGuard без сложных правил
  3. Прозрачность — не нужно добавлять явные маршруты для конечных точек
  4. Чистота — нет хаков с таблицами маршрутизации
  5. Гибкость — можно запускать отдельные процессы в физическом пространстве

Ссылки