namespaces,часть 4

Перевод сделан автором сайта goxpert.ru

Краткое содежание перевода

  • В Linux существуют пространства имен для изоляции сетевых ресурсов.
  • Сетевое пространство имен изолирует сетевые ресурсы, создавая свои собственные сетевые устройства, таблицы маршрутизации и правила брандмауэра.
  • Команда ip в Linux является швейцарским армейским ножом для работы в сети.
  • Именованные сетевые пространства имен проще в использовании и могут существовать без участия процессов в качестве членов.
  • Устройства Veth используются для создания виртуальных сетевых устройств Ethernet, которые обеспечивают связь между пространствами имен.
  • Для взаимодействия с Linux используется интерфейс Netlink, который предоставляет API поверх сокетов для сетевой маршрутизации и управления устройствами

Глубокое погружение в пространства имен Linux, часть 4

Пространства имен монтирования(Mount namespaces) изолируют ресурсы файловой системы. Это в значительной степени охватывает все, что связано с файлами в системе. Среди инкапсулированных ресурсов есть файл, содержащий список точек монтирования, которые видны процессу, и, как мы намекали в вступительном посте, изоляция может обеспечить такое поведение, что изменение списка (или любого другого файла) в некотором экземпляре пространства имен монтирования M(mount namespace) не влияло на этот список в другом экземпляре (так что только процессы в M наблюдают изменения).

В этом заключительном посте серии мы рассмотрим сетевые пространства имен. Как мы намекали во вступительном посте, сетевое пространство имен изолирует связанные с сетью ресурсы - процесс, запущенный в отдельном сетевом пространстве имен, имеет свои собственные сетевые устройства, таблицы маршрутизации, правила брандмауэра и т.д. Мы можем сразу увидеть это в действии, изучив нашу текущую сетевую среду.

Команда ip

Поскольку в этом посте мы будем взаимодействовать с сетевыми устройствами, мы восстановим требования к суперпользователю, которые мы смягчили в предыдущих постах. С этого момента мы будем предполагать, что оба ip и isolate выполняются с sudo.

1
2
3
4
5
$ 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: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:96:2e:3b brd ff:ff:ff:ff:ff:ff

Звездой шоу здесь является ip command - швейцарский армейский нож для создания сетей в Linux - и мы будем широко использовать его в этом посте. Прямо сейчас мы только что запустили link list подкоманду, которая покажет нам, какие сетевые устройства в настоящее время доступны в системе (здесь у нас есть lo интерфейс обратной связи и ens33 интерфейс локальной сети ethernet).

Как и во всех других пространствах имен, система начинается с начального сетевого пространства имен, к которому принадлежат все процессы, если не указано иное. Запуск этой ip link list команды как есть дает нам сетевые устройства, принадлежащие исходному пространству имен (поскольку наша оболочка и ip команда принадлежат этому пространству имен).

Именованные сетевые пространства имен

Давайте создадим новое сетевое пространство имен:

1
2
3
$ ip netns add coke
$ ip netns list
coke

И снова мы использовали команду ip. Его netns подкоманда позволяет нам играть с сетевыми пространствами имен - например, мы можем создавать новые сетевые пространства имен с помощью add подкоманды netns и использовать list, чтобы, ну, составить их список.

Вы могли заметить, что list вернуло только наше недавно созданное пространство имен - разве оно не должно возвращать как минимум два, второе из которых является исходным пространством имен, о котором мы упоминали ранее? Причина этого в том, что ip создается так называемое именованное сетевое пространство имен, которое просто является сетевым пространством имен, идентифицируемым по уникальному имени (в нашем случае coke). Через list показаны только именованные сетевые пространства имен, а начальное сетевое пространство имен не названо.

Именованные сетевые пространства имен получить проще. Например, для каждого именованного сетевого пространства имен в /var/run/netns папке создается файл, который может использоваться процессом, который хочет переключиться на свое пространство имен. Еще одним свойством именованных сетевых пространств имен является то, что они могут существовать без участия какого-либо процесса в качестве члена - в отличие от безымянных, которые будут удалены после завершения работы всех входящих в них процессов.

Теперь, когда у нас есть дочернее сетевое пространство имен, мы можем посмотреть на сеть с его точки зрения.

  • Мы будем использовать командную строку C$ чтобы подчеркнуть оболочку, работающую внутри дочернего сетевого пространства имен.
1
2
3
4
$ ip netns exec coke bash
C$ 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

exec $namespace $command Подкоманда выполняется $command в именованном сетевом пространстве имен $namespace. Здесь мы запустили оболочку внутри coke пространства имен и перечислили доступные сетевые устройства. Мы видим, что, по крайней мере, наше ens33 устройство исчезло. Единственное устройство, которое отображается, - это loopback, и даже этот интерфейс не работает.

