Структура сети в Docker (Часть 2.1)

Структура сети в Docker (Часть 2.1)

ЛЕТОПИСЕЦ КИТЕЖ-ГРАДА

Введение: Одним из основных направлений Docker является максимальное упрощение при использовании технологии контейнеризации. И хотя настройка сети обычно требует дополнительного внимания, Docker упрощает запуск контейнеров в сети общего назначения. В первую очередь Docker полагается на то, что узел может выполнять определенные функции для работы сети Docker. А именно, узел должен быть настроен для разрешения переадресации по протоколу IP. Кроме того, уже достаточно давно в Docker поддерживается технология преобразования сетевых адресов (NAT). Однако, перед тем как начать рассматривать сетевые компоненты самого Docker, давайте немного углубимся в то, как вообще организована сеть в контейнерах как такого.

Структура контейнерной сети: В предыдущих статьях я уже упоминал об одном из пространств имён, которое присутствует в Linux, а именно – Network Namespace или сетевое пространство имён. Стоит напомнить, что данный тип пространства представляет собой логическую копию сетевого стека со своими собственными маршрутами, правилами брандмауэра и сетевыми устройствами. Мы не будем затрагивать остальные пространства имён в Linux и ограничимся только областью видимости сетевого стека. Для создания соответствующего пространства имён достаточно использовать утилиту ip, которая присутствует чуть ли не в каждом дистрибутиве Linux. Чтобы организовать новое сетевое пространство, необходимо выполнить следующую команду:

sudo ip netns add netns1

Также можно выполнить следующую команду, чтобы убедиться в том, что нужное нам пространство имён было создано:

ip netns

Новое сетевое пространство имён было создано, но как теперь начать его использовать? Чтобы войти в ранее созданное пространство имён, можно использовать несколько разных команд, каждая из которых, так или иначе, приведет нас к желаемому результату. Это можно сделать следующим образом:

ip netns exec netns1 /bin/bash

Или, например, вот так:

nsenter --net=/var/run/netns/netns1 bash

Теперь выполним следующую команду:

ip address

В результате чего будет получен вот такой вывод:

1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

Приведённый выше пример демонстрирует, что процесс bash, работающий внутри пространства имён netns1, видит совершенно другой сетевой стек. Отсутствуют как правила маршрутизации, так и правила iptables, есть только один Loopback-интерфейс. Кстати говоря, наш Loopback-интерфейс хоть и присутствует, но на данный момент он неактивен. Если мы зайдем в пространство имён netns1 и попробуем пропинговать самих себя, то у нас ничего не выйдет:

ip netns exec netns1 /bin/bash
ping 127.0.0.1

ping: connect: Network is unreachable

Чтобы исправить данную проблему необходимо выполнить следующие несколько команд:

ip link set dev lo up
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
 inet 127.0.0.1/8 scope host lo
 valid_lft forever preferred_lft forever
 inet6 ::1/128 scope host 
 valid_lft forever preferred_lft forever

И теперь, если обратиться на самих себя с помощью утилиты ping, будет получен положительный результат:

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.019 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.027 ms
^C
Схема отражает суть, однако имена / значения не соответствуют содержимому в статье.

Loopback-интерфейс это хорошо, но выделенный сетевой стек будет бесполезен, если с ним невозможно взаимодействовать из вне. К счастью, ОС Linux предоставляет подходящее средство для этого – Virtual Ethernet Devices (veth). Согласно определению, veth-device – это виртуальное устройство Ethernet. Оно работает как туннель между сетевыми пространствами имён для создания моста к физическому сетевому устройству в другом пространстве имён. Также может быть использован как автономное сетевое устройство. Виртуальные устройства Ethernet всегда работают парами. Создадим их сейчас, только стоит отметить, что выполнятся данная команда должна в корневом пространстве имен, поэтому убедитесь, что вы находитесь в нём. Ориентироваться всегда можно по наличию тех или иных интерфейсов, список которых можно получить с помощью ip link. Поскольку сейчас я нахожусь в выделенном сетевом пространстве, сперва мне необходимо его покинуть, а уже после применить команду, которая представлена в примере ниже:

exit
ip link add veth0 type veth peer name veth1

Ниже представлен результат, полученный после выполнения команды:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   inet 127.0.0.1/8 scope host lo
      valid_lft forever preferred_lft forever
   inet6 ::1/128 scope host 
      valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
   link/ether 08:00:27:22:39:33 brd ff:ff:ff:ff:ff:ff
   inet 192.168.88.240/24 brd 192.168.88.255 scope global eth0
      valid_lft forever preferred_lft forever
   inet6 fe80::a00:27ff:fe22:3933/64 scope link 
      valid_lft forever preferred_lft forever
12: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
   link/ether b2:1b:d0:46:62:d7 brd ff:ff:ff:ff:ff:ff
13: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
   link/ether 86:05:77:52:21:e8 brd ff:ff:ff:ff:ff:ff

С помощью данной команды мы только что создали пару взаимосвязанных виртуальных устройств Ethernet. Имена были выбраны произвольно. И veth0, и veth1 после создания находятся в сетевом стеке вашего узла (также называемом Root Network Namespace). Чтобы связать корневое пространство имён с пространством имён netns1, нам нужно сохранить одно из устройств в корневом пространстве имён и переместить другое в netns1:

ip link set veth1 netns netns1
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   inet 127.0.0.1/8 scope host lo
      valid_lft forever preferred_lft forever
   inet6 ::1/128 scope host 
      valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
   link/ether 08:00:27:22:39:33 brd ff:ff:ff:ff:ff:ff
   inet 192.168.88.240/24 brd 192.168.88.255 scope global eth0
      valid_lft forever preferred_lft forever
   inet6 fe80::a00:27ff:fe22:3933/64 scope link 
      valid_lft forever preferred_lft forever
13: veth0@if12: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 22:55:4e:34:ce:ba brd ff:ff:ff:ff:ff:ff link-netns netns1

Мы также можем зайти в пространство имён netns1 и убедиться в том, что интерфейсы были связаны:

ip netns exec netns1 /bin/bash
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   inet 127.0.0.1/8 scope host lo
      valid_lft forever preferred_lft forever
   inet6 ::1/128 scope host 
      valid_lft forever preferred_lft forever
12: veth1@if13: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
   link/ether b2:1b:d0:46:62:d7 brd ff:ff:ff:ff:ff:ff link-netnsid 13

Далее вновь покинем сетевое пространство имён netns1, перейдя в корневое и выполним следующую череду команд:

exit
ip addr add 192.168.0.1/24 dev veth0
ip link set dev veth0 up
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   inet 127.0.0.1/8 scope host lo
      valid_lft forever preferred_lft forever
   inet6 ::1/128 scope host 
      valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
   link/ether 08:00:27:22:39:33 brd ff:ff:ff:ff:ff:ff
   inet 192.168.88.240/24 brd 192.168.88.255 scope global eth0
      valid_lft forever preferred_lft forever
   inet6 fe80::a00:27ff:fe22:3933/64 scope link 
      valid_lft forever preferred_lft forever
13: veth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
   link/ether 22:55:4e:34:ce:ba brd ff:ff:ff:ff:ff:ff link-netns netns1
   inet 192.168.0.1/24 scope global veth0
      valid_lft forever preferred_lft forever
   inet6 fe80::8405:77ff:fe52:21e8/64 scope link 
      valid_lft forever preferred_lft forever

Опять вернемся к созданному ранее пространству имён и выполним те же команды, только с зеркально противоположными значениями:

ip netns exec netns1 /bin/bash
ip addr add 192.168.0.2/24 dev veth1
ip link set dev veth1 up
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
   link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
   inet 127.0.0.1/8 scope host lo
      valid_lft forever preferred_lft forever
   inet6 ::1/128 scope host 
      valid_lft forever preferred_lft forever
