Все, кто работал с Docker-контейнерами, использовали Docker Network, особенно драйвер «Bridge». Просто запустив команду здесь и еще одну там, мы получим список контейнеров, работающих «внутри сети». Это так просто, что заставляет нас чувствовать, что это «волшебство». Но задумывались ли вы когда-нибудь, как это работает?

Я задаюсь тем же вопросом, когда пытался использовать Docker для имитации сетей. Итак, я попал в приключение понимания «волшебства» этого. В этом посте я расскажу вам, как работает эта «магия» и какие технологии используются для ее достижения.

О хозяине

Мой хост — это MacBook с macOS Catalina и Docker 19.03.8. установлен. Команды, используемые в этом посте, не сильно изменятся, если хост использует Linux. Docker в macOS работает на виртуальной машине, нам потребуются дополнительные шаги для доступа к нему, но для машин Linux нет необходимости выполнять эти команды.

Взгляд внутрь черного ящика.

Как я уже упоминал, Docker в macOS находится на виртуальной машине, но, к счастью, есть способ получить к нему доступ.

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

screen ~/Library/Containers/com.docker.docker/Data/vms/0/tty

Оказавшись внутри, мы запустим следующую команду:

docker-desktop:~# ip link show

Эта команда отобразит список интерфейсов, используемых виртуальной машиной Docker. Если на момент запуска команды у нас не запущена ни одна сеть Docker, она должна отображать как минимум 5 интерфейсов:

docker-desktop:~# ip link show
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: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 02:50:00:00:00:01 brd ff:ff:ff:ff:ff:ff
3: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
4: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/tunnel6 :: brd ::
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether 02:42:e9:e4:14:60 brd ff:ff:ff:ff:ff:ff

Отображаемые интерфейсы: eth0 (интерфейс Ethernet), lo (интерфейс обратной связи), tunl0 (туннельный интерфейс), ip6tnl (туннельный интерфейс IPv6) и docker0. Все эти интерфейсы являются общими, кроме docker0.

Чтобы узнать больше об интерфейсе docker0, мы можем запустить следующую команду:

docker-desktop:~# ip -d link show docker0

Это отобразит:

docker-desktop:~# ip -d link show docker0
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether 02:42:e9:e4:14:60 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535
bridge forward_delay 1500 hello_time 200 max_age 2000 ageing_time 30000 stp_state 0 priority 32768 vlan_filtering 0 vlan_protocol 802.1Q bridge_id 8000.02:42:E9:E4:14:60 designated_root 8000.02:42:E9:E4:14:60 root_port 0 root_path_cost 0 topology_change 0 topology_change_detected 0 hello_timer    0.00 tcn_timer    0.00 topology_change_timer    0.00 gc_timer  295.94 vlan_default_pvid 1 vlan_stats_enabled 0 group_fwd_mask 0 group_address 01:80:c2:00:00:00 mcast_snooping 1 mcast_router 1 mcast_query_use_ifaddr 0 mcast_querier 0 mcast_hash_elasticity 4 mcast_hash_max 512 mcast_last_member_count 2 mcast_startup_query_count 2 mcast_last_member_interval 100 mcast_membership_interval 26000 mcast_querier_interval 25500 mcast_query_interval 12500 mcast_query_response_interval 1000 mcast_startup_query_interval 3125 mcast_stats_enabled 0 mcast_igmp_version 2 mcast_mld_version 1 nf_call_iptables 0 nf_call_ip6tables 0 nf_call_arptables 0 addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

Если мы посмотрим на отображаемый текст, то увидим bridge forward_delay

Но что означает слово мост на интерфейсе?

Мост — это своего рода интерфейс, который ведет себя как переключатель. Но как устройства подключаются к этому коммутатору? Я объясню это с помощью следующей схемы:

На схеме br0 — это интерфейс моста, через который подключены виртуальные машины (nets1 и nets2). Виртуальные машины подключаются к мосту с помощью другого специального интерфейса с именем VETH (виртуальный Ethernet).

Интерфейс VETH ведет себя как «кабель Ethernet», и подключаемые устройства должны быть парами (то же самое и с настоящим кабелем Ethernet с двумя разъемами). Обычно это соединяет интерфейсы между ними. Например, eth0 виртуальных машин может быть подключен к интерфейсам eth моста.

Чтобы убедиться, что docker0 является мостовым интерфейсом, мы запускаем следующую команду:

docker-desktop:~# ip link show type bridge

Это отобразит интерфейсы моста:

docker-desktop:~# ip link show type bridge
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether 02:42:e9:e4:14:60 brd ff:ff:ff:ff:ff:ff