1
2
C$ ping 127.0.0.1
connect: Network is unreachable

Мы уже должны привыкнуть к этому, настройки по умолчанию для пространств имен обычно очень строгие. Как мы видим, в сетевых пространствах имен не будет никаких устройств, кроме loopback. Однако мы можем настроить loopback интерфейс без какой-либо бумажной волокиты:

1
2
3
4
5
C$ ip link set dev lo up
C$ ping 127.0.0.1
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.034 ms
...

Сетевая изоляция

Мы уже начинаем понимать, что, запуская процесс во вложенном сетевом пространстве имен, таком как coke, мы можем быть уверены, что он изолирован от остальной системы в том, что касается сетевого взаимодействия. Наш процесс оболочки, запущенный в coke может взаимодействовать только через loopback - это означает, что он может взаимодействовать только с процессами, которые также являются членами coke пространства имен, но в настоящее время других процессов-членов нет (и во имя изоляции мы хотели бы, чтобы так и оставалось), так что здесь немного одиноко. Давайте попробуем немного ослабить эту изоляцию, мы создадим туннель, через который процессы в coke могут взаимодействовать с процессами в нашем исходном пространстве имен.

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

Устройства Veth

Для удовлетворения наших потребностей мы будем использовать виртуальноевиртуальное(virtual) ethernet сетевое устройство (или, для краткости veth). Устройства Veth всегда создаются как пара устройств по типу туннеля, так что сообщения, записанные на устройство на одном конце, выходят из устройства на другом конце. Вы могли бы догадаться, что мы могли бы легко иметь один конец в исходном сетевом пространстве имен, а другой - в нашем дочернем сетевом пространстве имен, и вся связь между сетевыми пространствами имен осуществлялась бы через соответствующее конечное устройство veth (и вы были бы правы).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Create a veth pair (veth0 <=> veth1)
# Создание пары veth (veth0 <=> veth1)
$ ip link add veth0 type veth peer name veth1

# Move the veth1 end to the new namespace
# Перемещение veth1 в новое пространство имён
$ ip link set veth1 netns coke

# List the network devices from inside the new namespace
# Просмотр сетевых устройств в новом пространстве имён
C$ 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
7: veth1@if8: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether ee:16:0c:23:f3:af brd ff:ff:ff:ff:ff:ff link-netnsid 0

Наше veth1 устройство теперь отображается в coke пространстве имен. Но чтобы пара veth заработала, нам нужно предоставить им обоим IP-адреса и настроить интерфейсы. Мы сделаем это в соответствующем сетевом пространстве имен.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# In the initial namespace
# В исходном пространстве имён
$ ip addr add 10.1.1.1/24 dev veth0
$ ip link set dev veth0 up

# In the coke namespace
# В пространстве имён coke
C$ ip addr add 10.1.1.2/24 dev veth1
C$ ip link set dev veth1 up

C$ ip addr show veth1
7: veth1@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether ee:16:0c:23:f3:af brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.1.1.2/24 scope global veth1
valid_lft forever preferred_lft forever
inet6 fe80::ec16:cff:fe23:f3af/64 scope link
valid_lft forever preferred_lft forever

Мы должны увидеть, что veth1 установлено и имеет назначенный нам адрес 10.1.1.2 - то же самое должно произойти для veth0 в исходном пространстве имен. Теперь мы должны быть в состоянии выполнять пинг между пространствами имен между двумя процессами, запущенными в обоих пространствах имен.

1
2
3
4
5
6
7
8
$ ping -I veth0 10.1.1.2
PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.041 ms
...
C$ ping 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.067 ms
...

Реализация

Исходный код этого поста можно найти здесь.

Как обычно, сейчас мы попытаемся воспроизвести то, что видели до сих пор, в коде. В частности, нам нужно будет сделать следующее:

  1. Выполните команду в новом сетевом пространстве имен.
  2. Создайте пару veth (veth0 <=> veth1).
  3. Переместите устройство veth1 в новое пространство имен.
  4. Назначьте IP-адреса обоим устройствам и выведите их на экран.

Шаг 1 прост: мы создаем наш командный процесс в новом пространстве имен network, добавляя CLONE_NEWNET флаг в clone:

1
int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWNET;

На оставшихся этапах мы в первую очередь будем использовать интерфейс Netlink для взаимодействия с Linux. Netlink в основном используется для связи между обычными приложениями (например, isolate) и ядром Linux. Поверх сокетов предоставляется API, основанный на протоколе, который определяет структуру и содержимое сообщений. Используя этот протокол, мы можем отправлять сообщения, которые Linux получает и преобразует в запросы - например, создать пару veth с именами veth0 и veth1.

Давайте начнем с создания нашего сокета netlink. В нем мы указываем, что хотим использовать NETLINK_ROUTE протокол - этот протокол охватывает реализации сетевой маршрутизации и управления устройствами.