12: mdd1@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
   link/ether b2:1b:d0:46:62:d7 brd ff:ff:ff:ff:ff:ff link-netnsid 0
   inet 192.168.0.2/24 scope global mdd1
      valid_lft forever preferred_lft forever
   inet6 fe80::b01b:d0ff:fe46:62d7/64 scope link 
      valid_lft forever preferred_lft forever

Адреса были назначены, поэтому теперь можно попробовать выполнить ping, чтобы убедиться в том, что связность между пространствами имён присутствует. Сначала попробуем выполним ping находясь в пространстве имён netns1:

ping 192.168.0.1
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.058 ms
64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=0.052 ms
64 bytes from 192.168.0.1: icmp_seq=3 ttl=64 time=0.053 ms
64 bytes from 192.168.0.1: icmp_seq=4 ttl=64 time=0.087 ms
^C
--- 192.168.0.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3066ms
rtt min/avg/max/mdev = 0.052/0.062/0.087/0.016 ms

Теперь выйдем из созданного пространства имён и выполним команду ping находясь в сетевом пространстве хоста:

exit
ping 192.168.0.2
PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=0.053 ms
64 bytes from 192.168.0.2: icmp_seq=2 ttl=64 time=0.047 ms
64 bytes from 192.168.0.2: icmp_seq=3 ttl=64 time=0.066 ms
64 bytes from 192.168.0.2: icmp_seq=4 ttl=64 time=0.046 ms
^C
--- 192.168.0.2 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3051ms
rtt min/avg/max/mdev = 0.046/0.053/0.066/0.008 ms
Схема отражает суть, однако имена / значения не соответствуют содержимому в статье.

NAT в контейнерной сети: Предыдущие примеры помогли нам настроить внутреннюю сеть, однако со стороны интерфейса созданного пространства имён мы по-прежнему не можем достучаться до DNS-сервера Google:

ip netns exec netns1 /bin/bash
ping 8.8.8.8

ping: connect: Network is unreachable

Более того, мы вообще никакую другую сеть пропинговать не можем, поскольку в созданном нами пространстве имён существует только один маршрут:

192.168.0.0/24 dev mdd1 proto kernel scope link src 192.168.0.2.

Всё остальное находится за пределами досягаемого. То бишь, нам нужно каким-то образом соединить сеть, определяемую нашей парой виртуальных интерфейсов, с той, доступ к которой осуществляется через наш реальный аппаратный сетевой интерфейс, либо путем настройки моста, либо с помощью NAT. В случае, если узел запущен в каком-нибудь облачном сервисе, я рекомендую использовать NAT, так как с мостом могут возникнуть проблемы. На примере NAT мы и разберем как реализовать связь с внешним миром. Итак, для начала нужно сообщить сетевому стеку внутри пространства имен о том, чтобы он маршрутизировал всё через машину на другом конце соединения, определяемом ее внутренним IP-адресом:

ip netns exec netns1 /bin/bash
ip route add default via 192.168.0.1
exit

Обратите внимание, что адрес, который мы указываем в команде ip route add по умолчанию, является адресом конца пары виртуальных интерфейсов, который в свою очередь находится за пределами пространства имен процесса. Это значит, что мы явным образом говорим, что эта другая машина является нашим маршрутизатором. Теперь сетевой стек внутри пространства имен знает, куда направлять данные, но, поскольку на другой стороне нет ничего, что можно было бы отправить дальше, наши пакеты отбрасываются. Нам нужно использовать iptables для настройки этой стороны вне пространства имен. Сначала нужно сообщить узлу, что он может маршрутизировать данные:

echo 1 > /proc/sys/net/ipv4/ip_forward

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