Как мы видим, он показывает только файл docker0. Но зачем нам интерфейс docker0, если мы не создали сеть докеров? Это связано с тем, что интерфейс docker0 создается Docker по умолчанию в момент создания контейнера, этот контейнер будет добавлен в эту сеть по умолчанию.

Далее мы рассмотрим docker0 на хост-компьютере. Откройте новый терминал (этот терминал должен быть на хост-компьютере) и выполните следующую команду:

docker network ls

Эта команда отобразит сети Docker, которые есть на нашем хосте:

NETWORK ID          NAME                DRIVER          SCOPE
97afd7b53a88        bridge              bridge            local
f258843b5645        host                host                local
274ae0d40799        none                null                local

Сеть с именем bridge соответствует сети docker0. Убедимся в этом, проверив IP-адреса моста в хосте и в виртуальной машине докера, они должны совпадать.

Чтобы увидеть IP-адрес docker0, мы запускаем следующую команду в сеансе виртуальной машины:

docker-desktop:~# ip address show docker0

Это отобразит:

5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:e9:e4:14:60 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:e9ff:fee4:1460/64 scope link
valid_lft forever preferred_lft forever

IP-адрес docker0: 172.17.0.1.

Теперь нам нужно узнать IP-адрес docker0 на нашем хосте, для этого нам нужно выполнить следующую команду:

docker network inspect bridge --format='{{json .IPAM.Config}}'

Он выведет:

[{"Subnet":"172.17.0.0/16","Gateway":"172.17.0.1"}]

Первое направление (172.17.0.0) соответствует подсети сети, второе — шлюзу сети. Docker по умолчанию всегда выбирает интерфейс моста в качестве шлюза, поэтому мы можем предположить, что 172.17.0.1 — это направление, назначенное docker0 (видит его с хоста).

Как мы видим, два направления IP совпадают, поэтому сетевой мост на нашем хосте соответствует интерфейсу docker0 на виртуальной машине.

Теперь, когда мы знаем, что интерфейс docker0 является мостом, а контейнеры связаны между собой с его помощью через VETH, мы должны увидеть их в действии. Давайте добавим контейнер в сеть.

Создайте файл докера, содержащий следующее:

FROM python:3.7
RUN apt-get update && \
apt-get -y install sudo && \
apt-get install net-tools && \
apt-get install iputils-ping && \
sudo apt-get install curl && \
sudo apt-get install traceroute && \
sudo apt-get install nano
RUN useradd -m docker && echo "docker:docker" | chpasswd && adduser docker sudo
USER docker
CMD /bin/bash

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

Выполните следующую команду на хосте и по тому же пути к файлу докера:

docker build -t network-machine .

Созданный образ будет называться network-machine.

Теперь запустим контейнер

docker run -itd --cap-add=NET_ADMIN --name=m1 network-machine

Эта команда создаст контейнер с использованием сетевой машины, и у него будут разрешения на выполнение некоторых сетевых команд (контейнер будет называться m1). Как мы знаем, если мы запустим контейнер без указания сети, он будет добавлен в сеть по умолчанию (интерфейс docker0).

Сеть с точки зрения виртуальной машины

В созданном нами сеансе виртуальной машины выполните следующее:

docker-desktop:~# ip address show

Как мы знаем, эта команда отобразит интерфейсы на ВМ, но в данном случае у нас будет новый:

docker-desktop:~# ip address show
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 brd 127.255.255.255 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 02:50:00:00:00:01 brd ff:ff:ff:ff:ff:ff
inet 192.168.65.3/24 brd 192.168.65.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::50:ff:fe00:1/64 scope link
valid_lft forever preferred_lft forever
3: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
4: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1000
link/tunnel6 :: brd ::
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:e9:e4:14:60 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:e9ff:fee4:1460/64 scope link
valid_lft forever preferred_lft forever
34: veth12d18b2@if33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether aa:0e:35:0b:ec:1c brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::a80e:35ff:fe0b:ec1c/64 scope link
valid_lft forever preferred_lft forever

Новый интерфейс 34: veth12d18b2@if33. Этого нового интерфейса не было, потому что мы не создали контейнер. Это интерфейс, который подключается к интерфейсу docker0 внутри виртуальной машины.

При таком поведении мы можем предположить, что каждый раз, когда мы создаем контейнер, будет создаваться интерфейс VETH для подключения этого контейнера к docker0. Давайте создадим еще один контейнер, чтобы проверить это поведение.

docker run -itd --cap-add=NET_ADMIN --name=m2 network-machine

Это аналогичный контейнер с той лишь разницей, что он будет называться m2. Если мы хотим осмотреть сеть, там должен отображаться новый контейнер. Кроме того, мы можем получить к нему доступ и увидеть его интерфейс eth0, для этого будет назначен IP-адрес, который будет соответствовать подсети docker0.

