Данный пост обединяет в себе два других замечательных (на мой взгляд) поста на тему 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) cgroupnamespace (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_namespacespid_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