iptables -L FORWARD
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Хорошо, теперь нужно внести некоторые изменения в iptables NAT, чтобы у нас появилась маршрутизация. Посмотрим, что у нас есть на текущий момент:

iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target    prot opt source              destination        

Chain INPUT (policy ACCEPT)
target    prot opt source              destination        

Chain OUTPUT (policy ACCEPT)
target    prot opt source              destination        

Chain POSTROUTING (policy ACCEPT)
target    prot opt source              destination

Так, сперва включим MASQUERADE из сети 192.168.0.* на нашем основном Ethernet-интерфейсе eth0:

iptables -t nat -A POSTROUTING -s 192.168.0.0/255.255.255.0 -o eth0 -j MASQUERADE

Теперь сообщим о том, что мы будем пересылать данные, поступающие на eth0, которые могут быть перенаправлены на наш интерфейс veth0. А тот, как вы должны помнить, является концом пары виртуального соединения, находящегося вне пространства имен:

iptables -A FORWARD -i eth0 -o veth0 -j ACCEPT

Также не забываем про маршрутизацию в обратном направлении:

iptables -A FORWARD -o eth0 -i veth0 -j ACCEPT

И я напоминаю, что все команды по iptables должны быть выполнены в корневом пространстве имён. Теперь давайте посмотрим, что произойдет, если мы попытаемся пропинговать 8.8.8.8 изнутри пространства имён:

ip netns exec netns1 /bin/bash
ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=37.9 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=115 time=37.2 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=115 time=36.8 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=115 time=37.2 ms
64 bytes from 8.8.8.8: icmp_seq=5 ttl=115 time=37.3 ms
64 bytes from 8.8.8.8: icmp_seq=6 ttl=115 time=36.4 ms
^C
--- 8.8.8.8 ping statistics ---
6 packets transmitted, 6 received, 0% packet loss, time 5007ms
rtt min/avg/max/mdev = 36.449/37.184/37.978/0.504 ms

На текущий момент у нас есть сетевое пространство имен, в котором мы можем работать как сетевой клиент – процессы, запущенные внутри него, могут получить доступ к внешнему Миру. Однако, мы не можем запустить сервер внутри пространства имен и получить к нему доступ извне. Для этого нам нужно настроить переадресацию портов:

iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 6000 -j DNAT --to-destination 192.168.0.2:8080

Или, если говорить проще, когда что-то приходит на порт 6000, мы должны отправить это на порт 8080 на интерфейсе 192.168.0.2 (который является концом пары виртуальных интерфейсов, которая в свою очередь находится внутри пространства имен). Затем мы сообщаем, что мы хотим пересылать данные туда и обратно, через новые и установленные соединения с IP-адресом нашего интерфейса с пространством имен:

iptables -A FORWARD -p tcp -d 192.168.0.2 --dport 8080 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT

Bridge в контейнерной сети: Вернемся к мостам и рассмотрим Linux Bridge – еще один виртуализированный сетевой объект. Linux Bridge ведёт себя как коммутатор. Он пересылает пакеты между подключенными к нему интерфейсами. А поскольку это коммутатор, то он работает на уровне L2, то бишь использует Ethernet. Чтобы предыдущие этапы нашего эксперимента в дальнейшем не вносили путаницы, удалим существующие сетевые пространства имён и создадим их заново, основываясь на командах, представленных ранее:

ip netns delete netns1
ip link delete veth0

Новые два пространства имён со всеми вытекающими командами:

ip netns add netnsbr21
ip link add veth0 type veth peer name ceth0
ip link set veth0 up
ip link set ceth0 netns netnsbr21
nsenter --net=/var/run/netns/netnsbr21
ip link set lo up
ip link set ceth0 up
ip addr add 192.168.0.21/16 dev ceth0
exit
ip netns add netnsbr22
ip link add veth1 type veth peer name ceth1
ip link set veth1 up
ip link set ceth1 netns netnsbr22
nsenter --net=/var/run/netns/netnsbr22
ip link set lo up
ip link set ceth1 up
ip addr add 192.168.0.22/16 dev ceth1
exit

