Docker – виртуализация сети. Часть 3 (свой оверлей)
В этой статье мы посмотрим, как можно создать собственный оверлей при помощи стандартных команд Linux.
В статье Docker – Виртуализация сети. Часть 1 мы рассмотрели, как Docker создает выделенное пространство имен для оверлей сети и подключает контейнеры с этому пространству имен. В статье Docker – Виртуализация сети. Часть 2 (VXLAN) мы подробно рассмотрели, как Docker использует VXLAN для туннелирования трафика между хостами в оверлей сети. В этой статье мы посмотрим, как можно создать собственный оверлей при помощи стандартных команд Linux.
Ручное создание оверлея
Если вы выполняли команды из первых двух статей, вам нужно очистить хосты докеров, удалив все наши контейнеры и оверлей сеть:
# останавливаем все контейнеры
$ docker stop $(docker ps -a -q)
# удаляем все контейнеры
$ docker rm -f $(docker ps -a -q)
# удаляем созданную сеть
$ docker network rm demo-network
Далее, первое, что нам необходимо сделать – это создать собственное пространство имен для сети. Все последующие команды необходимо выполнить на обоих хостах docker-1 и docker-2:
$ sudo ip netns add overlay-ns
Теперь в этом пространстве имен мы создадим bridge, присвоим ему IP-адрес и поднимем его интерфейс:
$ sudo ip netns exec overlay-ns ip link add dev br0 type bridge
$ sudo ip netns exec overlay-ns ip addr add dev br0 192.168.0.1/24
$ sudo ip netns exec overlay-ns ip link set br0 up
Далее необходимо создать интерфейс VXLAN и подключить его к бриджу:
$ sudo ip link add dev vxlan1 type vxlan id 42 proxy learning dstport 4789
$ sudo ip link set vxlan1 netns overlay-ns
$ sudo ip netns exec overlay-ns ip link set vxlan1 master br0
$ sudo ip netns exec overlay-ns ip link set vxlan1 up
Самой важной командой здесь является создание интерфейса VXLAN. Мы настроили его на использование идентификатора VXLAN id 42 и туннелирование трафика по стандартному порту VXLAN. Опция прокси позволяет интерфейсу vxlan отвечать на запросы ARP (см. часть 2). Обратите внимание, что мы не создавали интерфейс VXLAN внутри пространства имен, вместо этого мы создали его на хосте, а затем только переместили его в пространство имен. Это необходимо, чтобы интерфейс VXLAN мог поддерживать связь с нашим основным интерфейсом хоста и отправлять трафик по сети. Если бы мы создали интерфейс внутри пространства имен (например, как мы сделали для br0), мы бы не смогли отправлять трафик за пределы пространства имен.
Как только вы выполните эти команды на docker-1 и docker-2, то мы получим следующую схему:
Настройка хоста Docker-1
Теперь мы создадим контейнеры и подключим их к нашему бриджу. Начнем с docker-1. Сначала создаем контейнер:
$ docker run -d --net=none --name=demo debian /bin/bash -c "while true; do sleep 1; done"
Нам понадобится путь сетевого пространства имен для этого контейнера. Мы можем найти его в метаданных контейнера (список наиболее важных и распространенных команд по работе с Docker контейнерами).
$ ctn_ns_path=$(docker inspect --format="{{ .NetworkSettings.SandboxKey}}" demo)
Наш контейнер не имеет сетевого подключения из-за опции —net=none. Далее мы создадим интерфейс veth и поместим его ендпоинт veth1 в наше пространство имен оверлей сети, присоединим его к бриджу и поднимем его.
$ sudo ip link add dev veth1 mtu 1450 type veth peer name veth2 mtu 1450
$ sudo ip link set dev veth1 netns overlay-ns
$ sudo ip netns exec overlay-ns ip link set veth1 master br0
$ sudo ip netns exec overlay-ns ip link set veth1 up
В первой команде используется MTU 1450. Это необходимо из-за накладных расходов, добавляемых заголовками VXLAN. И последний шаг — настроить veth2: нам надо отправить его в пространство имен контейнерных сетей и настроить его с помощью MAC-адреса (02:42:c0:a8:00:02) и IP-адреса (192.168.0.2):
$ ctn_ns=${ctn_ns_path##*/}
$ sudo ln -sf $ctn_ns_path /var/run/netns/$ctn_ns
$ sudo ip link set dev veth2 netns $ctn_ns
$ sudo ip netns exec $ctn_ns ip link set dev veth2 name eth0 address 02:42:c0:a8:00:02
$ sudo ip netns exec $ctn_ns ip addr add dev eth0 192.168.0.2/24
$ sudo ip netns exec $ctn_ns ip link set dev eth0 up
$ sudo rm /var/run/netns/$ctn_ns
Символьная ссылка в /var/run/netns требуется для того, чтобы мы могли использовать собственные команды net netns, что позволит нам переместить интерфейс в пространство имен контейнеров. Также в этом примере мы используем ту же схему адресации, что и в Docker: последние 4 байта MAC-адреса соответствуют IP-адресу контейнера, а второй — идентификатору VXLAN.
Настройка хоста Docker-2
Далее мы должны сделать то же самое на docker-2, но указав разные MAC и IP-адреса (02:42:c0:a8:00:03 и 192.168.0.3). Запускаем контейнер:
$ docker run -d --net=none --name=demo-2 debian /bin/bash -c "while true; do sleep 1; done"
Определяем пространство имен:
$ ctn_ns_path=$(docker inspect --format="{{ .NetworkSettings.SandboxKey}}" demo-2)
Поднимаем интерфейс veth1:
$ sudo ip link add dev veth1 mtu 1450 type veth peer name veth2 mtu 1450
$ sudo ip link set dev veth1 netns overlay-ns
$ sudo ip netns exec overlay-ns ip link set veth1 master br0
$ sudo ip netns exec overlay-ns ip link set veth1 up
Настраиваем veth2:
$ ctn_ns=${ctn_ns_path##*/}
$ sudo ln -sf $ctn_ns_path /var/run/netns/$ctn_ns
$ sudo ip link set dev veth2 netns $ctn_ns
$ sudo ip netns exec $ctn_ns ip link set dev veth2 name eth0 address 02:42:c0:a8:00:03
$ sudo ip netns exec $ctn_ns ip addr add dev eth0 192.168.0.3/24
$ sudo ip netns exec $ctn_ns ip link set dev eth0 up
$ sudo rm /var/run/netns/$ctn_ns
Теперь наш стенд имеет следующую конфигурацию:
Теперь, когда наши контейнеры настроены, можно протестировать подключение:
$ docker exec -it demo ping 192.168.0.3
PING 192.168.0.3 (192.168.0.3): 56 data bytes
^C--- 192.168.0.3 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss
Мы еще не можем пинговать соседний контейнер. Давайте попробуем понять, почему, посмотрев записи ARP в контейнере и в пространстве имен оверлей сети:
$ docker exec demo ip neighbor show
$ sudo ip netns exec overlay-ns ip neighbor show
Обе команды не возвращают никакого результата: они не знают, какой MAC-адрес связан с IP 192.168.0.3. Мы можем проверить, что наша команда генерирует запрос ARP, запустив tcpdump в пространстве имен оверлей сети:
$ sudo ip netns exec overns tcpdump -i br0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
Если мы повторно запустим команду ping с другого терминала на хосте docker-1, то получим следующий вывод:
07:13:09.833584 ARP, Request who-has 192.168.0.3 tell 192.168.0.2, length 28
07:13:10.836146 ARP, Request who-has 192.168.0.3 tell 192.168.0.2, length 28
Запрос ARP отправляется бродкастом и принимается нашим пространством имен, но он не получает ответа. В части 2 мы видели, что демон Docker заполняет таблицы ARP и FDB и использует опцию прокси-сервера интерфейса VXLAN для ответа на эти запросы. Мы настроили наш интерфейс с этой опцией, чтобы мы могли сделать то же самое. Осталось просто добавить записи ARP и FDB в пространстве имен созданной нами оверлей сети:
$ sudo ip netns exec overlay-ns ip neighbor add 192.168.0.3 lladdr 02:42:c0:a8:00:03 dev vxlan1
$ sudo ip netns exec overlay-ns bridge fdb add 02:42:c0:a8:00:03 dev vxlan1 self dst 192.168.1.5 vni 42 port 4789
Первая команда создает запись ARP для 192.168.0.3, а вторая настраивает форвард таблицу, указывая, что MAC-адрес доступен за интерфейсом VXLAN, с идентификатором VXLAN 42 и хостом 192.168.1.5. У нас есть связь?
$ docker exec -it demo ping 192.168.0.3
PING 192.168.0.3 (192.168.0.3): 56 data bytes
^C--- 192.168.0.3 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss
Пока что нет, т.к. мы еще не настроили хост docker-2: ICMP-запрос получен контейнером на docker-2, но он не знает, куда ответить. Мы можем проверить это на docker-2:
$ sudo ip netns exec overlay-ns ip neighbor show
$ sudo ip netns exec overlay-ns bridge fdb show
d2:ec:31:61:6b:cd dev vxlan1 master br0 permanent
02:42:c0:a8:00:02 dev vxlan1 master br0
52:34:d5:96:a2:0e dev veth1 master br0 permanent
d2:ec:31:61:6b:cd dev vxlan1 vlan 1 master br0 permanent
52:34:d5:96:a2:0e dev br0 vlan 1 master br0 permanent
02:42:c0:a8:00:03 dev veth1 master br0
02:42:c0:a8:00:02 dev vxlan1 dst 192.168.1.4 link-netnsid 0 self
33:33:00:00:00:01 dev veth1 self permanent
01:00:5e:00:00:01 dev veth1 self permanent
33:33:ff:96:a2:0e dev veth1 self permanent
Первая команда показывает, как и ожидалось, что у нас нет информации ARP на 192.168.0.3. А вот результат второй команды удивителен, потому что мы видим запись в базе данных FDB для нашего контейнера на docker-1. И вот что происходит: когда ICMP-запрос достигает интерфейса, входящий пакет был «узнан» и его данные добавлены в базу данных. Такое поведение возможно благодаря опции «обучения» интерфейса VXLAN. Давайте добавим информацию ARP на docker-2 и убедимся, что ping работает нормально:
$ sudo ip netns exec overlay-ns ip neighbor add 192.168.0.2 lladdr 02:42:c0:a8:00:02 dev vxlan1
Пингуем с docker-1:
$ docker exec -it demo ping 192.168.0.3
PING 192.168.0.3 (192.168.0.3): 56 data bytes
64 bytes from 192.168.0.3: icmp_seq=0 ttl=64 time=1.472 ms
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.375 ms
^C--- 192.168.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.375/0.923/1.472/0.549 ms
Мы только что успешно создали оверлей сеть при помощи стандартных команд Linux.
Динамическое обнаружение контейнеров
Мы только что создали свою оверлей сеть с нуля. Однако, при таком подходе нам нужно вручную создавать записи ARP и FDB для контейнеров, чтобы они имели возможность общаться друг с другом. Давайте рассмотрим, как этот процесс может быть автоматизирован.
Для начала очистим нашу стенд, чтобы снова начать с нуля. На docker-1 и docker-2:
$ docker stop $(docker ps -a -q)
$ docker rm $(docker ps -a -q)
$ sudo ip netns delete overlay-ns
Обработка событий Netlink
Netlink используется для передачи информации между процессами пространства ядра и пользователя: https://en.wikipedia.org/wiki/Netlink. Iproute2, который мы использовали ранее для настройки интерфейсов, полагается на Netlink для получения/отправки информации о сетевой конфигурации в ядро. Он состоит из нескольких протоколов («семейств»), предназначенных для связи с различными компонентами ядра. Наиболее распространенным протоколом является NETLINK_ROUTE, который является интерфейсом для маршрутизации и компоновки линков.
Для каждого протокола сообщения Netlink организованы группами, например, в NETLINK_ROUTE у вас есть:
- RTMGRP_LINK: линки на связанные сообщения
- RTMGRP_NEIGH: сообщения, связанные соседом
- многие другие
Для каждой группы у вас есть несколько уведомлений, например:
- RTMGRP_LINK:
- RTM_NEWLINK: была создана ссылка
- RTM_DELLINK: ссылка удалена
- RTMGRP_NEIGH:
- RTM_NEWNEIGH: добавлен сосед
- RTM_DELNEIGH: сосед удален
- RTM_GETNEIGH: ядро ищет соседа
Я описал сообщения, полученные в пользовательском пространстве в момент, когда ядро отправляет уведомления об этих событиях, аналогичные сообщения могут быть отправлены в ядро для настройки линков или соседей.
Iproute2 позволяет нам прослушивать события Netlink с помощью команды monitor. Предположим, мы хотим отслеживать информацию о линке, тогда:
$ ip monitor link
В другом терминале на docker-1 мы можем создать линк, а затем удалить его:
$ sudo ip link add dev veth1 type veth peer name veth2
$ sudo ip link del veth1
На первом терминале мы видим следующий вывод, когда мы создали интерфейсы:
48: veth2@NONE: mtu 1500 qdisc noop state DOWN
link/ether b2:d0:53:73:dc:59 brd ff:ff:ff:ff:ff:ff
49: veth1@veth2: mtu 1500 qdisc noop state DOWN
link/ether 86:b8:1b:23:93:6e brd ff:ff:ff:ff:ff:ff
И, когда удалили их:
Deleted 49: veth1@NONE: mtu 1500 qdisc noop state DOWN
link/ether 86:b8:1b:23:93:6e brd ff:ff:ff:ff:ff:ff
Deleted 48: veth2@NONE: mtu 1500 qdisc noop state DOWN
link/ether b2:d0:53:73:dc:59 brd ff:ff:ff:ff:ff:ff
Мы можем использовать эту команду для мониторинга и других событий. Запустим в первом терминале:
$ ip monitor route
Во втором смотрим на результат выполнения команд:
$ ip route add 8.8.8.8 via 192.168.1.4
$ ip route del 8.8.8.8 via 192.168.1.4
Вывод будет следующим:
8.8.8.8 via 192.168.1.4 dev eth0
Deleted 8.8.8.8 via 192.168.1.4 dev eth0
В нашем случае нас интересуют события связанные с соседом, а именно RTM_GETNEIGH, которые генерируются, когда ядро не имеет информации о соседях и отправляет это уведомление в пространство пользователя, чтобы приложение могло его создать. По умолчанию это событие не отправляется в пользовательское пространство, но мы можем включить это поведение и отслеживать нужные нам уведомления:
$ echo 1 | sudo tee -a /proc/sys/net/ipv4/neigh/eth0/app_solicit
$ ip monitor neigh
В последствие этот параметр нам не понадобится, потому что параметры l2miss и l3miss нашего интерфейса vxlan будут генерировать RTM_GETNEIGH события автоматически.
А пока во втором терминале мы можем сгенерировать событие GETNEIGH:
$ ping 192.168.1.100
Полученный нами вывод от монитора:
miss 192.168.1.100 dev eth0 INCOMPLETE
192.168.1.100 dev eth0 FAILED
Мы можем использовать ту же команду в контейнерах, которые подключены к нашей оверлей сети. Давайте создадим ее и подключим к ней контейнер. Чтобы не вводить все команды из примеров выше, на docker-1 создадим файл create_overlay.sh следующего содержания:
#!/bin/bash
sudo ip netns delete overlay-ns 2> /dev/null && echo "Deleting existing overlay"
sudo ip netns add overlay-ns
sudo ip netns exec overlay-ns ip link add dev br0 type bridge
sudo ip netns exec overlay-ns ip addr add dev br0 192.168.0.1/24
sudo ip link add dev vxlan1 type vxlan id 42 proxy learning l2miss l3miss dstport 4789
sudo ip link set vxlan1 netns overlay-ns
sudo ip netns exec overlay-ns ip link set vxlan1 master br0
sudo ip netns exec overlay-ns ip link set vxlan1 up
sudo ip netns exec overlay-ns ip link set br0 up
И файл attach-ctn.sh следующего содержания:
#!/bin/bash
ctn=${1:-demo}
ip=${2:-2}
ctn_ns_path=$(docker inspect --format="{{ .NetworkSettings.SandboxKey}}" $ctn)
ctn_ns=${ctn_ns_path##*/}
# create veth interfaces
sudo ip link add dev veth1 mtu 1450 type veth peer name veth2 mtu 1450
# attach first peer to the bridge in our overlay namespace
sudo ip link set dev veth1 netns overlay-ns
sudo ip netns exec overlay-ns ip link set veth1 master br0
sudo ip netns exec overlay-ns ip link set veth1 up
# crate symlink to be able to use ip netns commands
sudo ln -sf $ctn_ns_path /var/run/netns/$ctn_ns
sudo ip link set dev veth2 netns $ctn_ns
# move second peer tp container network namespace and configure it
sudo ip netns exec $ctn_ns ip link set dev veth2 name eth0 address 02:42:c0:a8:00:0${ip}
sudo ip netns exec $ctn_ns ip addr add dev eth0 192.168.0.${ip}/24
sudo ip netns exec $ctn_ns ip link set dev eth0 up
# Clean up symlink
sudo rm /var/run/netns/$ctn_ns
Далее делаем то, что задумывали:
$ chmod +x ./create_overlay.sh
$ chmod +x ./attach-ctn.sh
# Создаем оверлей
$ ./create_overlay.sh
# Запускаем контейнер
$ docker run -d --net=none --name=demo-1 debian /bin/bash -c "while true; do sleep 1; done"
# Подключаем к сети
$ ./attach-ctn.sh demo-1 2
# Запускаем ip monitor в контейнере
$ docker exec demo-1 ip monitor neigh
В соседнем терминале мы можем выполнить ping какого-нибудь неизвестного хоста для генерации событий GETNEIGH:
$ docker exec demo-1 ping 192.168.0.3
В первом терминале видим события:
192.168.0.3 dev eth0 FAILED
Мы также можем посмотреть в пространство имен нашей оверлей сети:
$ sudo ip netns exec overlay-ns ip monitor neigh
miss 192.168.0.3 dev vxlan1 STALE
Это событие несколько отличается, потому что оно генерируется интерфейсом vxlan (т.к. мы создали интерфейс с параметрами l2miss и l3miss). Давайте добавим запись соседа в пространство имен сети:
$ sudo ip netns exec overlay-ns ip neighbor add 192.168.0.3 lladdr 02:42:c0:a8:00:03 dev vxlan1 nud permanent
Теперь, если мы запустим команду ip monitor neigh и попытаемся выполнить ping с другого терминала, вот что мы получим:
$ ip netns exec overlay-ns ip monitor neigh
miss dev vxlan1 lladdr 02:42:c0:a8:00:03 STALE
Теперь, когда у нас есть информация ARP, мы получаем l2miss, потому что мы не знаем, где находится MAC-адрес в оверлее. Давайте добавим эту информацию:
$ sudo ip netns exec overlay-ns bridge fdb add 02:42:c0:a8:00:03 dev vxlan1 self dst 192.168.1.5 vni 42 port 4789
Если мы снова запустим команду ip monitor neigh и попытаемся выполнить ping, мы больше не увидим события соседей. Команда ip monitor очень полезна, чтобы видеть, что происходит, но т.к. нам теперь нужно поймать эти события для заполнения L2 и L3 информации, то мы будем взаимодействовать с ними программно.
Вот простой python пример, показывающий как подписаться на сообщения Netlink и декодировать события GETNEIGH. Файл l2l3miss.py:
#!/usr/bin/env python
import os
import socket
import logging
import struct
# Мапинг констант из Linux ядра.
RTMGRP_LINK = 1
RTMGRP_NOTIFY = 2
RTMGRP_NEIGH = 4
RTMGRP_TC = 8
NLMSG_NOOP = 1
NLMSG_ERROR = 2
RTM_NEWLINK = 16
RTM_DELLINK = 17
RTM_GETNEIGH = 30
if_family = {2 : "AF_INET"}
nud_state = {
0x01 : "NUD_INCOMPLETE",
0x02 : "NUD_REACHABLE",
0x04 : "NUD_STALE",
0x08 : "NUD_DELAY",
0x10 : "NUD_PROBE",
0x20 : "NUD_FAILED",
0x40 : "NUD_NOARP",
0x80 : "NUD_PERMANENT",
0x00 : "NUD_NONE"
}
type = {
0 : "RTN_UNSPEC",
1 : "RTN_UNICAST",
2 : "RTN_LOCAL",
3 : "RTN_BROADCAST",
4 : "RTN_ANYCAST",
5 : "RTN_MULTICAST",
6 : "RTN_BLACKHOLE",
7 : "RTN_UNREACHABLE",
8 : "RTN_PROHIBIT",
9 : "RTN_THROW",
10 : "RTN_NAT",
11 : "RTN_XRESOLVE"
}
nda_type = {
0 : "NDA_UNSPEC",
1 : "NDA_DST",
2 : "NDA_LLADDR",
3 : "NDA_CACHEINFO",
4 : "NDA_PROBES",
5 : "NDA_VLAN",
6 : "NDA_PORT",
7 : "NDA_VNI",
8 : "NDA_IFINDEX",
9 : "NDA_MASTER",
10 : "NDA_LINK_NETNSID"
}
# Создать netlink сокет и слушать NEIGHBOR NOTIFICATION
s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
s.bind((os.getpid(), RTMGRP_NEIGH))
while True:
data = s.recv(65535)
msg_len, msg_type, flags, seq, pid = struct.unpack("=LHHLL", data[:16])
# Нас интересуют только сообщения GETNEIGH
if msg_type != RTM_GETNEIGH:
continue
data=data[16:]
ndm_family, _, _, ndm_ifindex, ndm_state, ndm_flags, ndm_type = struct.unpack("=BBHiHBB", data[:12])
logging.debug("Received a Neighbor miss")
logging.debug("Family: {}".format(if_family.get(ndm_family,ndm_family)))
logging.debug("Interface index: {}".format(ndm_ifindex))
logging.debug("State: {}".format(nud_state.get(ndm_state,ndm_state)))
logging.debug("Flags: {}".format(ndm_flags))
logging.debug("Type: {}".format(type.get(ndm_type,ndm_type)))
data=data[12:]
rta_len, rta_type = struct.unpack("=HH", data[:4])
logging.debug("RT Attributes: Len: {}, Type: {}".format(rta_len,nda_type.get(rta_type,rta_type)))
data=data[4:]
if nda_type.get(rta_type,rta_type) == "NDA_DST":
dst=socket.inet_ntoa(data[:4])
logging.info("L3Miss: Who has IP: {}?".format(dst))
if nda_type.get(rta_type,rta_type) == "NDA_LLADDR":
mac="%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB",data[:6])
logging.info("L2Miss: Who has MAC: {}?".format(mac))
Оригинал скрипта, как в прочем и друге примеры можно найти в репозитории Laurent Bernaille — автора этого материала. Давайте быстро перейдем к самой важной части скрипта. Во-первых, мы создаем сокет NETLINK, настраиваем его для протокола NETLINK_ROUTE и подписываемся на группу событий о соседях (RTMGRP_NEIGH):
s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
s.bind((os.getpid(), RTMGRP_NEIGH))
Мы декодируем сообщения и фильтруем только нужные нам для обработки GETNEIGH:
msg_len, msg_type, flags, seq, pid = struct.unpack("=LHHLL", data[:16])
if msg_type != RTM_GETNEIGH:
continue
Чтобы понять, как декодировано сообщение, вот вид этого сообщения. Заголовок Netlink оранжевый:
Как только у нас появится сообщение GETNEIGH, мы можем декодировать заголовок ndmsg(синий):
ndm_family, _, _, ndm_ifindex, ndm_state, ndm_flags, ndm_type = struct.unpack("=BBHiHBB", data[:12])
За этим заголовком следует структура rtattr, которая содержит интересующие нас данные. Сначала мы декодируем заголовок структуры (фиолетовый):
rta_len, rta_type = struct.unpack("=HH", data[:4])
Мы можем получать два разных типа сообщений:
NDA_DST: L3 miss, ядро ищет MAC-адрес, связанный с IP-адресом в поле данных (4 байта данных после заголовка rta)
NDA_LLADDR: L2 miss, ядро ищет хост vxlan для MAC-адреса в поле данных (6 байтов данных после заголовка rta)
data=data[4:]
if nda_type.get(rta_type,rta_type) == "NDA_DST":
dst=socket.inet_ntoa(data[:4])
logging.info("L3Miss: Who has IP: {}?".format(dst))
if nda_type.get(rta_type,rta_type) == "NDA_LLADDR":
mac="%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB",data[:6])
logging.info("L2Miss: Who has MAC: {}?".format(mac))
Давайте запустим этот скрипт, но предварительно пересоздадим все для чистоты эксперимента:
$ docker stop $(docker ps -aq)
$ docker rm $(docker ps -aq)
$ ./create_overlay.sh
$ docker run -d --net=none --name=demo-1 debian /bin/bash -c "while true; do sleep 1; done"
$ ./attach-ctn.sh demo-1 2
$ sudo ip netns exec overlay-ns ./l2l3miss.py
Выполняем пинг из соседнего терминала:
$ docker exec -it demo-1 ping 192.168.0.3
Получаем следующий вывод:
INFO:root:L3Miss: Who has IP: 192.168.0.3?
Если мы добавим информацию о соседе и позовем пинг снова:
$ sudo ip netns exec overlay-ns ip neighbor add 192.168.0.3 lladdr 02:42:c0:a8:00:03 dev vxlan1
$ docker exec -it demo-1 ping 192.168.0.3
Теперь мы получаем L2 miss-ы, потому что мы добавили L3 информацию:
INFO:root:L2Miss: Who has MAC: 02:42:c0:a8:00:03?
Динамическое обнаружение в Consul
Теперь, когда мы увидели, как мы можем получать уведомления о промахах L2 и L3 и ловить эти события в python, мы будем хранить все данные L2 и L3 в Consul и добавлять записи в пространство имен overlay, когда мы получаем событие о соседе.
Во-первых, мы будем создать записи в консуле. Мы можем сделать это также с помощью Web-интерфейса или curl:
$ curl -X PUT -d '02:42:c0:a8:00:02' http://192.168.1.6:8500/v1/kv/demo-1/arp/192.168.0.2
$ curl -X PUT -d '02:42:c0:a8:00:03' http://192.168.1.6:8500/v1/kv/demo-1/arp/192.168.0.3
$ curl -X PUT -d '192.168.1.4' http://192.168.1.6:8500/v1/kv/demo-1/fib/02:42:c0:a8:00:02
$ curl -X PUT -d '192.168.1.5' http://192.168.1.6:8500/v1/kv/demo-1/fib/02:42:c0:a8:00:03
Мы создаем два типа записей:
- ARP: используя ключи demo-1/arp/{IP-адрес} с MAC-адресом в качестве значения
- FIB: используя ключи demo-1/arp/{MAC-адрес} с IP-адресом сервера, на котором находится данный MAC-адрес, в качестве значения
В веб-интерфейсе мы получаем следующую картину ключей ARP:
Теперь нам просто нужно искать нужные данные, когда мы получаем событие GETNEIGH и заполнять таблицы ARP или FIB на основе данных из Consul. Вот слегка упрощенный скрипт на python (arpd-consul.py), который делает это:
#!/usr/bin/env python
import consul
import logging
from pyroute2 import NetNS
from pyroute2.netlink.rtnl import ndmsg
from pyroute2.netlink.exceptions import NetlinkError
vxlan_ns="overlay-ns"
consul_host="192.168.1.6"
consul_prefix="demo-1"
if_family = {2 : "AF_INET"}
nud_state = {
0x01 : "NUD_INCOMPLETE",
0x02 : "NUD_REACHABLE",
0x04 : "NUD_STALE",
0x08 : "NUD_DELAY",
0x10 : "NUD_PROBE",
0x20 : "NUD_FAILED",
0x40 : "NUD_NOARP",
0x80 : "NUD_PERMANENT",
0x00 : "NUD_NONE"
}
type = {
0 : "RTN_UNSPEC",
1 : "RTN_UNICAST",
2 : "RTN_LOCAL",
3 : "RTN_BROADCAST",
4 : "RTN_ANYCAST",
5 : "RTN_MULTICAST",
6 : "RTN_BLACKHOLE",
7 : "RTN_UNREACHABLE",
8 : "RTN_PROHIBIT",
9 : "RTN_THROW",
10 : "RTN_NAT",
11 : "RTN_XRESOLVE"
}
nda_type = {
0 : "NDA_UNSPEC",
1 : "NDA_DST",
2 : "NDA_LLADDR",
3 : "NDA_CACHEINFO",
4 : "NDA_PROBES",
5 : "NDA_VLAN",
6 : "NDA_PORT",
7 : "NDA_VNI",
8 : "NDA_IFINDEX",
9 : "NDA_MASTER",
10 : "NDA_LINK_NETNSID"
}
ipr = NetNS(vxlan_ns)
ipr.bind()
c=consul.Consul(host=consul_host, port=8500)
while True:
msg=ipr.get()
for m in msg:
if m['event'] != 'RTM_GETNEIGH':
continue
ifindex=m['ifindex']
ifname=ipr.get_links(ifindex)[0].get_attr("IFLA_IFNAME")
if m.get_attr("NDA_DST") is not None:
ipaddr=m.get_attr("NDA_DST")
logging.info("L3Miss on {}: Who has IP: {}?".format(ifname,ipaddr))
(idx,answer)=c.kv.get(consul_prefix+"/arp/"+ipaddr)
if answer is not None:
mac_addr=answer["Value"]
logging.info("Populating ARP table from Consul: IP {} is {}".format(ipaddr,mac_addr))
try:
ipr.neigh('add', dst=ipaddr, lladdr=mac_addr, ifindex=ifindex, state=ndmsg.states['permanent'])
except NetlinkError as (code,message):
print(message)
if m.get_attr("NDA_LLADDR") is not None:
lladdr=m.get_attr("NDA_LLADDR")
logging.info("L2Miss on {}: Who has Mac Address: {}?".format(ifname,lladdr))
(idx,answer)=c.kv.get(consul_prefix+"/fib/"+lladdr)
if answer is not None:
dst_host=answer["Value"]
logging.info("Populating FIB table from Consul: MAC {} is on host {}".format(lladdr,dst_host))
try:
ipr.fdb('add',ifindex=ifindex, lladdr=lladdr, dst=dst_host)
except NetlinkError as (code,message):
print(message)
Для установки pyroute2:
$ sudo yum -y install epel-release
$ sudo yum -y install python-pip
$ sudo pip install pyroute2
Для установки модуля consul:
$ sudo pip install python-consul
Полная версия этого скрипта также доступна в репозитории github, о котором упоминалось ранее. Вот краткое объяснение того, что этот скрипт делает:
Вместо обработки сообщений Netlink вручную мы используем библиотеку pyroute2. Эта библиотека будет анализировать сообщения Netlink и позволяет отправлять сообщения Netlink для настройки записей ARP/FIB. Кроме того, мы слушаем сокет Netlink в пространстве имен оверлей сети. Мы могли бы использовать команду ip netns для запуска скрипта в нашем пространстве имен, но нам также нужно обеспечить доступ к Consul для получения данных о конфигурации. Для этого мы запустим скрипт в пространстве имен сети хоста и забиндим сокет Netlink в пространстве имен оверлея:
from pyroute2 import NetNS
ipr = NetNS(vxlan_ns)
ipr.bind()
c=consul.Consul(host=consul_host,port=8500)
Далее ждем событий GETNEIGH:
while True:
msg=ipr.get()
for m in msg:
if m['event'] != 'RTM_GETNEIGH':
continue
Извлекаем индекс интерфейса и его имя (для логирования):
ifindex=m['ifindex']
ifname=ipr.get_links(ifindex)[0].get_attr("IFLA_IFNAME")
Теперь, если мы получаем сообщение о пропуске L3, будем получать IP-адрес из полезной нагрузки сообщения Netlink и пытаться найти связанную с ним ARP запись в Consul. Если мы найдем таковую, мы добавим запись соседа в пространство имен оверлея, отправив сообщение Netlink в ядро с соответствующей информацией.
if m.get_attr("NDA_DST") is not None:
ipaddr=m.get_attr("NDA_DST")
logging.info("L3Miss on {}: Who has IP: {}?".format(ifname,ipaddr))
(idx,answer)=c.kv.get(consul_prefix+"/arp/"+ipaddr)
if answer is not None:
mac_addr=answer["Value"]
logging.info("Populating ARP table from Consul: IP {} is {}".format(ipaddr,mac_addr))
try:
ipr.neigh('add', dst=ipaddr, lladdr=mac_addr, ifindex=ifindex, state=ndmsg.states['permanent'])
except NetlinkError as (code,message):
print(message)
Если мы получаем сообщение о пропуске L2, мы делаем то же самое с данными FIB.
Давайте посмотрим, как работает этот скрипт. Сначала мы очистим все и заново создадим пространство имен оверлея и контейнеры.
Удалите созданные в Web-интерфейсе Consul ключи. Далее на хосте docker-1:
$ docker stop $(docker ps -aq)
$ docker rm $(docker ps -aq)
$ ./create-overlay.sh
$ docker run -d --net=none --name=demo-1 debian /bin/bash -c "while true; do sleep 1; done"
$ ./attach-ctn.sh demo-1 2
На хосте docker-2:
$ docker stop $(docker ps -aq)
$ docker rm $(docker ps -aq)
$ ./create-overlay.sh
$ docker run -d --net=none --name=demo-1 debian /bin/bash -c "while true; do sleep 1; done"
$ ./attach-ctn.sh demo-2 3
Если мы попытаемся выполнить ping контейнера на docker-2 из контейнера на docker-1, у нас ничего не получится, потому что у нас пока нет данных ARP/FIB.
$ docker exec -it demo-1 ping -c 2 192.168.0.3
PING 192.168.0.3 (192.168.0.3): 56 data bytes
92 bytes from 192.168.0.2: Destination Host Unreachable
92 bytes from 192.168.0.2: Destination Host Unreachable
--- 192.168.0.3 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss
Запустим arpd-consul.py на обоих хостах:
$ sudo arpd-consul.py
И пробуем пинговать снова с другого терминала на docker-1:
$ docker exec -it demo-1 ping -c 2 192.168.0.3
PING 192.168.0.3 (192.168.0.3): 56 data bytes
64 bytes from 192.168.0.3: icmp_seq=0 ttl=64 time=1001.727 ms
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.745 ms
--- 192.168.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.745/501.236/1001.727/500.491 ms
При этом в выводе arpd-consul.py будет следующее:
INFO Starting new HTTP connection (1): consul
INFO L3Miss on vxlan1: Who has IP: 192.168.0.3?
INFO Populating ARP table from Consul: IP 192.168.0.3 is 02:42:c0:a8:00:03
INFO L2Miss on vxlan1: Who has Mac Address: 02:42:c0:a8:00:03?
INFO Populating FIB table from Consul: MAC 02:42:c0:a8:00:03 is on host 192.168.1.5
INFO L2Miss on vxlan1: Who has Mac Address: 02:42:c0:a8:00:03?
INFO Populating FIB table from Consul: MAC 02:42:c0:a8:00:03 is on host 192.168.1.5
Когда мы получаем пропуски L3 (нет данных ARP для 192.168.0.3), мы запрашиваем в Consul данные о MAC-адресе и заполняем таблицу соседей. Когда мы получаем пропуск L2 (нет информации FIB для 02:42:c0:a8:00:03), мы ищем этот MAC-адрес в Consul и заполняем базу данных форвардинга.
На docker-2 мы видим аналогичный вывод, но получаем только L3, потому что данные форварда L2 узнаются из пространством имен оверлея, когда пакет ICMP запроса попадает в сеть.
Вот краткая иллюстрация того, что мы построили: