Данный пост обединяет в себе два других замечательных (на мой взгляд) поста на тему Namespaces в Linux – A Tutorial for Isolating Your System with Linux Namespaces и Introduction to Linux namespaces – Part 1: UTS, с небольшими дополнениями и изменениями. Тем не менее – крайне рекомендую к прочтению оба поста выше, и ознакомиться с другими ссылками из списка Ссылки по теме.
Содержание
Введение
С появлением таких утилит, как Docker и Linux контейнеры стало возможным очень легко изолировать процессы в Linux системе в их собственное системное окружение. Это дало возможность запускать целый набор приложений на одной Linux машине и быть уверенным в том, что они не будут мешать друг другу, без необходимости использования зоопарка виртуальных машин.
Ключевая функциональность ядра, которая позволяет добиться такой изоляции – Linux Namespaces, появилась в Linux начиная с версии 2.4.19 в 2002 году (CLONE_NEWNS
), после чего с последующими обновлениями ядра добавлялись новые:
- UTS namespaces (
CLONE_NEWUTS
, Linux 2.6.19) - IPC namespaces (
CLONE_NEWIPC
, Linux 2.6.19) - PID namespaces (
CLONE_NEWPID
, Linux 2.6.24) - Network namespaces (
CLONE_NEWNET
, добавлен в разработку в Linux 2.4.19, 2.6.24 и завершён в Linux 2.6.29) - User namespaces (
CLONE_NEWUSER
, добавлен в разработку в Linux 2.6.23 и завершён в Linux 3.8) cgroup
namespace (CLONE_NEWCGROUP
, Linux 4.0)
Всякий, кто знаком с chroot
в Linux уже имеет базовое представление о том, чего можно добиться с помощью Namespaces: аналогично тому, как chroot
позволяет процессам видеть только определённую директорию как корень файловой системы – механизмы Linux namespaces позволяют выполнять операции изоляции в других механизмах операционной системы, такими как дерево процессов, сетевые интерфейсы, точки монтирования, IPC и так далее.
PID Namespace
Изначально Linux оддерживало только единое дерево процессов, в котором содержались ссылки на все запущенные в системе процессы в виде иерархического дерева. Любой процесс, при условии наличия соответствующих прав, мог подключиться к любому другому процессу и выполнить его трассировку или даже уничтожить его.
С появлением Linux Namespaces стало возможным иметь несколько “вложенных” друг в друга деревьев процессов: каждый процесс может иметь полностью изолированный набор процессов. С помощью этого достигается реальная изоляция процессов – процессы, принадлежащие к одному дереву процессов не могут подключиться или уничтожить процессы из дерева процессов другого, дочернего или родительского, процесса. По факту – эти процессы даже не имеют представления о том, что где-то есть другие такие процессы.
Каждый раз, когда компьютер с Linux запускается, он стартует с одного процесса с PID 1:
[simterm]
$ sudo pstree -pn | head systemd(1)-+-systemd-journal(246) |-lvmetad(258) |-systemd-udevd(265) |-systemd-timesyn(557)---{systemd-timesyn}(558) |-crond(562) |-dbus-daemon(563) |-systemd-logind(565) |-wpa_supplicant(566) |-NetworkManager(567)-+-{NetworkManager}(582) | `-{NetworkManager}(586)
[/simterm]
Этот процесс, в данном примере systemd(1)
, является корнем дерева процессов, и он инициализирует всю систему, запуская все остальные необходимые для её работы процессы, и все такие процессы запускаются в этом дереве ниже первого процесса.
Process Namespace позволяет создать новое дерево, с его собственным процессом с PID 1 – процесс, который создал такое дерево, остаётся в родительском пространстве имён, в основном дереве процессов, но его дочерний процесс становится корневым процессом собственного дерева процесса.
Таким образом, с помощью изоляции PID namespace, процессы в дочернем дереве процесса не имеют никакой возможности узнать о существовании своего родительского процесса, однако процессы в родительском пространстве имён имеют полное представление о дочернем пространстве, как если бы те были обычными процессами в родительском пространстве имён:
Кроме того, таким же образом возможно создавать вложенные простанства имён: один процесс порождает другой, дочерний, в новом PID namespace, а этот дочерний процесс порождает следующий, в ещё одном пространстве PID, и так далее.
С появлением PID namespaces один процесс может иметь несколько PID, по одному на каждый связанное с этим процессом пространство имён.
В исходном коде Linux, в файле pid.h
, мы можем увидеть, что структура pid
, которая ранее использовалась для отслеживания задач по одному PID, теперь использует структуру upid
для отслеживания задач по нескольким PID:
... /* * struct upid is used to get the id of the struct pid, as it is * seen in particular namespace. Later the struct pid is found with * find_pid_ns() using the int nr and struct pid_namespace *ns. */ struct upid { /* Try to keep pid_chain in the same cacheline as nr for find_vpid */ int nr; struct pid_namespace *ns; struct hlist_node pid_chain; }; struct pid { atomic_t count; unsigned int level; /* lists of tasks that use this pid */ struct hlist_head tasks[PIDTYPE_MAX]; struct rcu_head rcu; struct upid numbers[1]; }; ...
см. PID namespaces in the 2.6.24 kernel.
Создание PID namespace: clone()
и CLONE_NEWPID
Что бы создать новое пространство имён для PID – используем системный вызов clone()
(другой вариант – использовать fork()
и unshare()
), которому аргументом передадим флаг CLONE_NEWPID
.
После вызова clone()
– новый процесс будет создан в новом пространстве имён PID, с новым деревом процессов.
Используем следующй код:
#define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> #define STACK_SIZE (1024 * 1024) static char child_stack[STACK_SIZE]; static int child_function() { printf("Child's PID from child_function() from own namespace: %d\n", getpid()); return 0; } int main(int argc, char** argv) { pid_t pid = getpid(); pid_t child_pid = clone(child_function, child_stack + STACK_SIZE, CLONE_NEWPID | SIGCHLD, NULL); printf("PID for %s process = %d\n", argv[0], pid); printf("Child process created by clone() from %s have PID = %d\n", argv[0], child_pid); waitpid(child_pid, NULL, 0); return 0; }
Собираем, запускаем:
[simterm]
$ gcc pid_namespaces.c -opid_namespaces $ sudo ./pid_namespaces PID for ./pid_namespaces process = 29595 Child process created by clone() from ./pid_namespaces have PID = 29596 Child's PID from child_function() from own namespace: 1
[/simterm]
Кратко рассмотрим саму программу:
static char child_stack[STACK_SIZE]
: определяем размер стека для дочернего процессаpid_t pid = getpid()
: определяем переменную pid типаpid_t
, в которую заносим PID процесса, который выполняет саму программуpid_namespaces
pid_t child_pid = clone(...)
: определяем переменную child_pid типаpid_t
, которая получит значение PID из дочернего процесса, который создаётся вызовомclone()
clone(child_function, child_stack + STACK_SIZE, CLONE_NEWPID | SIGCHLD, NULL)
: выполянем вызовclone()
, которому передаём:child_function
: имя функции, которая будет выполнена дочерним процессом в новом namespace и выведет свой PID с помощьюgetpid()
child_stack
: расположение стека для дочернего процессаCLONE_NEWPID
: собственно флаг для использования нового namespace при создании дочернего процесса
printf("PID for %s process = %d\n", argv[0], pid)
: выводим имя программы pid_namespaces (argv[0]
) и PID процесса программы pid_namespacesprintf("Child process created by clone() from %s have PID = %d\n", argv[0], child_pid)
: выводим имя программы pid_namespaces, из которой вызываетсяclone()
и PID дочернего процесса, созданного вызовомclone()
Итак – функция clone()
порождает новый процесс, клонируя родительский, и начинает выполнение дочернего процесса с вызова функции child_function()
.
Однако, т.к. мы передали флаг CLONE_NEWPID
– новый процесс запускается в собственном пространстве имён и создаёт новое дерево процессов, поэтому getpid()
из child_function()
возвращает нам Process ID равным единице, т.е. самый первый процесс в этом новом дереве.
Проверяем.
Изменим функцию child_function()
, и к вызову getpid()
(получить свой PID) – добавим вызов функции getppid()
(Get Parent PID), в документации к которой сказано:
The getppid() function shall always be successful and no return value is reserved to indicate an error.
ОК:
... static int child_function() { printf("Child's PID from child_function() from own namespace: %d\n", getpid()); printf("Parent's PID from child_function() from own namespace: %d\n", getppid()); return 0; } ...
Собираем, проверяем:
[simterm]
$ gcc pid_namespaces.c -o pid_namespaces $ sudo ./pid_namespaces PID for ./pid_namespaces process = 10726 Child process created by clone() from ./pid_namespaces have PID = 10727 Child's PID from child_function() from own namespace: 1 Parent's PID from child_function() from own namespace: 0
[/simterm]
Parent’s PID равен нулю, как и ожидали, т.к. child_function()
запускается в собственном дереве и не видит родительского процесса вообще.
А теперь – изменим main()
и вызов clone()
в ней: уберём флаг CLONE_NEWPID
:
... pid_t child_pid = clone(child_function, child_stack + STACK_SIZE, SIGCHLD, NULL); ...
Пересобираем, запускаем:
[simterm]
$ sudo ./pid_namespaces Parent's PID for ./pid_namespaces process = 14186 Child process created by clone() from ./pid_namespaces have PID = 14187 Child's PID from child_function() from own namespace: 14187 Parent's PID from child_function() from own namespace: 14186
[/simterm]
Однако, у нас всё ещё нет полной изоляции: порожденный процесс до сих имеет доступ к другим общим ресусам системы.
Например, если бы дочерний процесс занимал порт 80 – это препятствовало бы любому другому процессу в системе использовать этот порт.
Network namespaces
Тут нам на помощь приходит Network namespaces, который позволяет каждому из создаваемых процессов видеть собственный набор сетевых интерфейсов.
Достигается это с помощью флага CLONE_NEWNET
для функции clone()
.
Но прежде, чем продолжить с примерами на C – давайте попробуем испольтзовать iproute2
и создать NET namespaces вручную.
ip netns
Проверяем интерфейсы на хосте (в данном случае – это обычный ноутбук ThinkPad с Arch Linux):
[simterm]
13:36:47 [setevoy@setevoy-arch-work ~] $ ip link list 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 3c:97:0e:a8:d8:31 brd ff:ff:ff:ff:ff:ff 3: wlp3s0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether f6:6d:9e:e1:9b:6a brd ff:ff:ff:ff:ff:ff ...
[/simterm]
Тут всё стандартно – loopback интефейс (lo0
), ethernet – enp0s25
, WiFi – wlp3s0
и пачка других.
Теперь – создадим новое пространство имён для сети:
[simterm]
$ sudo ip netns add demo
[/simterm]
И с помощью ip netns exec
вызовем ip link list
из созданного namespace:
[simterm]
$ sudo ip netns exec demo 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
[/simterm]
Внутри созданного пространства доступен только интерфейс lo, при чём он в статусе DOWN, а не Up, как lo на хост-системе.
А для того, что бы получить возможность обращаться к устройствам в созданном NET Namespace – надо создать Point-to-Point туннель между хостом и “гостевой” системой, которая работала бы в в новом пространстве.
Для этого в Linux можно использовать виртуальные интерфейсы (veth
– Virtual Ethernet), которые и используются в Docker для проброса трафика между хостом и создаваемыми контейнерами.
veth
создаёт два виртуальных интефейса – vent0
и veth1
, которые затем связываются с пространством имён хост-системы и пространством имён в контейнере (которого у нас нет, но в Docker это происходит именно так).
Создаём пару veth
:
[simterm]
$ sudo ip link add veth0 type veth peer name veth1
[/simterm]
Проверяем:
[simterm]
$ ip link list | grep veth 8: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 9: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
[/simterm]
Вот наши два туннеля – veth1 => veth0
, и обратный – veth0 => veth1
.
Подключаем интерфейс veth1
в пространство имён demo:
[simterm]
$ sudo ip link set veth1 netns demo
[/simterm]
Повторяем list
из пространства demo, как мы делали в начале:
[simterm]
$ history | grep "link list" 576 ip link list 579 sudo ip netns exec demo ip link list $ !579 sudo ip netns exec demo 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 8: veth1@if9: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 82:e8:df:df:11:e2 brd ff:ff:ff:ff:ff:ff link-netnsid 0
[/simterm]
8: veth1@if9
– вот и новый интерфейс в пространстве имён demo.
Теперь настроим этот интерфейс, как делали бы это на любом другом.
Активируем loopback:
[simterm]
$ sudo ip netns exec demo ip link set lo up
[/simterm]
Активируем veth1
:
[simterm]
$ sudo ip netns exec demo ip link set veth1 up
[/simterm]
Присваиваем IP 169.254.1.2 из сети 169.254.1.0/30 к veth1
:
[simterm]
$ sudo ip netns exec demo ip addr add 169.254.1.2/30 dev veth1
[/simterm]
Поднимаем интерфейс veth0
на хосте:
[simterm]
$ sudo ip link set veth0 up
[/simterm]
И присваем ему IP 169.254.1.1 из сети 169.254.1.0/30:
[simterm]
$ sudo ip addr add 169.254.1.1/30 dev veth0
[/simterm]
Проверяем доступ между пространствами:
[simterm]
$ ping 169.254.1.2 -c 1 PING 169.254.1.2 (169.254.1.2) 56(84) bytes of data. 64 bytes from 169.254.1.2: icmp_seq=1 ttl=64 time=0.077 ms --- 169.254.1.2 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.077/0.077/0.077/0.000 ms
[/simterm]
Таблица маршрутизации для 169.254.1.0/30:
[simterm]
$ ip route list 169.254.1.0/30 169.254.1.0/30 dev veth0 proto kernel scope link src 169.254.1.1
[/simterm]
Удаляем пару veth
, что бы продолжить с примером на С:
[simterm]
$ sudo ip link del veth0 $ ip link list | grep veth | wc -l 0
[/simterm]
Создание NET Namespace: clone()
и CLONE_NEWNET
Теперь повторим всё то же, но из C программы, используя clone()
, как в случае с PID namespace в начале поста, только с флагом CLONE_NEWNET
вместо CLONE_NEWPID
:
#define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> #define STACK_SIZE (1024 * 1024) static char child_stack[STACK_SIZE]; static int child_function() { printf("New NET namespace:\n\n"); system("ip link list"); printf("\n"); return 0; } int main() { printf("\nOriginal NET namespace:\n\n"); system("ip link list"); printf("\n"); pid_t child_pid = clone(child_function, child_stack + STACK_SIZE, CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, NULL); waitpid(child_pid, NULL, 0); return 0; }
Собираем, запускаем:
[simterm]
$ gcc net_namespaces.c -o net_namespaces $ sudo ./net_namespaces Original NET namespace: 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 3c:97:0e:a8:d8:31 brd ff:ff:ff:ff:ff:ff 3: wlp3s0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether f6:6d:9e:e1:9b:6a brd ff:ff:ff:ff:ff:ff 4: wwp0s20u4i6: <BROADCAST,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 0a:f4:53:ed:bb:89 brd ff:ff:ff:ff:ff:ff 5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default link/ether 02:42:65:d4:f1:a3 brd ff:ff:ff:ff:ff:ff 6: br-873fdc3bc652: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default link/ether 02:42:4f:36:54:44 brd ff:ff:ff:ff:ff:ff 7: br-9bcb68cdf3ad: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default link/ether 02:42:5b:75:bb:9a brd ff:ff:ff:ff:ff:ff New NET namespace: 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
[/simterm]
Всё отлично.
Обновим код, используем system()
для вызова команд и настройки сети:
#define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> #define STACK_SIZE (1024 * 1024) static char child_stack[STACK_SIZE]; // sync primitive int checkpoint[2]; static int child_function() { char c; // init sync primitive close(checkpoint[1]); // wait for network setup in parent read(checkpoint[0], &c, 1); // setup network system("ip link set lo up"); system("ip link set veth1 up"); system("ip addr add 169.254.1.2/30 dev veth1"); printf("New NET namespace:\n\n"); system("ip link list"); printf("\n"); // stay it UP to run ping sleep(500); return 0; } int main() { // init sync primitive pipe(checkpoint); pid_t child_pid = clone(child_function, child_stack + STACK_SIZE, CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, NULL); // further init: create a veth pair char* cmd; asprintf(&cmd, "ip link set veth1 netns %d", child_pid); system("ip link add veth0 type veth peer name veth1"); system(cmd); system("ip link set veth0 up"); system("ip addr add 169.254.1.1/30 dev veth0"); free(cmd); // signal "done" close(checkpoint[1]); printf("\nOriginal NET namespace:\n\n"); system("ip link list"); printf("\n"); waitpid(child_pid, NULL, 0); return 0; }
Собираем, запускаем:
[simterm]
$ sudo ./net_namespaces Original NET namespace: 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: enp0s25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 3c:97:0e:a8:d8:31 brd ff:ff:ff:ff:ff:ff 3: wlp3s0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether f6:6d:9e:e1:9b:6a brd ff:ff:ff:ff:ff:ff 4: wwp0s20u4i6: <BROADCAST,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 0a:f4:53:ed:bb:89 brd ff:ff:ff:ff:ff:ff 5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default link/ether 02:42:65:d4:f1:a3 brd ff:ff:ff:ff:ff:ff 6: br-873fdc3bc652: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default link/ether 02:42:4f:36:54:44 brd ff:ff:ff:ff:ff:ff 7: br-9bcb68cdf3ad: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default link/ether 02:42:5b:75:bb:9a brd ff:ff:ff:ff:ff:ff 19: veth0@if18: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN mode DEFAULT group default qlen 1000 link/ether 6e:69:05:c3:d0:81 brd ff:ff:ff:ff:ff:ff link-netnsid 1 New NET namespace: 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 18: veth1@if19: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 link/ether 52:2d:1a:9a:1a:63 brd ff:ff:ff:ff:ff:ff link-netnsid 0
[/simterm]
Проверяем:
[simterm]
$ ip route list 169.254.1.0/30 169.254.1.0/30 dev veth0 proto kernel scope link src 169.254.1.1 $ ping 169.254.1.2 -c 1 PING 169.254.1.2 (169.254.1.2) 56(84) bytes of data. 64 bytes from 169.254.1.2: icmp_seq=1 ttl=64 time=0.134 ms --- 169.254.1.2 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.134/0.134/0.134/0.000 ms
[/simterm]
Ссылки на код – тут>>>.
Ссылки по теме
Introducing Linux Network Namespaces
Introduction to Linux namespaces – Part 1: UTS
A Tutorial for Isolating Your System with Linux Namespaces
Container Namespaces – Deep Dive into Container Networking
Linux Network Namespace Introduction
Experiments with container networking
Containerization Mechanisms: Namespaces