Убедимся, что на нашем узле нет новых маршрутов:

ip route
default via 192.168.88.1 dev eth0 proto dhcp 
192.168.88.0/24 dev eth0 proto kernel scope link src 192.168.88.240 

Теперь создадим bridge интерфейс:

ip link add br0 type bridge
ip link set br0 up

Подключим к нему veth0 и veth1:

ip link set veth0 master br0
ip link set veth1 master br0
Схема отражает суть, однако имена / значения не соответствуют содержимому в статье.

И проверим возможность подключения между пространствами:

nsenter --net=/var/run/netns/netnsbr21
ping 192.168.0.22
PING 192.168.0.22 (192.168.0.22) 56(84) bytes of data.
64 bytes from 192.168.0.22: icmp_seq=1 ttl=64 time=0.085 ms
64 bytes from 192.168.0.22: icmp_seq=2 ttl=64 time=0.105 ms
64 bytes from 192.168.0.22: icmp_seq=3 ttl=64 time=0.059 ms
64 bytes from 192.168.0.22: icmp_seq=4 ttl=64 time=0.062 ms
^C
--- 192.168.0.22 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3067ms
rtt min/avg/max/mdev = 0.059/0.077/0.105/0.021 ms
nsenter --net=/var/run/netns/netnsbr22
ping 192.168.0.21
PING 192.168.0.21 (192.168.0.21) 56(84) bytes of data.
64 bytes from 192.168.0.21: icmp_seq=1 ttl=64 time=0.060 ms
64 bytes from 192.168.0.21: icmp_seq=2 ttl=64 time=0.061 ms
64 bytes from 192.168.0.21: icmp_seq=3 ttl=64 time=0.077 ms
64 bytes from 192.168.0.21: icmp_seq=4 ttl=64 time=0.060 ms
^C
--- 192.168.0.21 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3075ms
rtt min/avg/max/mdev = 0.060/0.064/0.077/0.010 ms

Все работает отлично. При этом мы даже не настраивали интерфейсы veth0 и veth1. Мы назначили только два IP-адреса интерфейсам ceth0 и ceth1. Но так как они оба находятся в одном сегменте Ethernet (подключены к виртуальному коммутатору), существует возможность подключения на уровне L2. То бишь, сейчас контейнеры могут подключаться друг к другу. Но будут ли удачны подключения к узлу, то есть к корневому пространству имён? Нет. Интерфейс eth0 не доступен, потому что отсутствует маршрут для этого подключения. Корневое пространство имён также не может взаимодействовать с ранее созданными пространствами имён. Чтобы установить связь между корневым пространством имён и созданными пространствами имён, нужно назначить IP-адрес сетевому интерфейсу моста:

ip addr add 172.18.0.1/16 dev br0

После того как интерфейсу моста был назначен IP-адрес, мы получили маршрут в таблице маршрутизации узла:

ip route
default via 192.168.88.1 dev eth0 proto dhcp 
192.168.0.0/16 dev br0 proto kernel scope link src 192.168.0.1 
192.168.88.0/24 dev eth0 proto kernel scope link src 192.168.88.240

Соответственно, теперь команда ping по отношению ко внутренним адресам пространств имен может быть выполнена корректно:

