- Глубокое погружение в Linux namespaces, часть 4
- Команда ip
- Именованные пространства имён Network
- Сетевое изолирование
- Устройства veth
- Реализация
- Netlink
- Netlink Message Format
- Атрибуты Netlink
- Глубокое погружение в Linux namespaces, часть 2
- User Namespaces
- Маппинги User ID
- Map-файлы
- Написание UID Map файлов
- Владелец пространств имён и привилегии
- Как разрешаются ID
- Групповые ID
- Реализация
Глубокое погружение в Linux namespaces, часть 4
В завершающем посте этой серии мы рассмотрим Network namespaces. Как мы упоминали в вводном посте, network namespace изолирует ресурсы, связанные с сетью: процесс, работающий в отдельном network namespace, имеет собственные сетевые устройства, таблицы маршрутизации, правила фаервола и т.д. Мы можем непосредственно увидеть это на практике, рассмотрев наше текущее сетевое окружение.
Команда ip
Поскольку в этом посте мы будем взаимодействовать с сетевыми устройствами, мы вернем жесткое требование наличия прав суперпользователя, которое мы смягчили в предыдущих постах. С этого момента мы будем предполагать, что как ip , так и isolate будут запускаться с sudo .
Звездой шоу здесь является команда ip — швейцарский армейский нож для работы с сетью в Linux — и мы будем активно использовать её в этом посте. Прямо сейчас мы только что выполнили подкоманду link list , чтобы увидеть, какие сетевые устройства в настоящее время доступны в системе (здесь есть lo — loopback-интерфес и `ens33, ethernet-интерфейс LAN.
Как и со всеми другими пространствами имён, система стартует с начальным network namespace, которому принадлежат все процесс процессы, если не задано иное. Выполнение команды ip link list как есть показывает нам сетевые устройства, принадлежащие изначальному пространству имён (поскольку и наш шелл, и команда ip принадлежат этому пространству имён).
Именованные пространства имён Network
Давайте создадим новый 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, мы можем взглянуть на сеть с его точки зрения.
Мы будем использовать приглашение командной строки C$ для обозначения шелла, работающего в дочернем 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.
Устройства veth
Для выполнения этого нашего требования, мы будем использовать сетевое устройство virtual ethernet (или сокращённо veth ). Устройства veth всегда создаются как пара устройств, связанных по принципу туннеля, так что сообщения, отправленные на одном конце, выходят из устройства на другом. Вы могли бы предположить, что мы могли бы легко иметь один конец в исходном network namespace, а другой — в нашем дочернем network namespace, а всё общение между пространствами имён network проходило бы через соответствующее оконечное устройство veth (и вы были бы правы).
Наше устройство veth1 теперь появилось в пространстве имён coke . Но чтобы заставить пару veth работать, нам нужно назначить там IP-адреса и поднять интерфейсы. Мы сделаем это в каждом соответствующем network namespace.
Мы должны увидеть, что интерфейс veth1 поднят и имеет назначенный нами адрес 10.1.1.2 . Тоже самое должно произойти с veth0 в исходном пространстве имён. Теперь у нас должна быть возможность сделать интер-namespace ping между двумя процессами, запущенными в обоих пространствах имён.
Реализация
Как обычно, теперь мы попытаемся воспроизвести то, что мы рассматривали до сих пор, в коде. В частности, нам нужно будет сделать следующее:
- Выполнить команду в новом network namespace.
- Создать пару veth (veth0 veth1).
- Переместить устройство veth1 в новый namespace.
- Назначить IP-адреса обоим устройствам и поднять их.
Шаг 1 прост: мы создаём наш командный процесс в новом пространстве имён network путём добавления флага CLONE_NEWNET к clone :
Netlink
Для остальных шагов мы будем преимущественно использовать Netlink интерфейс чтобы общаться с Linux. Netlink в основном используется для связи между обычными приложениями (вроде isolate ) и ядром Linux. Он предоставляет API поверх сокетов на основе протокола, который определяет структуру и содержание сообщения. Используя этот протокол, мы можем отправлять сообщения, которые получает Linux и преобразует в запросы вроде создать пару veth с именами veth0 и veth1.
Давайте начнем с создания нашего сокета netlink. При этом мы укажем, что хотим использовать протокол NETLINK_ROUTE — этот протокол охватывает реализации сетевой маршрутизации и управления устройствами.
Netlink Message Format
Сообщение в netlink — это четырехбайтовый выровненный блок данный, содержащий заголовок ( struct nlmsghdr ) и полезную нагрузку. Формат заголовка описан здесь. Модуль The Network Interface Service (NIS) определяет формат ( struct ifinfomsg ), с которого должна начинаться полезная нагрузка, относящаяся к управлению сетевым интерфейсом.
Наш следующий запрос будет представлен следующей структурой C :
Атрибуты Netlink
Модуль 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.
Источник
Глубокое погружение в Linux namespaces, часть 2
В предыдущей части мы только окунули пальцы ног в воды namespace и при этом увидели, как это было просто — запустить процесс в изолированном UTS namespace. В этом посте мы осветим User namespace.
Среди прочих ресурсов, связанных с безопасностью, User namespaces изолирует идентификаторы пользователей и групп в системе. В этом посте мы сосредоточимся исключительно на ресурсах user и group ID (UID и GID соответственно), поскольку они играют фундаментальную роль в проведении проверок разрешений и других действий во всей системе, связанных с безопасностью.
В Linux эти ID — просто целые числа, которые идентифицируют пользователей и группы в системе. И каждому процессу назначаются какие-то из них, чтобы задать к каким операциями/ресурсам этот процесс может и не может получить доступ. Способность процесса нанести ущерб зависит от разрешений, связанных с назначенными ID.
User Namespaces
Мы проиллюстрируем возможности user namespaces, используя только пользовательские ID. Точно такие же действия применимы к групповым ID, к которым мы обратимся далее в этому посте.
User namespace имеет собственную копию пользовательского и группового идентификаторов. Затем изолирование позволяет связать процесс с другим набором ID — в зависимости от user namespace, которому он принадлежит в данный момент. Например, процесс $pid может выполняться от root (UID 0) в user namespace P и внезапно продолжает выполняться от proxy (UID 13) после переключения в другой user namespace Q.
User spaces могут быть вложенными! Это означает, что экземпляр пользовательского namespace (родительский) может иметь ноль и больше дочерних пространств имён, и каждое дочернее пространство имён может, в свою очередь, иметь свои собственные дочерние пространства имён и так далее… (до достижения предела в 32 уровня вложенности). Когда создаётся новый namespace C, Linux устанавливает текущий User namespace процесса P, создающего C, как родительский для C и это не может быть изменено впоследствии. В результате все user namespaces имеют ровно одного родителя, образуя древовидную структуру пространств имён. И, как и в случае с деревьями, исключение из этого правила находится наверху, где у нас есть корневой (или начальный, дефолтный) namespace. Это, если вы еще не делаете какую-то контейнерную магию, скорее всего user namespace, к которому принадлежат все ваши процессы, поскольку это единственный user namespace с момента запуска системы.
В этом посте мы будем использовать приглашения командной строки P$ и C$ для обозначения шела, который в настоящее время работает в родительском P и дочернем C user namespace соответственно.
Маппинги User ID
User namespace, по сути, содержит набор идентификаторов и некоторую информацию, связывающую эти ID с набором ID других user namespace — этот дуэт определяет полное представление о ID процессов, доступных в системе. Давайте посмотрим, как это может выглядеть:
В другом окне терминала давайте запустим шелл с помощью unshare (флаг -U создаёт процесс в новом user namespace):
Погодите, кто? Теперь, когда мы находимся во вложенном шелле в C, текущий пользователь становится nobody? Мы могли бы догадаться, что поскольку C является новым user namespace, процесс может иметь иной вид ID. Поэтому мы, возможно, и не ждали, что он останется iffy , но nobody — это не смешно. С другой стороны, это здорово, потому что мы получили изолирование, которое и хотели. Наш процесс теперь имеет другую (хоть и поломанную) подстановку ID в системе — в настоящее время он видит всех, как nobody и каждую группу как nogroup .
Информация, связывающая UID из одного user namespace с другим, называется маппингом user ID. Он представляет из себя таблицы поиска соответствия ID в текущем user namespace для ID в других namespace и каждый user namespace связан ровно одним маппингом UID (в дополнение еще к одному маппингу GID для group ID).
Этот маппинг и есть то, что сломано в нашем unshare шелле. Оказывается, что новые user namespaces начинаются с пустого маппинга, и в результате Linux по умолчанию использует ужасного пользователя nobody . Нам нужно исправить это, прежде чем мы сможем сделать какую-либо полезную работу в нашем новом пространстве имён. Например, в настоящее время системные вызовы (например, setuid ), которые пытаются работать с UID, потерпят неудачу. Но не бойтесь! Верный традиции всё-есть-файл, Linux представляет этот маппинг с помощью файловой системы /proc в /proc/$pid/uid_map (в /proc/$pid/gid_map для GID), где $pid — ID процесса. Мы будем называть эти два файла map-файлами
Map-файлы
Map-файлы — особенные файлы в системе. Чем особенные? Ну, тем, что возвращают разное содержимое всякий раз, когда вы читаете из них, в зависимости от того, какой ваш процесс читает. Например, map-файл /proc/$pid/uid_maps возвращает маппинг от UID’ов из user namespace, которому принадлежит процесс $pid , UID’ам в user namespace читающего процесса. И, как следствие, содержимое, возвращаемое в процесс X, может отличаться от того, что вернулось в процесс Y, даже если они читают один и тот же map файл одновременно.
В частности, процесс X, считывающий UID map-файл /proc/$pid/uid_map , получает набор строк. Каждая строка отображает непрерывный диапазон UID’ов в user namespace C процесса $pid , соответствующий диапазону UID в другом namespace.
Каждая строка имеет формат $fromID $toID $length , где:
- $fromID является стартовым UID диапазона для user namespace процесса $pid
- $lenght — это длина диапазона.
- Трансляция $toID зависит от читающего процесса X. Если X принадлежит другому user namespace U, то $toID — это стартовый UID диапазона в U, который мапится с $fromID . В противном случае $toID — это стартовый UID диапазона в P — родительского user namespace процесса C.
Например, если процесс читает файл /proc/1409/uid_map и среди полученных строк видно 15 22 5 , то UID’ы с 15 по 19 в user namespace процесса 1409 маппятся в UID’ы 22-26 отдельного user namespace читающего процесса.
С другой стороны, если процесс читает из файла /proc/$$/uid_map (или map-файла любого процесса, принадлежащего тому же user namespace, что и читающий процесс) и получает 15 22 5 , то UID’ы c 15 по 19 в user namespace C маппятся в UID’ы c 22 по 26 родительского для C user namespace.
Давайте это попробуем:
Хорошо, это было не очень захватывающе, так как это были два крайних случая, но это говорит там о нескольких вещах:
- Вновь созданный user namespace будет фактически иметь пустые map-файлы.
- UID 4294967295 не маппится и непригоден для использования даже в root user namespace. Linux использует этот UID специально, чтобы показать отсутствие user ID.
Написание UID Map файлов
Чтобы исправить наш вновь созданный user namespace C, нам просто нужно предоставить наши нужные маппинги, записав их содержимое в map-файлы для любого процесса, который принадлежит C (мы не можем обновить этот файл после записи в него). Запись в этот файл говорит Linux две вещи:
- Какие UID’ы доступны для процессов, которые относятся к целевому user namespace C.
- Какие UID’s в текущем user namespace соответствуют UID’ам в C.
Например, если мы из родительского user namespace P запишем следующее в map-файл для дочернего пространства имён C:
мы по существу говорим Linux, что:
- Что касается процессов в C, единственным UID’ами, которые существуют в системе, являются UID’ы 0 и 3 . Например, системный вызов setuid(9) всегда будет завершаться чем-то вроде недопустимого id пользователя.
- UID’ы 1000 и 0 в P соответствуют UID’ам 0 и 3 в C. Например, если процесс, работающий с UID 1000 в P, переключится в C, он обнаружит, что после переключения его UID стал root 0 .
Владелец пространств имён и привилегии
В предыдущем посте мы упомянули, что при создании новых пространств имён требуется доступ с уровнем суперпользователя. User namespaces не налагают этого требования. На самом деле, еще одной их особенностью является то, что они могут владеть другими пространствами имён.
Всякий раз, когда создаётся не user namespace N, Linux назначает текущий user namespace P процесса, создающего N, владельцем namespace N. Если P создан наряду с другими пространствами имён в одном и том же системном вызове clone , Linux гарантирует, что P будет создан первым и назначен владельцем других пространств имён.
Владелец пространств имён важен потому, что процесс, запрашивающий выполнения привилегированного действия над ресурсом, задействованным не user namespace, будет иметь свои UID привилегии, проверенные в отношении владельца этого user namespace, а не корневого user namespace. Например, скажем, что P является родительским user namespace дочернего C, а P и C владеют собственными network namespace M и N соответственно. Процесс может не иметь привилегий для создания сетевых устройств, включенных в M, но может быть в состоянии это делать для N.
Следствием наличия владельца пространств имён для нас является то, что мы можем отбросить требование sudo при выполнении команд с помощью unshare или isolate , если если мы запрашиваем также создание и user namespace. Например, unshare -u bash потребует sudo , но unshare -Uu bash — уже нет:
К сожалению, мы повторно применим требование прав суперпользователя в следующем посте, так как isolate нуждается в привилегиях root в корневом user namespace, чтобы корректно настроить Mount и Network namespace. Но мы обязательно отбросим привилегии командного процесса, чтобы убедиться, что команда не имеет ненужных разрешений.
Как разрешаются ID
Мы только что увидели процесс, запущенный от обычного пользователя 1000 внезапно переключился на root . Не волнуйтесь, никакой эскалации привилегий не было. Помните, что это просто маппинг ID: пока наш процесс думает, что он является пользователем root в системе, Linux знает, что root — в его случае — означает обычный UID 1000 (благодаря нашему маппингу). Так что в то время, когда пространства имён, принадлежащие его новому user namespace (подобно network namespace в C), признают его права в качестве root , другие (как например, network namespace в P) — нет. Поэтому процесс не может делать ничего, что пользователь 1000 не смог бы.
Всякий раз, когда процесс во вложенном user namespace выполняет операцию, требующую проверки разрешений — например, создание файла — его UID в этом user namespace сравнивается с эквивалентным ID пользователя в корневом user namespace путём обхода маппингов в дереве пространств имён до корня. В обратном направлении происходит движение, например, когда он читает ID пользователей, как мы это делаем с помощью ls -l my_file . UID владельца my_file маппится из корневого user namespace до текущего и окончательный соответствующий ID (или nobody, если маппинг отсутствовал где-либо вдоль всего дерева) отдаётся читающему процессу.
Групповые ID
Даже если мы оказались root в C, мы до сих пор ассоциированы с ужасной nogroup в качестве нашего ID группы. Нам просто нужно сделать то же самое для соответствующего /proc/$pid/gid_map . Прежде чем мы сможем это сделать, нам нужно отключить системный вызов setgroups (в этом нет необходимости, если у нашего пользователя уже есть CAP_SETGID capability в P, но мы не будем предполагать этого, поскольку это обычно идёт вместе с привилегиями суперпользователя), написав «deny» в файл proc/$pid/setgroups :
Реализация
Как вы можете видеть, есть много сложностей, связанных с управлением user namespaces, но реализация довольно проста. Всё, что нам нужно сделать, это написать кучу строк в файл — муторно было узнать, что и где писать. Без дальнейших церемоний, вот наши цели:
- Клонировать командного процесса в его собственном user namespace.
- Написать в UID и GID map-файлы командного процесса.
- Сбросить все привилегии суперпользователя перед выполнением команды.
1 достигается простым добавлением флага CLONE_NEWUSER в наш системный вызов clone .
Для 2 мы добавляем функцию prepare_user_ns , которая осторожно представляет одного обычного пользователя 1000 в качестве root .
И вызовем его из основного процесса в родительском user namespace прямо перед тем, как мы подадим сигнал командному процессу.
Для шага 3 мы обновляем функцию cmd_exec , чтобы убедиться, что команда выполняется от обычного непривилегированного пользователя 1000 , которого мы предоставили в маппинге (помните, что root пользователь 0 в user namespace командного процесса — это пользователь 1000 ):
И это всё! isolate теперь запускает процесс в изолированном user namespace.
В этом посте было довольно много подробностей о том, как работают User namespaces, но в конце концов настройка экземпляра была относительно безболезненной. В следующем посте мы рассмотрим возможность запуска команды в своём собственном Mount namespace с помощью isolate (раскрывая тайну, стоящую за инструкцией FROM из Dockerfile ). Там нам потребуется немного больше помочь Linux, чтобы правильно настроить инстанс.
Источник