Перевод сделан автором сайта goxpert.ru
Краткое содежание перевода
- В Linux существуют пространства имен для изоляции сетевых ресурсов.
- Сетевое пространство имен изолирует сетевые ресурсы, создавая свои собственные сетевые устройства, таблицы маршрутизации и правила брандмауэра.
- Команда ip в Linux является швейцарским армейским ножом для работы в сети.
- Именованные сетевые пространства имен проще в использовании и могут существовать без участия процессов в качестве членов.
- Устройства Veth используются для создания виртуальных сетевых устройств Ethernet, которые обеспечивают связь между пространствами имен.
- Для взаимодействия с Linux используется интерфейс Netlink, который предоставляет API поверх сокетов для сетевой маршрутизации и управления устройствами
Глубокое погружение в пространства имен Linux, часть 4
Пространства имен монтирования(Mount namespaces) изолируют ресурсы файловой системы. Это в значительной степени охватывает все, что связано с файлами в системе. Среди инкапсулированных ресурсов есть файл, содержащий список точек монтирования, которые видны процессу, и, как мы намекали в вступительном посте, изоляция может обеспечить такое поведение, что изменение списка (или любого другого файла) в некотором экземпляре пространства имен монтирования M(mount namespace) не влияло на этот список в другом экземпляре (так что только процессы в M наблюдают изменения).
В этом заключительном посте серии мы рассмотрим сетевые пространства имен. Как мы намекали во вступительном посте, сетевое пространство имен изолирует связанные с сетью ресурсы - процесс, запущенный в отдельном сетевом пространстве имен, имеет свои собственные сетевые устройства, таблицы маршрутизации, правила брандмауэра и т.д. Мы можем сразу увидеть это в действии, изучив нашу текущую сетевую среду.
Команда ip
Поскольку в этом посте мы будем взаимодействовать с сетевыми устройствами, мы восстановим требования к суперпользователю, которые мы смягчили в предыдущих постах. С этого момента мы будем предполагать, что оба ip и isolate выполняются с sudo.
1 | $ ip link list |
Звездой шоу здесь является ip command - швейцарский армейский нож для создания сетей в Linux - и мы будем широко использовать его в этом посте. Прямо сейчас мы только что запустили link list подкоманду, которая покажет нам, какие сетевые устройства в настоящее время доступны в системе (здесь у нас есть lo интерфейс обратной связи и ens33 интерфейс локальной сети ethernet).
Как и во всех других пространствах имен, система начинается с начального сетевого пространства имен, к которому принадлежат все процессы, если не указано иное. Запуск этой ip link list команды как есть дает нам сетевые устройства, принадлежащие исходному пространству имен (поскольку наша оболочка и ip команда принадлежат этому пространству имен).
Именованные сетевые пространства имен
Давайте создадим новое сетевое пространство имен:
1 | $ ip netns add coke |
И снова мы использовали команду ip. Его netns подкоманда позволяет нам играть с сетевыми пространствами имен - например, мы можем создавать новые сетевые пространства имен с помощью add подкоманды netns и использовать list, чтобы, ну, составить их список.
Вы могли заметить, что list вернуло только наше недавно созданное пространство имен - разве оно не должно возвращать как минимум два, второе из которых является исходным пространством имен, о котором мы упоминали ранее? Причина этого в том, что ip создается так называемое именованное сетевое пространство имен, которое просто является сетевым пространством имен, идентифицируемым по уникальному имени (в нашем случае coke). Через list показаны только именованные сетевые пространства имен, а начальное сетевое пространство имен не названо.
Именованные сетевые пространства имен получить проще. Например, для каждого именованного сетевого пространства имен в /var/run/netns папке создается файл, который может использоваться процессом, который хочет переключиться на свое пространство имен. Еще одним свойством именованных сетевых пространств имен является то, что они могут существовать без участия какого-либо процесса в качестве члена - в отличие от безымянных, которые будут удалены после завершения работы всех входящих в них процессов.
Теперь, когда у нас есть дочернее сетевое пространство имен, мы можем посмотреть на сеть с его точки зрения.
- Мы будем использовать командную строку C$ чтобы подчеркнуть оболочку, работающую внутри дочернего сетевого пространства имен.
1 | $ ip netns exec coke bash |
exec $namespace $command Подкоманда выполняется $command в именованном сетевом пространстве имен $namespace. Здесь мы запустили оболочку внутри coke пространства имен и перечислили доступные сетевые устройства. Мы видим, что, по крайней мере, наше ens33 устройство исчезло. Единственное устройство, которое отображается, - это loopback, и даже этот интерфейс не работает.
1 | C$ ping 127.0.0.1 |
Мы уже должны привыкнуть к этому, настройки по умолчанию для пространств имен обычно очень строгие. Как мы видим, в сетевых пространствах имен не будет никаких устройств, кроме loopback. Однако мы можем настроить loopback интерфейс без какой-либо бумажной волокиты:
1 | C$ ip link set dev lo up |
Сетевая изоляция
Мы уже начинаем понимать, что, запуская процесс во вложенном сетевом пространстве имен, таком как coke, мы можем быть уверены, что он изолирован от остальной системы в том, что касается сетевого взаимодействия. Наш процесс оболочки, запущенный в coke может взаимодействовать только через loopback - это означает, что он может взаимодействовать только с процессами, которые также являются членами coke пространства имен, но в настоящее время других процессов-членов нет (и во имя изоляции мы хотели бы, чтобы так и оставалось), так что здесь немного одиноко. Давайте попробуем немного ослабить эту изоляцию, мы создадим туннель, через который процессы в coke могут взаимодействовать с процессами в нашем исходном пространстве имен.
Теперь любое сетевое взаимодействие должно осуществляться через некоторое сетевое устройство, а устройство может существовать только в одном сетевом пространстве имен в любой момент времени, поэтому взаимодействие между любыми двумя процессами в разных пространствах имен должно осуществляться по крайней мере через два сетевых устройства - по одному в каждом сетевом пространстве имен.
Устройства Veth
Для удовлетворения наших потребностей мы будем использовать виртуальноевиртуальное(virtual) ethernet сетевое устройство (или, для краткости veth). Устройства Veth всегда создаются как пара устройств по типу туннеля, так что сообщения, записанные на устройство на одном конце, выходят из устройства на другом конце. Вы могли бы догадаться, что мы могли бы легко иметь один конец в исходном сетевом пространстве имен, а другой - в нашем дочернем сетевом пространстве имен, и вся связь между сетевыми пространствами имен осуществлялась бы через соответствующее конечное устройство veth (и вы были бы правы).
1 | # Create a veth pair (veth0 <=> veth1) |
Наше veth1 устройство теперь отображается в coke пространстве имен. Но чтобы пара veth заработала, нам нужно предоставить им обоим IP-адреса и настроить интерфейсы. Мы сделаем это в соответствующем сетевом пространстве имен.
1 | # In the initial namespace |
Мы должны увидеть, что veth1 установлено и имеет назначенный нам адрес 10.1.1.2 - то же самое должно произойти для veth0 в исходном пространстве имен. Теперь мы должны быть в состоянии выполнять пинг между пространствами имен между двумя процессами, запущенными в обоих пространствах имен.
1 | $ ping -I veth0 10.1.1.2 |
Реализация
Исходный код этого поста можно найти здесь.
Как обычно, сейчас мы попытаемся воспроизвести то, что видели до сих пор, в коде. В частности, нам нужно будет сделать следующее:
- Выполните команду в новом сетевом пространстве имен.
- Создайте пару veth (veth0 <=> veth1).
- Переместите устройство veth1 в новое пространство имен.
- Назначьте IP-адреса обоим устройствам и выведите их на экран.
Шаг 1 прост: мы создаем наш командный процесс в новом пространстве имен network, добавляя CLONE_NEWNET флаг в clone:
1 | int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET; |
Netlink
На оставшихся этапах мы в первую очередь будем использовать интерфейс Netlink для взаимодействия с Linux. Netlink в основном используется для связи между обычными приложениями (например, isolate) и ядром Linux. Поверх сокетов предоставляется API, основанный на протоколе, который определяет структуру и содержимое сообщений. Используя этот протокол, мы можем отправлять сообщения, которые Linux получает и преобразует в запросы - например, создать пару veth с именами veth0 и veth1.
Давайте начнем с создания нашего сокета netlink. В нем мы указываем, что хотим использовать NETLINK_ROUTE протокол - этот протокол охватывает реализации сетевой маршрутизации и управления устройствами.
1 | int create_socket(int domain, int type, int protocol) |
Формат сообщения Netlink
Сообщение Netlink представляет собой выровненный по 4 байта блок данных, содержащий заголовок (struct nlmsghdr) и полезную нагрузку. Формат заголовка описан здесь. Модуль службы сетевого интерфейса (NIS) определяет формат (struct ifinfomsg), с которого должна начинаться полезная нагрузка, связанная с администрированием сетевого интерфейса.
Наш запрос будет представлен следующей C структурой:
1 |
|
Атрибуты Netlink
Модуль NIS требует, чтобы полезная нагрузка кодировалась как атрибуты Netlink. Атрибуты предоставляют способ сегментировать полезную нагрузку на подразделы. Атрибут имеет тип и длину в дополнение к полезной нагрузке, содержащей его фактические данные.
Полезная нагрузка сообщения Netlink будет закодирована в виде списка атрибутов (где любой такой атрибут, в свою очередь, может иметь вложенные атрибуты), и у нас будут некоторые вспомогательные функции для заполнения его атрибутами. В коде атрибут представлен rtattr структурой в linux/rtnetlink.h заголовочном файле в виде:
1 | struct rtattr { |
rta_len это длина полезной нагрузки атрибута, которая непосредственно следует за rt_attr структурой в памяти (т. е. за следующими rta_len байтами). То, как интерпретируется содержимое этой полезной нагрузки, определяется rta_type , а возможные значения полностью зависят от реализации получателя и отправляемого запроса.
В попытке собрать все это воедино, давайте посмотрим, как isolate выполняет запрос netlink для создания пары veth со следующей функцией, create_veth которая выполняет шаг2:
1 | // ip link add ifname type veth ifname name peername |
Как мы можем видеть, нам нужно быть точными в отношении того, что мы здесь отправляем - нам нужно было закодировать сообщение именно так, как оно будет интерпретировано реализацией ядра, и здесь нам потребовалось для этого 3 вложенных атрибута. Я уверен, что это где-то задокументировано, хотя я не смог найти это после некоторого поиска в Google - в основном я разобрался с этим через strace и исходный код ip команды.
Следующим шагом 3 является метод, который, учитывая имя интерфейса ifname и дескриптор файла сетевого пространства имен netns, перемещает устройство, связанное с этим интерфейсом, в указанное сетевое пространство имен.
1 |
|
После создания пары veth и перемещения одного конца в наше целевое сетевое пространство имен, на шаге 4 мы назначаем IP-адреса обоих конечных устройств и запускаем их интерфейсы. Для этого у нас есть вспомогательная функция, if_up которая, получив имя интерфейса ifname и IP-адрес ip, присваивает ip устройству ifname и запускает его. Для краткости мы не показываем их здесь, но вместо этого их можно найти здесь.
Наконец, мы объединяем эти методы, чтобы подготовить наше сетевое пространство имен для нашего командного процесса.
1 | static void prepare_netns(int child_pid) |
Затем мы можем вызвать prepare_netns сразу после того, как закончим настройку пользовательского пространства имен.
1 | ... |
Давайте попробуем!
1 | $ sudo ./isolate sh |