PING 192.168.0.21 (192.168.0.21) 56(84) bytes of data.
64 bytes from 192.168.0.21: icmp_seq=1 ttl=64 time=0.074 ms
64 bytes from 192.168.0.21: icmp_seq=2 ttl=64 time=0.091 ms
64 bytes from 192.168.0.21: icmp_seq=3 ttl=64 time=0.050 ms
64 bytes from 192.168.0.21: icmp_seq=4 ttl=64 time=0.056 ms
^C
--- 192.168.0.21 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3076ms
rtt min/avg/max/mdev = 0.050/0.067/0.091/0.018 ms
---------------------------------------------------------------
PING 192.168.0.22 (192.168.0.22) 56(84) bytes of data.
64 bytes from 192.168.0.22: icmp_seq=1 ttl=64 time=0.073 ms
64 bytes from 192.168.0.22: icmp_seq=2 ttl=64 time=0.087 ms
64 bytes from 192.168.0.22: icmp_seq=3 ttl=64 time=0.057 ms
64 bytes from 192.168.0.22: icmp_seq=4 ttl=64 time=0.091 ms
^C
--- 192.168.0.22 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3083ms
rtt min/avg/max/mdev = 0.057/0.077/0.091/0.013 ms

Интерфейс нашего пространства имён также получил возможность пинговать интерфейс моста, но они все ещё не могут связаться с хостом eth0. Для этого нужно добавить маршрут по умолчанию:

nsenter --net=/var/run/netns/netnsbr21
ip route add default via 192.168.0.1
nsenter --net=/var/run/netns/netnsbr22
ip route add default via 192.168.0.1

Теперь наш хост является маршрутизатором, а интерфейс моста стал шлюзом по умолчанию для контейнеров:

Схема отражает суть, однако имена / значения не соответствуют содержимому в статье.

Нам удалось добиться сетевой связности с корневым пространством имён. Далее попробуем подключить их к внешнему миру. Вообще переадресация пакетов, то есть функциональность маршрутизатора в Linux на текущем этапе должна была быть отключена, однако, поскольку ранее мы уже настраивали её в сочетании с NAT, наши пространства имён уже имеют возможность выйти в открытый мир. В противном случае нужно было бы выполнить следующую череду команд:

echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A POSTROUTING -s 192.168.0.0/16 ! -o br0 -j MASQUERADE

Команда довольно проста. Мы добавляем новое правило в таблицу NAT цепочки POSTROUTING с просьбой выполнить MASQUERADE всех исходящих пакетов из сети 192.168.0.0/16, но не через интерфейс моста.

nsenter --net=/var/run/netns/netnsbr21
ping 8.8.8.8
nsenter --net=/var/run/netns/netnsbr21
ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=37.4 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=115 time=38.2 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=115 time=38.2 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=115 time=38.3 ms
^C
--- 8.8.8.8 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 37.445/38.056/38.322/0.428 ms

PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=37.8 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=115 time=38.2 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=115 time=37.4 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=115 time=37.6 ms
^C
--- 8.8.8.8 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3012ms
rtt min/avg/max/mdev = 37.406/37.784/38.208/0.378 ms

Общие сведения о сети Docker: В общем случае сеть Docker построена на базе Container Network Model, которая позволяет кому угодно создать свой собственный сетевой драйвер. Таким образом, у контейнеров есть доступ к разным типам сетей и они могут подключаться к нескольким сетям одновременно. Как видно на диаграмме ниже, CNM имеет интерфейсы для IPAM и сетевых плагинов. APIS подключаемого модуля IPAM может создавать / удалять пулы адресов, а также выделять / отменять IP-адреса контейнеров для добавления или их удаления из сети. API сетевых плагинов применяется для создания / удаления сетей и добавления / удаления контейнеров из сети:

На приведенной выше диаграмме изображены пять основных объектов CNM. Рассмотрим каждый из этих объектов:

Network Controller: Cетевой контроллер предоставляет простой APIS для Docker, который выделяет сети и управляет ими.
Driver: Драйвер отвечает за управление сетью. Может быть несколько драйверов, которые участвуют в сети для выполнения различных сценариев развертывания и использования.
Network: Сеть обеспечивает связь между конечными точками одной сети и изолирует их от остальных. Соответствующий драйвер уведомляется всякий раз, когда сеть обновляется или создается заново.
Endpoint: Конечная точка обеспечивает подключение для служб, предоставляемых одним контейнером сети, с другими службами, предоставляемыми другими контейнерами. Конечная точка имеет глобальную область воздействия и является службой, а не контейнером.
Sandbox: Песочница создается, когда пользователь отправляет запрос на создание конечной точки в сети. Песочница может иметь более одной конечной точки, подключенной к разным сетям.

