Часть 1
Часть 2
Часть 3
Часть 4 В завершающем посте этой серии мы рассмотрим Network namespaces. Как мы упоминали в вводном посте, network namespace изолирует ресурсы, связанные с сетью: процесс, работающий в отдельном network namespace, имеет собственные сетевые устройства, таблицы маршрутизации, правила фаервола и т.д. Мы можем непосредственно увидеть это на практике, рассмотрев наше текущее сетевое окружение. Звездой шоу здесь является команда ip — швейцарский армейский нож для работы с сетью в Linux — и мы будем активно использовать её в этом посте. Прямо сейчас мы только что выполнили подкоманду link list, чтобы увидеть, какие сетевые устройства в настоящее время доступны в системе (здесь есть lo — loopback-интерфес и `ens33, ethernet-интерфейс LAN. Как и со всеми другими пространствами имён, система стартует с начальным network namespace, которому принадлежат все процесс процессы, если не задано иное. Выполнение команды ip link list как есть показывает нам сетевые устройства, принадлежащие изначальному пространству имён (поскольку и наш шелл, и команда ip принадлежат этому пространству имён). Давайте создадим новый network namespace: И снова мы использовали команду ip. Подкоманда netns позволяет нам играться с пространствами имён network: например, мы можем создавать новые сетевые пространства network с помощью подкоманды add команды netns и использовать list для их вывода.
Вы могли заметить, что list возвращал только наш вновь созданный namespace. Разве он не должен возвращать по крайней мере два, одним из которых был бы исходным namespace, о котором мы упоминали ранее? Причина этого в том, что ip создаёт то, что называется named network namespace, который является просто network namespace, идентифицируемый уникальным именем (в нашем случае coke). Только именованные пространства имён network отображаются подкомандой list, а изначальный network namespace не именованный. Проще всего получить именованные пространства имён network. Например, в каждом именованном network namespace создаётся файл в каталоге /var/run/netns и им сможет воспользоваться процесс, который хочет переключиться на свой namespace. Другое свойство именованных пространств имён network заключается в том, что они могут существовать без наличия какого-либо процесса — в отличие от неименованных, которые будут удалены как только завершатся все принадлежащие им процессы. Теперь, когда у нас есть дочерний network namespace, мы можем взглянуть на сеть с его точки зрения. Запуск субкоманды exec $namespace $command выполняет $command в именованном network namespace $namespace. Здесь мы запустили шелл внутри пространства имён coke и посмотрели доступные сетевые устройства. Мы видим, что, по крайней мере, наше устройство ens33 исчезло. Единственное устройство, которое видно, это лупбек и даже этот интерфейс погашен. Мы должны теперь свыкнуться с тем, что настройки по умолчанию для пространств имён обычно очень строгие. Для пространств имён network, как мы видим, никаких устройств, помимо loopback, не будет доступно. Мы можем поднять интерфейс loopback без всяких формальностей: Мы уже начинаем понимать, что запустив процесс во вложенном network namespace, таком как coke, мы можем быть уверены, что он изолирован от остальной системы в том, что касается сети. Наш шелл-процесс, работающий в coke, может общаться только через loopback. Это означает, что он может общаться только с процессами, которые также являются членами пространства имён coke, но в настоящее время других таких процессов нет (и, во имя изолированности, мы хотели бы, чтобы так и оставалось), так что он немного одинок. Давайте попробуем несколько ослабить эту изолированность. Мы создадим туннель, через который процессы в coke смогут общаться с процессами в нашем исходном пространстве имён. Сейчас любое сетевое общение должно происходить через какое-то сетевое устройство, а устройство может существовать ровно в одном network namespace в данный конкретный момент времени, поэтому связь между любыми двумя процессами в разных пространствах имён должна осуществляться как минимум через два сетевых устройства — по одному в каждом network namespace. Для выполнения этого нашего требования, мы будем использовать сетевое устройство virtual ethernet (или сокращённо veth). Устройства veth всегда создаются как пара устройств, связанных по принципу туннеля, так что сообщения, отправленные на одном конце, выходят из устройства на другом. Вы могли бы предположить, что мы могли бы легко иметь один конец в исходном network namespace, а другой — в нашем дочернем network namespace, а всё общение между пространствами имён network проходило бы через соответствующее оконечное устройство veth (и вы были бы правы). Наше устройство veth1 теперь появилось в пространстве имён coke. Но чтобы заставить пару veth работать, нам нужно назначить там IP-адреса и поднять интерфейсы. Мы сделаем это в каждом соответствующем network namespace. Мы должны увидеть, что интерфейс veth1 поднят и имеет назначенный нами адрес 10.1.1.2. Тоже самое должно произойти с veth0 в исходном пространстве имён. Теперь у нас должна быть возможность сделать интер-namespace ping между двумя процессами, запущенными в обоих пространствах имён. Как обычно, теперь мы попытаемся воспроизвести то, что мы рассматривали до сих пор, в коде. В частности, нам нужно будет сделать следующее: Шаг 1 прост: мы создаём наш командный процесс в новом пространстве имён network путём добавления флага CLONE_NEWNET к clone: Для остальных шагов мы будем преимущественно использовать Netlink интерфейс чтобы общаться с Linux. Netlink в основном используется для связи между обычными приложениями (вроде isolate) и ядром Linux. Он предоставляет API поверх сокетов на основе протокола, который определяет структуру и содержание сообщения. Используя этот протокол, мы можем отправлять сообщения, которые получает Linux и преобразует в запросы вроде создать пару veth с именами veth0 и veth1. Давайте начнем с создания нашего сокета netlink. При этом мы укажем, что хотим использовать протокол NETLINK_ROUTE — этот протокол охватывает реализации сетевой маршрутизации и управления устройствами. Сообщение в netlink — это четырехбайтовый выровненный блок данный, содержащий заголовок (struct nlmsghdr) и полезную нагрузку. Формат заголовка описан здесь. Модуль The Network Interface Service (NIS) определяет формат (struct ifinfomsg), с которого должна начинаться полезная нагрузка, относящаяся к управлению сетевым интерфейсом. Наш следующий запрос будет представлен следующей структурой C: Модуль NIS требует, чтобы полезная нагрузка была закодирована как атрибуты Netlink. Атрибуты обеспечивают способ сегментировать полезную нагрузку на подсекции. Атрибут имеет тип и длину в дополнение к полезной нагрузке, содержащей сами данные. Полезная нагрузка в сообщении Netlink будет закодирована как список атрибутов (где любой такой атрибут, в свою очередь, может иметь вложенные атрибуты), а у нас будет несколько вспомогательных функций для заполнения его атрибутами. В коде атрибут представлен в заголовочном файле linux/rtnetlink.h структурой rtattr как: rta_len — это длина полезной нагрузки атрибута, что следует в памяти сразу за структурой rt_attr struct (то есть следующие rta_len байты). Как интерпретируется содержимое этой полезной нагрузки, задается rta_type, а возможные значения полностью зависят от реализации получателя и отправляемого запроса. В попытке собрать всё это вместе, давайте посмотрим, как isolate делает netlink запрос для создания для создания пары veth с помощью следующей функции create_veth, которая выполняет шаг 2: Как мы видим, нам нужно быть точными в том, что мы отправляем сюда: нам нужно было закодировать сообщение точно так, как оно будет интерпретироваться реализацией ядра, и здесь нам потребовалось три вложенных атрибута для этого. Я уверен, что это где-то задокументировано, хотя, немного погуглив, мне не удалось этого найти — в основном я разобрался с помощью strace и исходного кода команды ip. Далее, для шага 3, этот метод, который, учитывая имя интерфейса ifname и network namespace файлового дескриптора netns, перемещает устройство, связанное с этим интерфейсом, в указанный network namespace. После создания пары veth и перемещения одного конца в наш целевой network namespace, на шаге 4 мы назначаем IP-адреса обоим конечным устройствам и поднимаем их интерфейсы. Для этого у нас есть вспомогательная функция if_up, которая, учитывая имя интерфейса ifname и IP-адрес ip, назначает ip устройству ifname и поднимает его. Для краткости мы не показываем их тут, но вместо этого они могут быть найдены здесь. Наконец, мы объединяем эти методы, чтобы подготовить наш network namespace для нашего командного процесса. Затем мы можем вызвать prepare_netns сразу после того, как мы закончим настройку нашего user namespace. Давайте попробуем!