Изоляция трафика разных сетей в пределах одной операционной системы распространена повсеместно. Для конечных пользователей она незаметна — в этом ее цель. Но знаете ли вы, какие средства для разных уровней изоляции предоставляет ядро Linux и как ими воспользоваться самому, не рассчитывая на интегрированные средства управления вроде Docker? Давайте попрубуем разобраться.
Можно по-разому использовать изоляцию частей сетевого стека. Провайдеры предоставляют своим клиентам виртуальные выделенные линии и вынуждены как-то обходить конфликты адресов пользовательских сетей, чтобы агрегировать их трафик на одном определенном устройстве. Админы и программисты изолируют приложения в контейнерах, чтобы не запускать отдельную копию всей оперционной системы в виртуальной машине, и контейнер взаимодействует с миром через виртуальный интерфейс, хотя ядро у всех контейнеров общее.
В этой статье предполагается, что у вас свежее ядро — 4.8 или новее.
- Множественные таблицы маршрутизации
- Создание своих таблиц
- Создание правил
- Классификация пакетов по меткам netfilter
- Классификация по идентификаторам пользователей
- VRF
- Создание VRF
- Присваиваем VRF сетевые интерфейсы
- Выполнение команды внутри VRF
- Сетевые пространства имен (network namespaces)
- Создание пространств имен
- Выполнение команды
- Взаимодействие с другими пространствами имен
- Выводы
Множественные таблицы маршрутизации
Эта возможность известна всем, кто когда-либо делал балансировку между двумя провайдерами. Кроме таблицы маршрутизации по умолчанию, в Linux можно создать до 255 независимых друг от друга таблиц.
Таблица по умолчанию называется main, и ей присвоен номер 254. Именно в нее попадают маршруты, если добавить их с помощью ip route add или route без дополнительных параметров. Еще пара таблиц зарезервирована, а с остальными можно делать что захотите.
Какая таблица для какого трафика будет использоваться, определяется правилами, вы можете создать их с помощью ip rule add. Вывод ip rule show отобразит правила по умолчанию:
1 2 3 4 |
$ ip rule show 0: from all lookup local 32766: from all lookup main 32767: from all lookup default |
Символьные имена таблиц вы найдете (и перенастроите) в файле /etc/iproute2/rt_tables. У правила про таблицу main (254) такой низкий приоритет, чтобы пользовательские таблицы с меньшими номерами обрабатывались до нее. Как видите, никакого особого статуса у main на самом деле нет, это правило вообще можно было бы удалить или переопределить.
Таблица local, которая фигурирует в правиле 0, содержит специальные маршруты для локальных и широковещательных адресов. Например, присвоение адреса 192.0.2.1/24 интерфейсу eth0 создает вот такие маршруты в этой таблице:
1 2 3 |
broadcast 192.0.2.0 dev eth0 proto kernel scope link src 192.0.2.1 local 192.0.2.1 dev eth0 proto kernel scope host src 192.0.2.1 broadcast 192.0.2.255 dev eth0 proto kernel scope link src 192.0.2.1 |
Ядро самостоятельно управляет этой таблицей. О не стоит знать, но трогать в ней ничего не следует.
РЕКОМЕНДУЕМ:
Советы и трюки для настройки сети в Linux
Создание своих таблиц
Новые таблицы создаются автоматически, если добавить в них маршрут. Указать таблицу можно опцией table:
1 |
$ sudo ip route add 203.0.113.0/24 via 192.0.2.10 table 200 |
Чтобы увидеть маршруты в новой таблице, нужно добавить ту же опцию к ip route show.
1 2 |
$ ip route show table 200 203.0.113.0/24 via 192.0.2.1 dev eth0 |
В iproute2 все команды можно сокращать. Например: ip ro sh t 200.
Сами по себе таблицы не приносят никакой пользы — нужны правила, которые направят в них трафик.
Создание правил
Правила создаются командой ip rule add. Наиболее распространенный вариант применения — source-based routing, к примеру отправка трафика разных сетей через различных провайдеров.
Предположим, что у нас есть локальная сеть 10.0.0.0/24 и мы хотим отправить ее трафик в интернет через маршрутизатор 192.0.2.10, а не основной маршрут по умолчанию. Это можно сделать командами
1 2 |
$ sudo ip route add default via 192.0.2.10 table 200 $ sudo ip rule add from 10.0.0.0/24 table 200 |
За детальными инструкциями по работе с несколькими провайдерами можно обратиться к классическим lartc.org HOWTO или policyrouting.org, а за справкой по синтаксису — к man ip-rule или моему руководству.
Классификация пакетов по меткам netfilter
Факт первый: возможности классификации трафика можно серьезно расширить с помощью опции fwmark. Сама команда ip rule add поддерживает не очень много собственно сетевых опций: from/ to (адрес источника и назначений), iif/ oif (входной и выходной интерфейс), ipproto (протокол), sport/ dport (порт источника и назначения), tos (значение Type of Service).
К счастью, netfilter может добавлять к пакетам метки, можно ссылаться на них в опции fwmark и создавать правила даже для самых экзотических ситуаций.
Главное — знать порядок обработки пакетов в ядре и добавлять правила для установки меток в цепочку, которая обрабатывается до маршрутизации пакетов: например, mangle OUTPUT для локального трафика и mangle FORWARD для трафика из других сетей.
Порядок обработки пакетов отлично показан на диаграмме.
В качестве глупого, но показательного примера мы отправим ICMP-пакеты размером больше ста байт через шлюз 192.0.2.20. Для простоты тестирования мы сделаем, чтобы правило применялось к локальным пакетам, поэтому метку мы ставим в цепочке mangle OUTPUT:
1 2 3 |
$ sudo iptables -t mangle -I OUTPUT -m length --length 100: -p icmp -j MARK --set-mark 0xdeadbeef $ sudo ip route add default via 192.0.2.20 table 201 $ sudo ip rule add fwmark 0xdeadbeef table 201 |
Чтобы протестировать в реальности, замените 192.0.2.10 на какой-нибудь хост из вашей сети — ядро Linux не добавляет маршруты через заведомо недостижимый шлюз.
Классификация по идентификаторам пользователей
Факт второй: правила можно применять к отдельным пользователям. Возможных применений два. Первое — имитировать VRF или netns, запустив процесс от имени пользователя, к которому применяется правило. Так делать не стоит, в новых ядрах есть намного лучшие решения.
Но эта возможность может помочь в отладке правил. Проверить, как работают маршруты, может быть не так просто, особенно если нет доступа ко всем хостам. В таком случае можно применить правило к пользователю и тестировать от его имени. Таким же способом можно завернуть весь трафик пользователя в VPN-туннель, не трогая системный трафик. Допустим, у нас поднят OpenVPN с интерфейсом tun0 и есть пользователь test с id=1000. Отправим весь его трафик через VPN:
1 2 |
$ sudo ip route add default dev tun0 table 205 $ sudo ip rule add uidrange 1000-1000 table 205 |
Как видите, возможные применения множественных таблиц и правил весьма широки. Но они не позволяют разрешить конфликт адресов между сегментами сети или полностью изолировать трафик процесса. Для этих целей нужно использовать VRF или network namespaces. Начнем с VRF.
РЕКОМЕНДУЕМ:
Туннели в Linux
VRF
VRF (Virtual Routing and Forwarding) наиболее популярен среди провайдеров, которым нужно подключить независимых клиентов к одному физическому устройству. С его помощью можно ассоциировать сетевые интерфейсы с отдельными таблицами маршрутизации. Если думаете, что того же эффекта можно достигнуть опцией iif/oif в ip rule add, то спешу огорчить вас — это не совсем так.
С «простыми» таблицами и правилами вы можете добавить свои маршруты в отдельную таблицу, но служебные маршруты ядра — local и broadcast, которые требуются для правильной работы с адресами интерфейса, остаются в таблице по умолчанию. Это и делает невозможным подключение клиентов с одинаковыми или пересекающимися сетями к одному маршрутизатору.
Каждый раз, когда мы присваиваем интерфейсу адрес, ядро создает уже описанные выше local- и broadcast-маршруты в таблице local и маршрут к сети в таблице main. Например, присвоение интерфейсу eth0 адреса 192.0.2.100/24 создает маршрут вида
1 |
192.0.2.0/24 dev eth1 proto kernel scope link src 192.0.2.100 |
Эти маршруты называют connected routes. Если бы их не было, создать маршрут вроде ip route add default via 192.0.2.1 было бы невозможно: адрес 192.0.2.1 считался бы недостижимым из-за отсутствия явного маршрута к 192.0.2.0/24.
VRF позволяет разрешить конфликты адресов, перенеся в отдельную таблицу все ассоциированные с сетевым интерфейсом маршруты — как пользовательские, так и служебные.
Создание VRF
Для примера создадим VRF с названием customer1 и привяжем его к таблице маршрутизации 50:
1 2 |
$ sudo ip link add customer1 type vrf table 50 $ sudo ip link set dev customer1 up |
Он выглядит как виртуальная сетевая карта. Все сетевые интерфейсы ядро создает в выключенном состоянии, поэтому без второй команды не обойтись: перед использованием его нужно включить. Просмотреть VRF вы можете командой ip vrf show:
1 2 3 4 |
$ ip vrf show Name Table ----------------------- customer1 50 |
Присваиваем VRF сетевые интерфейсы
В VRF можно добавить любые интерфейсы, как физические, так и виртуальные (к примеру, туннели). Для демонстрации мы создадим интерфейс типа dummy (аналог loopback) и присвоим ему адрес 10.133.0.1/24:
1 2 3 |
$ sudo ip link add dev dum1 type dummy $ sudo ip link set dum1 up $ sudo ip address add 10.133.0.1/24 dev dum1 |
Каждый раз, когда мы присваиваем интерфейсу адрес, в таблице main появляется маршрут к его сети:
1 2 |
$ ip route show 10.133.0.1/24 10.133.0.0/24 dev dum1 proto kernel scope link src 10.133.0.1 |
Теперь привяжем наш интерфейс к VRF customer1:
1 |
$ sudo ip link set dum1 master |
Картина в ip vrf show не изменится — к сожалению, эта команда не показывает принадлежность интерфейсов VRF. Все интерфейсы определенного VRF можно увидеть в выводе ip link show vrf $VRF:
1 2 3 |
$ ip link show vrf customer1 6: dum1: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue master customer1 state UNKNOWN mode DEFAULT group default qlen 1000 link/ether 8e:6f:b6:9a:df:e0 brd ff:ff:ff:ff:ff:ff |
Если выполнить ip ro sh 10.133.0.0/24, вы увидите, что маршрут к этой сети исчез. Ядро перенесло его и все остальные служебные маршруты для этой сети в таблицу 50:
1 2 3 4 5 |
$ ip ro sh t 50 broadcast 10.133.0.0 dev dum1 proto kernel scope link src 10.133.0.1 10.133.0.0/24 dev dum1 proto kernel scope link src 10.133.0.1 local 10.133.0.1 dev dum1 proto kernel scope host src 10.133.0.1 broadcast 10.133.0.255 dev dum1 proto kernel scope link src 10.133.0.1 |
Если удалить интерфейс из VRF командой sudo ip link set dum1 nomaster, маршруты вернутся обратно.
РЕКОМЕНДУЕМ:
Этапы загрузки Linux
Выполнение команды внутри VRF
VRF изолирует именно сети, а не приложения. Один и тот же процесс может подключиться к нескольким разным VRF. Работу с VRF с точки зрения разработчика приложений мы не будем затрагивать в этой статье.
Знать про существование VRF нужно очень узкому классу программ: в основном демонам протоколов маршрутизации и различным реализациям VPN. Любую программу очень легко запустить внутри любого VRF с помощью команды ip vrf exec $VRF $COMMAND.
К примеру, выполним ping к адресу самого dum1. Из VRF по умолчанию он не сработает, поскольку его маршруты были удалены из основной таблицы:
1 2 3 4 5 |
$ ping 10.133.0.1 PING 10.133.0.1 (10.133.0.1) 56(84) bytes of data. ^C --- 10.133.0.1 ping statistics --- 4 packets transmitted, 0 received, 100% packet loss, time 58ms |
Но внутри VRF customer1 все работает:
1 2 3 |
$ sudo ip vrf exec customer1 ping 10.133.0.1 PING 10.133.0.1 (10.133.0.1) 56(84) bytes of data. 64 bytes from 10.133.0.1: icmp_seq=1 ttl=64 time=0.042 ms |
Данным способом можно выполнить любую команду (включая другую команду ip). Это существенно упрощает отладку и тестирование настроек VRF по сравнению с простыми таблицами и правилами.
Сетевые пространства имен (network namespaces)
VRF обеспечивает изоляцию только на сетевом уровне. Канальный уровень остается общим для всех VRF, и, к примеру, sudo ip vrf exec customer1 ip link list покажет нам все интерфейсы: и принадлежащий нашему VRF dum1, и все остальные. Это может быть как преимуществом, так и недостатком.
Если VRF недостаточно, можно привлечь сетевые пространства имен (netns) — самый глубокий уровень изоляции. Именно его используют контейнеры вроде LXC. Они создают полную копию сетевого стека, и процессы в одном пространстве имен не могут увидеть ничего из других — даже физические сетевые интерфейсы.
Создание пространств имен
Создать новое пространство имен можно командой ip netns add, а просмотреть список существующих — командой ip netns show.
1 2 3 |
$ sudo ip netns add mynamespace $ ip netns show mynamespace |
Для простоты тестирования в пределах отдельно взятой машины создадим еще один dummy-интерфейс dum2 и присвоим его нашему пространству имен:
1 2 3 |
$ sudo ip link add dum2 type dummy $ sudo ip link set dum2 up $ sudo ip link set dum2 netns mynamespace |
Если в случае с VRF мы могли увидеть наш dum1 в обычном выводе ip link list, то с пространствами имен дела обстоят иначе: dum2 исчезнет оттуда и увидеть его можно будет только изнутри mynamespace. Давайте убедимся в этом.
Выполнение команды
Выполнить команду внутри пространства имен можно с помощью ip netns exec $NAMESPACE $COMMAND. Если выполнить данным способом ip link list внутри mynamspace, мы увидим dum2 и копию интерфейса lo (loopback) — и ничего больше.
1 2 3 4 5 |
$ sudo ip netns exec mynamespace ip link list 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 7: dum2: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether ca:00:b9:06:49:70 brd ff:ff:ff:ff:ff:ff |
В свежих версиях iproute2 (в моей 5.0.0 точно) есть более короткий способ — ip -n $NAMESPACE:
1 2 3 |
$ sudo ip -n mynamespace link show dum2 7: dum2: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether ca:00:b9:06:49:70 brd ff:ff:ff:ff:ff:ff |
Следует отметить, что при передаче интерфейса в другое пространство имен он всегда будет выключен и с него будут удалены все адреса, поэтому в скриптах инициализации и подобном поднимать интерфейс и настраивать его нужно уже после ip link set ... netns.
Взаимодействие с другими пространствами имен
Как видите, устройства и процессы внутри пространства имен оказываются в полной изоляции. Взаимодействие между разными пространствами возможно только через пару виртуальных интерфейсов. Для этого используются специальные интерфейсы типа veth. Они всегда создаются парами, создать только один невозможно:
1 |
$ sudo ip link add name veth1 type veth peer name veth2 |
Устройство veth1 мы оставим в пространстве имен по умолчанию, а veth2 добавим в mynamespace, после чего включим оба интерфейса (они, как обычно, создаются выключенными):
1 2 3 |
$ sudo ip link set dev veth2 netns mynamespace $ sudo ip -n mynamespace link set veth2 up $ sudo ip link set dev veth1 up |
Мы присвоим адреса обоим интерфейсам, и взаимодействие между пространствами имен будет организовано так же, как маршрутизация трафика между разными хостами. Используем сеть 10.136.0.0/30 для связи между ними:
1 2 3 4 5 |
$ sudo ip addr add 10.136.0.1/30 dev veth1 $ sudo ip -n mynamespace address add 10.136.0.2/30 dev veth2 $ ping 10.136.0.2 PING 10.136.0.2 (10.136.0.2) 56(84) bytes of data. 64 bytes from 10.136.0.2: icmp_seq=1 ttl=64 time=0.057 ms |
Добавим адрес интерфейсу dum2 и статический маршрут к его сети из основного пространства имен.
1 2 3 4 5 |
$ sudo ip -n mynamespace address add 10.134.0.1/24 dev dum2 $ sudo ip ro add 10.134.0.0/24 via 10.136.0.2 $ ping 10.134.0.1 PING 10.134.0.1 (10.134.0.1) 56(84) bytes of data. 64 bytes from 10.134.0.1: icmp_seq=1 ttl=64 time=0.036 ms |
Поскольку связь между пространствами имен неотличима от физического соединения или туннеля, с их помощью можно тестировать сценарии настройки сети без создания отдельных контейнеров или виртуальных машин.
Существует несколько проектов для интеграции с systemd, позволяющих упростить изоляцию приложений, например systemd-named-netns.
РЕКОМЕНДУЕМ:
Погружение в x86-64 SystemV ABI
Выводы
Как видите, ядро Linux предоставляет механизмы изоляции сетевого трафика на все случаи жизни — от минимальной до полной. Надеюсь, эта статья поможет вам выбрать нужный для ваших задач и эффективно им воспользоваться.