Выполните следующую команду на виртуальной машине Docker, чтобы отобразить на ней интерфейсы:

docker-desktop:~# ip address show

Вывод:

docker-desktop:~# ip address 
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 brd 127.255.255.255 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 02:50:00:00:00:01 brd ff:ff:ff:ff:ff:ff
inet 192.168.65.3/24 brd 192.168.65.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::50:ff:fe00:1/64 scope link
valid_lft forever preferred_lft forever
3: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
link/ipip 0.0.0.0 brd 0.0.0.0
4: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1000
link/tunnel6 :: brd ::
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:e9:e4:14:60 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:e9ff:fee4:1460/64 scope link
valid_lft forever preferred_lft forever
34: veth12d18b2@if33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether aa:0e:35:0b:ec:1c brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::a80e:35ff:fe0b:ec1c/64 scope link
valid_lft forever preferred_lft forever
40: veth99e0059@if39: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether e2:4a:0b:24:c4:96 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::e04a:bff:fe24:c496/64 scope link
valid_lft forever preferred_lft forever

Как мы и ожидали, у нас появился новый интерфейс VETH. Чтобы узнать, какой интерфейс VETH соответствует каждому контейнеру, мы можем взять интерфейс eth0 из одного из контейнеров и сопоставить «eth0@if‹interface_number›».

Например, контейнер eth0 для m1:

33: eth0@if34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever

Его interface_number равен 34, поэтому он соответствует интерфейсу 34 виртуальной машины Docker:

docker-desktop:~# ip address show type veth
34: veth12d18b2@if33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether aa:0e:35:0b:ec:1c brd ff:ff:ff:ff:ff:ff link-netnsid 1
inet6 fe80::a80e:35ff:fe0b:ec1c/64 scope link
valid_lft forever preferred_lft forever
40: veth99e0059@if39: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether e2:4a:0b:24:c4:96 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::e04a:bff:fe24:c496/64 scope link
valid_lft forever preferred_lft forever

Номер интерфейса 34 — это veth12d18b2@if33, так что это VETH, используемый m1 для подключения к мосту docker0.

Наконец, если мы хотим узнать, какие VETH подключены к нашему мосту, мы можем запустить следующую команду на виртуальной машине:

docker-desktop:~# ip link show master docker0

В этом случае мы можем видеть интерфейсы VETH, подключенные к docker0, но мы можем использовать любой другой мост с этой командой, чтобы увидеть VETH, подключенные к соответствующему мосту.

docker-desktop:~# ip link show master docker0
34: veth12d18b2@if33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether aa:0e:35:0b:ec:1c brd ff:ff:ff:ff:ff:ff link-netnsid 1
40: veth99e0059@if39: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether e2:4a:0b:24:c4:96 brd ff:ff:ff:ff:ff:ff link-netnsid 0

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

Если мы хотим проверить связь между двумя контейнерами, мы можем отправить ping-запросы между ними.

Глядя на диаграмму, интерфейс docker0 имеет соединение с интерфейсом eth0, это дает возможность нашим контейнерам отправлять эхо-запросы на другие машины в Интернете. Но на самом деле это соединение осуществляется с помощью брандмауэра Linux с именем «Iptables».

Iptables имеет возможность создавать правила для управления пакетами, которые проходят через наши интерфейсы. Docker по умолчанию создает специальные правила для контейнеров.

Если вы хотите увидеть эти правила, выполните следующую команду на виртуальной машине:

docker-desktop:~# iptables -L

Он будет отображать:

docker-desktop:~# iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
DOCKER-USER  all  --  anywhere             anywhere
DOCKER-ISOLATION-STAGE-1  all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
DOCKER     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
Chain DOCKER (1 references)
target     prot opt source               destination
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target     prot opt source               destination
DOCKER-ISOLATION-STAGE-2  all  --  anywhere             anywhere
RETURN     all  --  anywhere             anywhere
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
target     prot opt source               destination
DROP       all  --  anywhere             anywhere
RETURN     all  --  anywhere             anywhere
Chain DOCKER-USER (1 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere

Эти таблицы имеют правила, которые выполняются, если пакет INPUT, OUTPUT или FORWARD нашей сети.

Docker по умолчанию создает правила для перенаправления пакетов, которые отправляются с docker0 на интерфейс eth0. Кроме того, создает NAT в наших сетях докеров, чтобы они вели себя как частные сети и блокировали доступ извне.

Вывод

Знание функциональности docker0 не всегда необходимо, вы можете в значительной степени контролировать Docker Network, используя его команды, и с их помощью можно многого добиться. Но, зная, как работает черный ящик, мы сможем добавить или изменить некоторые его характеристики. В моем случае я использовал Docker Network для моделирования сетей с различными типами NAT.