Помимо различных сторонних сетевых драйверов, непосредственно у самого Docker присутствует четыре встроенных драйвера. Вот они:

Bridge: С точки зрения сети Bridge представляет собой устройство канального уровня, которое перенаправляет трафик между сегментами сети. Мост может выступать как в роли аппаратного устройства, так и в роли программного устройства, работающего в ядре узла. С точки зрения Docker, Bridge использует программный мост, который позволяет контейнерам, подключенным к одной сети, взаимодействовать, обеспечивая при этом изоляцию от контейнеров, которые не подключены к этой мостовой сети. Драйвер Bridge в Docker автоматически устанавливает правила на реальном узле, чтобы контейнеры в разных сетях не могли напрямую взаимодействовать друг с другом. Сеть Bridge применяется к контейнерам, работающим на одном узле демона Docker. Для связи между контейнерами, работающими на разных узлах демона Docker, вы можете управлять маршрутизацией на уровне ОС или использовать оверлейную сеть. Когда вы запускаете Docker, сеть Bridge является сетью по умолчанию и создается автоматически, а только что запущенные контейнеры подключаются к ней, если сеть не была указана явным образом.
Host: Если вы используете сетевой режим хоста для контейнера, сетевой стек этого контейнера не будет изолирован от самого хоста (контейнер разделяет сетевое пространство имен узла), и контейнеру не выделяется собственный IP-адрес. Например, если вы запускаете контейнер, который привязывается к порту 80, и используете сеть хоста, приложение контейнера будет доступно на порту 80 по IP-адресу хоста. Сеть в режиме хоста может быть полезна для оптимизации производительности, а также в ситуациях, когда контейнеру необходимо обрабатывать большой диапазон портов, поскольку он не требует преобразования сетевых адресов (NAT). Сетевой драйвер хоста работает только на узлах Linux и не поддерживается в Docker Desktop для Mac/Windows или Docker EE для Windows Server.
Overlay: Драйвер оверлейной сети создает распределенную сеть между несколькими узлами демона Docker. Эта сеть находится поверх сети, специфичной для узла, позволяя контейнерам, подключенным к ней, безопасно обмениваться данными при включенном шифровании. Docker прозрачно обрабатывает маршрутизацию каждого пакета от и к правильному узлу демона Docker, а также правильному контейнеру назначения. У контейнеров есть свои адреса сети и подсети, и они могут напрямую обмениваться данными, даже если они располагаются физически на разных узлах:
Macvlan: Некоторые приложения нуждаются в прямом подключении к физической сети. В такой ситуации вы можете использовать сетевой драйвер macvlan для назначения MAC-адреса виртуальному сетевому интерфейсу каждого контейнера, чтобы он выглядел как физический сетевой интерфейс, напрямую подключенный к физической сети. В этом случае вам необходимо назначить физический интерфейс на вашем узле Docker для использования macvlan, а также подсеть и шлюз macvlan. Вы даже можете изолировать свою сеть macvlan, используя разные физические сетевые интерфейсы. Использование macvlan драйвера иногда является лучшим выбором при работе с устаревшими приложениями, которые ожидают, что они будут напрямую подключены к физической сети:

Послесловие: Рассмотренный в этой статье подход к организации сети контейнеров является лишь одним из возможных (пожалуй, что наиболее широко используемым). Есть еще много других способов, реализованных через официальные или сторонние плагины, но все они сильно зависят от средств виртуализации сети Linux. Таким образом, контейнеризацию по праву можно рассматривать как технологию виртуализации. Уже в следующей подчасти речь пойдет непосредственно о практической составляющей сети в рамках Docker.

Report Page