What is: Linux namespaces, примеры PID и Network namespaces

By | 03/10/2018
 

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

см. Namespaces in operation.

Всякий, кто знаком с chroot в Linux уже имеет базовое представление о том, чего можно добиться с помощью Namespaces: аналогично тому, как chroot позволяет процессам видеть только определённую директорию как корень файловой системы – механизмы Linux namespaces позволяют выполнять операции изоляции в других механизмах операционной системы, такими как дерево процессов, сетевые интерфейсы, точки монтирования, IPC и так далее.

PID Namespace

Изначально Linux оддерживало только единое дерево процессов, в котором содержались ссылки на все запущенные в системе процессы в виде иерархического дерева. Любой процесс, при условии наличия соответствующих прав, мог подключиться к любому другому процессу и выполнить его трассировку или даже уничтожить его.

С появлением Linux Namespaces стало возможным иметь несколько “вложенных” друг в друга деревьев процессов: каждый процесс может иметь полностью изолированный набор процессов. С помощью этого достигается реальная изоляция процессов – процессы, принадлежащие к одному дереву процессов не могут подключиться или уничтожить процессы из дерева процессов другого, дочернего или родительского, процесса. По факту – эти процессы даже не имеют представления о том, что где-то есть другие такие процессы.

Каждый раз, когда компьютер с Linux запускается, он стартует с одного процесса с PID 1:

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)

Этот процесс, в данном примере 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;
}

Собираем, запускаем:

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

Кратко рассмотрим саму программу:

  • 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_namespaces
  • printf("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;
}
...

Собираем, проверяем:

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

Parent’s PID равен нулю, как и ожидали, т.к. child_function() запускается в собственном дереве и не видит родительского процесса вообще.

А теперь – изменим main() и вызов clone() в ней: уберём флаг CLONE_NEWPID:

...
pid_t child_pid = clone(child_function, child_stack + STACK_SIZE, SIGCHLD, NULL);
...

Пересобираем, запускаем:

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

Однако, у нас всё ещё нет полной изоляции: порожденный процесс до сих имеет доступ к другим общим ресусам системы.

Например, если бы дочерний процесс занимал порт 80 – это препятствовало бы любому другому процессу в системе использовать этот порт.

Network namespaces

Тут нам на помощь приходит Network namespaces, который позволяет каждому из создаваемых процессов видеть собственный набор сетевых интерфейсов.

Достигается это с помощью флага CLONE_NEWNET для функции clone().

Но прежде, чем продолжить с примерами на C – давайте попробуем испольтзовать iproute2 и создать NET namespaces вручную.

ip netns

Проверяем интерфейсы на хосте (в данном случае – это обычный ноутбук ThinkPad с Arch Linux):

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
...

Тут всё стандартно – loopback интефейс (lo0), ethernetenp0s25, WiFiwlp3s0 и пачка других.

Теперь – создадим новое пространство имён для сети:

sudo ip netns add demo

И с помощью ip netns exec вызовем ip link list из созданного namespace:

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

Внутри созданного пространства доступен только интерфейс lo, при чём он в статусе DOWN, а не Up, как lo на хост-системе.

А для того, что бы получить возможность обращаться к устройствам в созданном NET Namespace – надо создать Point-to-Point туннель между хостом и “гостевой” системой, которая работала бы в в новом пространстве.

Для этого в Linux можно использовать виртуальные интерфейсы (vethVirtual Ethernet), которые и используются в Docker для проброса трафика между хостом и создаваемыми контейнерами.

см. Docker Container Tutorial

veth создаёт два виртуальных интефейса – vent0 и veth1, которые затем связываются с пространством имён хост-системы и пространством имён в контейнере (которого у нас нет, но в Docker это происходит именно так).

Создаём пару veth:

sudo ip link add veth0 type veth peer name veth1

Проверяем:

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

Вот наши два туннеля – veth1 => veth0, и обратный – veth0 => veth1.

Подключаем интерфейс veth1 в пространство имён demo:

sudo ip link set veth1 netns demo

Повторяем list из пространства demo, как мы делали в начале:

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

8: veth1@if9 – вот и новый интерфейс в пространстве имён demo.

Теперь настроим этот интерфейс, как делали бы это на любом другом.

Активируем loopback:

sudo ip netns exec demo ip link set lo up

Активируем veth1:

sudo ip netns exec demo ip link set veth1 up

Присваиваем IP 169.254.1.2 из сети 169.254.1.0/30 к veth1:

sudo ip netns exec demo ip addr add 169.254.1.2/30 dev veth1

Поднимаем интерфейс veth0 на хосте:

sudo ip link set veth0 up

И присваем ему IP 169.254.1.1 из сети 169.254.1.0/30:

sudo ip addr add 169.254.1.1/30 dev veth0

Проверяем доступ между пространствами:

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

Таблица маршрутизации для 169.254.1.0/30:

ip route list 169.254.1.0/30
169.254.1.0/30 dev veth0 proto kernel scope link src 169.254.1.1

Удаляем пару veth, что бы продолжить с примером на С:

sudo ip link del veth0
ip link list | grep veth | wc -l

Создание 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;
}

Собираем, запускаем:

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

Всё отлично.

Обновим код, используем 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;
}

Собираем, запускаем:

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

Проверяем:

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

Ссылки на код – тут>>>.

Ссылки по теме

man7/namespaces

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

Network namespaces

Linux Network Namespace Introduction

Experiments with container networking

Containerization Mechanisms: Namespaces