1
2
3
4
5
6
7
8
9
10
11
int create_socket(int domain, int type, int protocol)
{
int sock_fd = socket(domain, type, protocol);
if (sock_fd < 0)
die("cannot open socket: %m\n");

return sock_fd;
}

int sock_fd = create_socket(
PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);

Сообщение Netlink представляет собой выровненный по 4 байта блок данных, содержащий заголовок (struct nlmsghdr) и полезную нагрузку. Формат заголовка описан здесь. Модуль службы сетевого интерфейса (NIS) определяет формат (struct ifinfomsg), с которого должна начинаться полезная нагрузка, связанная с администрированием сетевого интерфейса.

Наш запрос будет представлен следующей C структурой:

1
2
3
4
5
6
7
#define MAX_PAYLOAD 1024

struct nl_req {
struct nlmsghdr n; // Netlink message header
struct ifinfomsg i; // Payload starting with NIS module info
char buf[MAX_PAYLOAD]; // Remaining payload
};

Модуль NIS требует, чтобы полезная нагрузка кодировалась как атрибуты Netlink. Атрибуты предоставляют способ сегментировать полезную нагрузку на подразделы. Атрибут имеет тип и длину в дополнение к полезной нагрузке, содержащей его фактические данные.

Полезная нагрузка сообщения Netlink будет закодирована в виде списка атрибутов (где любой такой атрибут, в свою очередь, может иметь вложенные атрибуты), и у нас будут некоторые вспомогательные функции для заполнения его атрибутами. В коде атрибут представлен rtattr структурой в linux/rtnetlink.h заголовочном файле в виде:

1
2
3
4
struct rtattr {
unsigned short rta_len;
unsigned short rta_type;
};

rta_len это длина полезной нагрузки атрибута, которая непосредственно следует за rt_attr структурой в памяти (т. е. за следующими rta_len байтами). То, как интерпретируется содержимое этой полезной нагрузки, определяется rta_type , а возможные значения полностью зависят от реализации получателя и отправляемого запроса.

В попытке собрать все это воедино, давайте посмотрим, как isolate выполняет запрос netlink для создания пары veth со следующей функцией, create_veth которая выполняет шаг2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// ip link add ifname type veth ifname name peername
void create_veth(int sock_fd, char *ifname, char *peername)
{
__u16 flags =
NLM_F_REQUEST // This is a request message - Это сообщение запроса
| NLM_F_CREATE // Create the device if it doesn't exist - cоздание устройства, если оно не существует
| NLM_F_EXCL // If it already exists, do nothing - Если оно уже существует, ничего не делать
| NLM_F_ACK; // Reply with an acknowledgement or error - Ответ с подтверждением или ошибкой

// Initialise request message.
// Инициализация сообщения запроса.
struct nl_req req = {
.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
.n.nlmsg_flags = flags,
.n.nlmsg_type = RTM_NEWLINK, // This is a netlink message - Это сообщение netlink
.i.ifi_family = PF_NETLINK,
};
struct nlmsghdr *n = &req.n;
int maxlen = sizeof(req);

/*
* Create an attribute r0 with the veth info. e.g if ifname is veth0
* then the following will be appended to the message
* Создание атрибута r0 с информацией о veth. Например, если ifname - veth0,
* тогда нижеследующее будет добавлено к сообщению
* {
* rta_type: IFLA_IFNAME
* rta_len: 5 (len(veth0) + 1)
* data: veth0\0
* }
*/
addattr_l(n, maxlen, IFLA_IFNAME, ifname, strlen(ifname) + 1);

// Add a nested attribute r1 within r0 containing iface info
// Добавление вложенного атрибута r1 в r0, содержащего информацию iface
struct rtattr *linfo =
addattr_nest(n, maxlen, IFLA_LINKINFO);
// Specify the device type is veth
// Указание типа устройства veth
addattr_l(&req.n, sizeof(req), IFLA_INFO_KIND, "veth", 5);

// Add another nested attribute r2
// Добавление еще одного вложенного атрибута r2
struct rtattr *linfodata =
addattr_nest(n, maxlen, IFLA_INFO_DATA);

// This next nested attribute r3 one contains the peer name e.g veth1
// Следующий вложенный атрибут r3 содержит имя соседнего устройства, например veth1
struct rtattr *peerinfo =
addattr_nest(n, maxlen, VETH_INFO_PEER);
n->nlmsg_len += sizeof(struct ifinfomsg);
addattr_l(n, maxlen, IFLA_IFNAME, peername, strlen(peername) + 1);
addattr_nest_end(n, peerinfo); // end r3 nest - конец вложенного атрибута r3

addattr_nest_end(n, linfodata); // end r2 nest - конец вложенного атрибута r2
addattr_nest_end(n, linfo); // end r1 nest - конец вложенного атрибута r1

// Send the message
// Отправка сообщения
send_nlmsg(sock_fd, n);
}

Как мы можем видеть, нам нужно быть точными в отношении того, что мы здесь отправляем - нам нужно было закодировать сообщение именно так, как оно будет интерпретировано реализацией ядра, и здесь нам потребовалось для этого 3 вложенных атрибута. Я уверен, что это где-то задокументировано, хотя я не смог найти это после некоторого поиска в Google - в основном я разобрался с этим через strace и исходный код ip команды.

Следующим шагом 3 является метод, который, учитывая имя интерфейса ifname и дескриптор файла сетевого пространства имен netns, перемещает устройство, связанное с этим интерфейсом, в указанное сетевое пространство имен.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// $ ip link set veth1 netns coke
void move_if_to_pid_netns(int sock_fd, char *ifname, int netns)
{
struct nl_req req = {
.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
.n.nlmsg_type = RTM_NEWLINK,
.i.ifi_family = PF_NETLINK,
};

addattr_l(&req.n, sizeof(req), IFLA_NET_NS_FD, &netns, 4);
addattr_l(&req.n, sizeof(req), IFLA_IFNAME,
ifname, strlen(ifname) + 1);
send_nlmsg(sock_fd, &req.n);
}

После создания пары veth и перемещения одного конца в наше целевое сетевое пространство имен, на шаге 4 мы назначаем IP-адреса обоих конечных устройств и запускаем их интерфейсы. Для этого у нас есть вспомогательная функция, if_up которая, получив имя интерфейса ifname и IP-адрес ip, присваивает ip устройству ifname и запускает его. Для краткости мы не показываем их здесь, но вместо этого их можно найти здесь.

Наконец, мы объединяем эти методы, чтобы подготовить наше сетевое пространство имен для нашего командного процесса.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static void prepare_netns(int child_pid)
{
char *veth = "veth0";
char *vpeer = "veth1";
char *veth_addr = "10.1.1.1";
char *vpeer_addr = "10.1.1.2";
char *netmask = "255.255.255.0";

// Create our netlink socket
// Создание нашего сокета netlink
int sock_fd = create_socket(
PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);

// ... and our veth pair veth0 <=> veth1.
// ... и нашей пары veth veth0 <=> veth1.
create_veth(sock_fd, veth, vpeer);

// veth0 is in our current (initial) namespace
// so we can bring it up immediately.
// veth0 находится в нашем текущем (исходном) namespace
// так что мы можем сразу поднять его.
if_up(veth, veth_addr, netmask);

// ... veth1 will be moved to the command namespace.
// To do that though we need to grab a file descriptor
// to and enter the commands namespace but first we must
// remember our current namespace so we can get back to it
// when we're done.
// ... veth1 будет перемещен в namespace команды.
// Для этого нам нужно получить файловый дескриптор
// и перейти в namespace команды, но сначала мы должны
// запомнить наш текущий namespace, чтобы мы могли вернуться в него
// когда закончим.
int mynetns = get_netns_fd(getpid());
int child_netns = get_netns_fd(child_pid);

// Move veth1 to the command network namespace.
// Перемещение veth1 в network namespace команды.
move_if_to_pid_netns(sock_fd, vpeer, child_netns);

// ... then enter it
// ... и переход туда
if (setns(child_netns, CLONE_NEWNET)) {
die("cannot setns for child at pid %d: %m\n", child_pid);
}

// ... and bring veth1 up
// ... и поднятие veth1-интерфейса
if_up(vpeer, vpeer_addr, netmask);

// ... before moving back to our initial network namespace.
// ... перед возвращением в наш исходный network namespace.
if (setns(mynetns, CLONE_NEWNET)) {
die("cannot restore previous netns: %m\n");
}

close(sock_fd);
}

Затем мы можем вызвать prepare_netns сразу после того, как закончим настройку пользовательского пространства имен.

1
2
3
4
5
6
7
8
9
10
11
...
// Get the writable end of the pipe.
// Получение записываемого конца пайпа.
int pipe = params.fd[1];

prepare_userns(cmd_pid);
prepare_netns(cmd_pid);

// Signal to the command process we're done with setup.
// Сигнал командному процессу, что мы закончили настройку.
...

Давайте попробуем!

1
2
3
4
5
6
7
8
9
10
11
$ sudo ./isolate sh
===========sh============
$ ip link list
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
31: veth1@if32: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP qlen 1000
link/ether 2a:e8:d9:df:b4:3d brd ff:ff:ff:ff:ff:ff
# Verify inter-namespace connectivity
$ ping 10.1.1.1
PING 10.1.1.1 (10.1.1.1): 56 data bytes
64 bytes from 10.1.1.1: seq=0 ttl=64 time=0.145 ms

Вот вам и namespaces

Поделиться