Перейти к содержимому

Как добавить маршруты в докер контейнер

  • автор:

Управление сетевыми интерфейсами в Docker

Есть такие сетевые приложения, которым необходимо иметь выход во внешний мир не одним портом, а сразу большой группой. Примерами таких приложений могут являться различные программы работающие с потоковыми видео/аудио каналами. Например различные PBX решения, такие как Asterisk или FreeSWITCH.

Собственно, с попыток контейнеризировать Asterisk всё и началось. Есть готовые образы на Docker HUB, хорошим примером является dougbtv/asterisk.

Дальнейший текст можно считать вольным переводом документации по настройке сети в docker .

Не использовать контейнеризацию сетевой подсистемы

Это один из очень простых способов предоставить доступ к контейнеру. Если создать контейнер с опцией —net=host , то контейнер будет запущен с сетевой подсистемой хост-системы. Это крайне просто в реализации, но могут возникнуть другие проблемы. Например, придется создавать алиасы на необходимый внешний интерфейс с другими IP-адресами, а в приложения в контейнерах запускать с привязкой именно на этот адрес, а не на 0.0.0.0 . Все проблемы можно решить с помощью создания простого скрипта, который будет “разруливать” эти проблемы:

  • создавать алиас на интерфейс
  • передавать адрес в контейнер в какой-нибудь переменной
  • контейнер будет использовать эту переменную для запуска приложения

Но есть и положительная сторона — нет необходимости узнавать адрес назначаемый контейнеру.

И как дополнение — контейнер имеет доступ к хостовой подсистеме, может управлять ею, может иметь доступ к своему внешнему API. Это сомнительная необходимость, в частности со стороны безопасности.

Проброс одного порта

Проброс одного порта в docker существовал изначально и делался крайне просто с помощью ключа -p или опции EXPOSE в Dockerfile . Например для запуска контейнера с mysql достаточно было указать порт внутри контейнера и порт для хост машины -p : (ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort):

docker run -p 3306:3306 mysql 

Подробне про это можно прочитать в официальной документации тут.

Просмотреть проброшенные порты можно с помощью docker port :

docker port

Проброс группы портов

Проброс группы портов был востребован очень давно, но был добавлен в исходный код только 1 ноября 2014. Этот функционал уже доступен в docker версии 1.4.1 .

Пример совсем не похож на реальность, но на то он и пример. Необходимо пробросить 5 портов, 2000-2005 или 3000-3005. Сделаем это опцией —expose , но в одном случае ключ -P (публиковать проброшенные порты на хост-систему) будет установлен в состояние по умолчанию — false , а в другом изменим его состояние на true .

docker run --name ubuntu_true --expose=2000-2005 -P=true -d -i -t ubuntu /bin/bash 

После этого в iptables в цепочках FORWARD и DOCKER увидим следующее:

$ sudo iptables -L -n . Chain FORWARD (policy ACCEPT) target prot opt source destination ACCEPT tcp -- 0.0.0.0/0 192.168.10.13 tcp dpt:2003 ACCEPT tcp -- 0.0.0.0/0 192.168.10.13 tcp dpt:2002 ACCEPT tcp -- 0.0.0.0/0 192.168.10.13 tcp dpt:2001 ACCEPT tcp -- 0.0.0.0/0 192.168.10.13 tcp dpt:2000 ACCEPT tcp -- 0.0.0.0/0 192.168.10.13 tcp dpt:2005 ACCEPT tcp -- 0.0.0.0/0 192.168.10.13 tcp dpt:2004 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 . $ sudo iptables -L -n -t nat . Chain DOCKER (2 references) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:51166 to:192.168.10.13:2004 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:51167 to:192.168.10.13:2005 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:51168 to:192.168.10.13:2000 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:51169 to:192.168.10.13:2001 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:51170 to:192.168.10.13:2002 DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:51171 to:192.168.10.13:2003 

Как видно из вывода iptables — сервис docker создает правила для проброса портов с хост-системы в контейнер и обратно. Но, стоит учесть то, что при использовании автоматической публикации портов в хост-систему он использует свободные порты из диапазона 49153-65535.

Теперь сделаем тоже самое, но с диапазоном портов 3000-3005 и без публикации портов в хост-систему:

docker run --name ubuntu_true --expose=2000-2005 -P=true -d -i -t ubuntu /bin/bash 

После этого мы не уивидим никаких дополнительных правил для проброса портов, лишь правила для bridge-интерфейса docker0 :

$ sudo iptables -L -n . Chain FORWARD (policy ACCEPT) target prot opt source destination ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 . $ sudo iptables -L -n -t nat . Chain DOCKER (2 references) target prot opt source destination 

Теперь docker не создал правил для проброса портов. Сделаем это сами и всего лишь одной строкой для каждой цепочки:

sudo iptables -t nat -A DOCKER ! -i docker0 -p tcp -m multiport --dports 3000:3005 -j DNAT --to-destination 192.168.10.14 sudo iptables -A FORWARD -d 192.168.10.14 ! -i docker0 -o docker0 -p tcp -m multiport --dports 3000:3005 -j ACCEPT 

Маршрутизация в контейнер

Можно сделать всё на много проще. На сетевом интерфейсе хост-системы уже включен ip_forward (это требуется при первичной настройке docker ) и этого достаточно.

Допустим на хост-системе есть 2 сетевых интерфейса с ip-адресами: 10.10.10.10 и 172.172.172.172. Для доступа к контейнерам из внешней сети достаточно на роутерах во внешней сети указать маршрут на сеть docker через один из ip адресов хост-системы. Но тут встает другая проблема — закрепить IP-адрес за контейнером.

Для назначения статическиого адреса контейнеру создадим контейнер со своей сетевой подсистемой, но никак не связанной с хостовой. Для этого используется ключ —net=none :

docker run --name ubuntu_none --net=none -d -i -t ubuntu /bin/bash 

Далее, следуя инструкциям из документации назначим IP-адрес контейнеру:

# Узнать PID контейнера $ docker inspect -f '>' ubuntu_none 22894 $ pid=22894 $ sudo mkdir -p /var/run/netns $ sudo ln -s /proc/$pid/ns/net /var/run/netns/$pid # Узнать IP-адрес и маску на интерфейсе docker0 $ ip addr show docker0 20: docker0: . inet 192.168.10.1/28 scope global docker0 # Создание пары интерфейсов pbx1br0 и cont1 с последующей # привязкой pbx1br0 к бриджу и его "поднятие" $ sudo ip link add pbx1br0 type veth peer name cont1 $ sudo brctl addif docker0 pbx1br0 $ sudo ip link set pbx1br0 up # Поместим cont1 в контейнер, переименуем в eth0 # и включим с нобходимым IP-адресом $ sudo ip link set cont1 netns $pid $ sudo ip netns exec $pid ip link set dev cont1 name eth0 $ sudo ip netns exec $pid ip link set eth0 address 12:34:56:78:9a:bc $ sudo ip netns exec $pid ip link set eth0 up $ sudo ip netns exec $pid ip addr add 192.168.10.2/28 dev eth0 $ sudo ip netns exec $pid ip route add default via 192.168.10.1 

Теперь внутри контейнера ubuntu_none установлен IP-адрес 192.168.10.2. Таким образом был назначен необходимый IP-адрес.

Скрипт для запуска контейнеров со постоянным IP-адресом

Эта часть не совсем относится к настройке сети в контейнерах docker , а скорее к автоматизации описанного ранее.

Почитать подробнее про ностройку сети в docker можно тут — Network Configuration.

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

Как правильно настроить маршрутизацию Docker в OpenVPN сети?

Добрый день! Задача организовать связь между сетями докер через частную сеть openVPN. На сервере создается контейнер с установленным в него OpenVPN в качестве сервера и создается в докер дополнительная сеть с адресами 192.168.2.0/24. К этой сети подключаются сам сервер OpenVpn и другие докер контейнеры. Далее на другой машине в Docker создается клиент OpenVpn, который подключается к серверу. У клиента так же есть отдельная докер сеть 192.168.3.0/24. Так вот связь между клиентом и сервером сделал все работает. Пингуются и сервер и клиент между собой и из других контейнеров как клиента так и сервера, а вот сами другие контейнеры (не сервер и не клиент) не доступны. Нужно сделать доступ ко всем контейнерам в сетей 192.168.3.0 и 192.168.2.0

Файл настроек сервера port 1194 proto udp dev tun ca /etc/openvpn/keys/ca.crt cert /etc/openvpn/keys/server.crt key /etc/openvpn/keys/server.key dh /etc/openvpn/keys/dh.pem server 192.168.50.0 255.255.255.0 client-config-dir /etc/openvpn/ccd ifconfig-pool-persist /etc/openvpn/ipp.txt cipher AES-256-GCM ncp-ciphers AES-256-GCM:AES-256-CBC auth SHA512 tls-auth /etc/openvpn/keys/ta.key 1 tls-server tls-version-min 1.2 key-direction 0 keepalive 10 60 persist-key persist-tun client-to-client keepalive 10 120 comp-lzo no max-clients 10 persist-key persist-tun route-method exe status /etc/openvpn/log/openvpn-status.log log /etc/openvpn/log/openvpn.log verb 3 mute 20 route 192.168.3.0 255.255.255.0 route 192.168.2.0 255.255.25O.0 push «route 192.168.3.0 255.255.255.0» push «route 192.168.2.0 255.255.255.0» push «route 192.168.50.0 255.255.255.0»
Маршруты сервера default via 192.168.2.1 dev eth0 192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.2 192.168.3.0/24 via 192.168.50.2 dev tun0 192.168.50.0/24 via 192.168.50.2 dev tun0 192.168.50.2 dev tun0 proto kernel scope link src 192.168.50.1

Т.е. пинугется сам сервер с клиента 192.168.3.2 -> 192.168.2.2 и обратно. Также проходит пинг от 192.168.3.3 -> 192.168.3.2 при добавлении ему маршрута
ip route add 192.168.3.0/24 via 192.168.2.2
Не работает ping от 192.168.3.2 -> 192.168.3.3
Не работает ping 192.168.2.2 до 192.168.3.3

  • Вопрос задан более двух лет назад
  • 280 просмотров

Routing and Docker

Добрый день! На системе Debian установлен сервис docker 17.12.1-ce и имеются 2 интерфейса с одинаковыми gateway в одной сети, работающих через 2 таблицы описанных в /etc/iptoute2/rt_tables — T1, T2:

iface eth0 inet static address 10.10.6.2 netmask 255.255.252.0 # hook scripts post-up ip route add default via '10.10.6.1' dev eth0 table T1 post-up ip rule add from '10.10.6.2' table T1 post-up ip rule add to '10.10.6.2' table T1 post-up ip route add default via '10.10.6.1' metric 101 dev eth0 post-down ip rule del from 0/0 to 0/0 table T1 post-down ip rule del from 0/0 to 0/0 table T1 

второй интерфейс настроен аналогично, при поднятии образа докера поднимается мост docker0 и в системе добавляется маршрут:

172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 

и из докера наружу ходят все пакеты нормально, за исключением интерфейсов хоста: eth0 и eth1, на ip адреса этих интерфейсов пакеты пропадают сразу после 172.17.0.1. На ip докера все пакеты тоже проходят нормально. Решение добавить маршрут в таблицу

ip route add 172.17.0.0/16 dev docker0 table T1 
  1. насколько это правильное решение?
  2. как сделать так чтобы при поднятии бриджа сервис докера сам добавлял нужный маршрут?

Сеть контейнеров — это не сложно

Работа с контейнерами многим кажется волшебством, пришло время разобраться как работает сеть контейнеров. Мы покажем на примерах, что это совсем не сложно. Помните, что контейнеры — всего лишь изолированные процессы Linux.

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

  • Как виртуализировать сетевые ресурсы, чтобы контейнеры думали, что у каждого из них есть выделенный сетевой стек?
  • Как превратить контейнеры в дружелюбных соседей, не дать им мешать друг другу и научить хорошо общаться?
  • Как настроить сетевой доступ из контейнера во внешний мир (например, в Интернет)?
  • Как получить доступ к контейнерам, работающим на сервере, из внешнего мира (публикация портов)?

Отвечая на эти вопросы, мы настроим сеть контейнеров с нуля, используя стандартные инструменты Linux. В результате станет очевидно, что сеть контейнеров — это не что иное, как простая комбинация хорошо известных возможностей Linux:

  • Network namespaces
  • Virtual Ethernet devices (veth)
  • Virtual network switches (bridge)
  • IP маршрутизация и преобразование сетевых адресов (NAT)

Нам потребуется немного сетевой магии и никакого кода .

С чего начать?

Все примеры в статье были сделаны на виртуальной машине CentOS 8. Но вы можете выбрать тот дистрибутив, который вам нравится.

Создадим виртуальную машину с помощью Vagrant и подключимся к ней по SSH:

$ vagrant init centos/8 $ vagrant up $ vagrant ssh [vagrant@localhost ~]$ uname -a Linux localhost.localdomain 4.18.0-147.3.1.el8_1.x86_64

Мы не будем использовать какое-либо популярное решение для контейнеризации (например, docker или podman). Вместо этого мы сосредоточимся на основных концепциях и воспользуемся минимальным набором инструментов для достижения наших учебных целей.

Изоляция контейнеров с помощью Network namespaces

Что составляет сетевой стек Linux? Ну, очевидно, набор сетевых устройств. Что еще? Набор правил маршрутизации. И не забываем про настройку netfilter, создадим необходимые правила iptables.

Напишем небольшой скрипт inspect-net-stack.sh :

#!/usr/bin/env bash echo "> Network devices" ip link echo -e "\n> Route table" ip route echo -e "\n> Iptables rules" iptables --list-rules

Но прежде, чем запускать его, давайте внесём изменения в правила iptables, чтобы идентифицировать их в дальнейшем:

$ sudo iptables -N ROOT_NS
$ sudo ./inspect-net-stack.sh > Network devices 1: lo: 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: mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff > Route table default via 10.0.2.2 dev eth0 proto dhcp metric 100 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100 > Iptables rules -P INPUT ACCEPT -P FORWARD ACCEPT -P OUTPUT ACCEPT -N ROOT_NS

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

Мы уже упоминали об одном из Linux namespaces, используемых для изоляции контейнеров, которое называет сетевое пространство имён (Network namespace). Если заглянуть в man ip-netns , то мы прочтём, что «Network namespace логически является копией сетевого стека со своими собственными маршрутами, правилами брандмауэра и сетевыми устройствами». Мы не будем затрагивать другие Linux namespaces в этой статье и ограничимся только областью видимости сетевого стека.

Для создания Network namespace нам достаточно утилиты ip , которая входим в популярный пакет iproute2 . Создадим новое сетевое пространство имён:

$ sudo ip netns add netns0 $ ip netns netns0

Новое сетевое пространство имён создано, но как начать его использовать? Воспользуемся командой Linux под названием nsenter . Она осуществляет вход в одно или несколько указанных пространств имен, а затем выполняет в нём указанную программу:

$ sudo nsenter --net=/var/run/netns/netns0 bash # The newly created bash process lives in netns0 $ sudo ./inspect-net-stack.sh > Network devices 1: lo: 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 > Route table > Iptables rules -P INPUT ACCEPT -P FORWARD ACCEPT -P OUTPUT ACCEPT

Приведённый выше пример показывает, что процесс bash, работающий внутри пространства имён netns0 , видит совершенно другой сетевой стек. Отсутствуют правила маршрутизации, и правила iptables, есть только один loopback interface. Все идет по плану.

Подключаем контейнер к хосту через virtual Ethernet devices (veth)

Выделенный сетевой стек будет бесполезен, если к нему отсутствует доступ. К счастью, Linux предоставляет подходящее средство для этого — virtual Ethernet devices (veth)! Согласно man veth , «veth-device — это виртуальные устройства Ethernet. Они работают как туннели между сетевыми пространствами имён для создания моста к физическому сетевому устройству в другом пространстве имён, а также могут использоваться как автономные сетевые устройства».

Виртуальные Ethernet устройства всегда работают парами. Создадим их прямо сейчас:

$ sudo ip link add veth0 type veth peer name ceth0

С помощью этой единственной команды мы только что создали пару взаимосвязанных виртуальных Ethernet устройств. Имена veth0 и ceth0 были выбраны произвольно:

$ ip link 1: lo: 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: mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff 5: ceth0@veth0: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff 6: veth0@ceth0: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff

И veth0 , и ceth0 после создания находятся в сетевом стеке хоста (также называемом Root Network namespace). Чтобы связать корневое пространство имён с пространством имён netns0 , нам нужно сохранить одно из устройств в корневом пространстве имён и переместить другое в netns0 :

$ sudo ip link set ceth0 netns netns0 # List all the devices to make sure one of them disappeared from the root stack $ ip link 1: lo: 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: mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff 6: veth0@if5: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 link/ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff link-netns netns0

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

$ sudo ip link set veth0 up $ sudo ip addr add 172.18.0.11/16 dev veth0

Продолжим с netns0 :

$ sudo nsenter --net=/var/run/netns/netns0 $ ip link set lo up # whoops $ ip link set ceth0 up $ ip addr add 172.18.0.10/16 dev ceth0 $ ip link 1: lo: 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 5: ceth0@if6: mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000 link/ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff link-netnsid 0

# From netns0, ping root's veth0 $ ping -c 2 172.18.0.11 PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data. 64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.038 ms 64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.040 ms --- 172.18.0.11 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 58ms rtt min/avg/max/mdev = 0.038/0.039/0.040/0.001 ms # Leave netns0 $ exit # From root namespace, ping ceth0 $ ping -c 2 172.18.0.10 PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data. 64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.073 ms 64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.046 ms --- 172.18.0.10 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 3ms rtt min/avg/max/mdev = 0.046/0.059/0.073/0.015 ms

Обратите внимание, если мы попытаемся проверить доступность любых других адресов из пространства имен netns0, у нас ничего не получится:

# Inside root namespace $ ip addr show dev eth0 2: eth0: mtu 1500 qdisc fq_codel state UP group default qlen 1000 link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute eth0 valid_lft 84057sec preferred_lft 84057sec inet6 fe80::5054:ff:fee3:2777/64 scope link valid_lft forever preferred_lft forever # Remember this 10.0.2.15 $ sudo nsenter --net=/var/run/netns/netns0 # Try host's eth0 $ ping 10.0.2.15 connect: Network is unreachable # Try something from the Internet $ ping 8.8.8.8 connect: Network is unreachable

Для таких пакетов в таблице маршрутизации netns0 просто нет маршрута. В настоящий момент существует единственный маршрут до сети 172.18.0.0/16:

# From netns0 namespace: $ ip route 172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10

В Linux есть несколько способов заполнения таблицы маршрутизации. Один из них — извлечение маршрутов из подключенных напрямую сетевых интерфейсов. Помните, что таблица маршрутизации в netns0 была пустой сразу после создания пространства имен. Но затем мы добавили туда устройство ceth0 и присвоили ему IP-адрес 172.18.0.10/16 . Поскольку мы использовали не простой IP-адрес, а комбинацию адреса и сетевой маски, сетевому стеку удалось извлечь из него информацию о маршрутизации. Каждый пакет, предназначенный для сети 172.18.0.0/16 , будет отправлен через устройство ceth0 . Но все остальные пакеты будут отброшены. Точно так же есть новый маршрут в корневом пространстве имен:

# From root namespace: $ ip route # . omitted lines . 172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11

На этом этапе мы ответили на первый вопрос. Теперь мы знаем, как изолировать, виртуализировать и подключать сетевые стеки Linux.

Объединение контейнеров с помощью virtual network switch (bridge)

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

# From root namespace $ sudo ip netns add netns1 $ sudo ip link add veth1 type veth peer name ceth1 $ sudo ip link set ceth1 netns netns1 $ sudo ip link set veth1 up $ sudo ip addr add 172.18.0.21/16 dev veth1 $ sudo nsenter --net=/var/run/netns/netns1 $ ip link set lo up $ ip link set ceth1 up $ ip addr add 172.18.0.20/16 dev ceth1
# From netns1 we cannot reach the root namespace! $ ping -c 2 172.18.0.21 PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data. From 172.18.0.20 icmp_seq=1 Destination Host Unreachable From 172.18.0.20 icmp_seq=2 Destination Host Unreachable --- 172.18.0.21 ping statistics --- 2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 55ms pipe 2 # But there is a route! $ ip route 172.18.0.0/16 dev ceth1 proto kernel scope link src 172.18.0.20 # Leaving netns1 $ exit # From root namespace we cannot reach the netns1 $ ping -c 2 172.18.0.20 PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data. From 172.18.0.11 icmp_seq=1 Destination Host Unreachable From 172.18.0.11 icmp_seq=2 Destination Host Unreachable --- 172.18.0.20 ping statistics --- 2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 23ms pipe 2 # From netns0 we CAN reach veth1 $ sudo nsenter --net=/var/run/netns/netns0 $ ping -c 2 172.18.0.21 PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data. 64 bytes from 172.18.0.21: icmp_seq=1 ttl=64 time=0.037 ms 64 bytes from 172.18.0.21: icmp_seq=2 ttl=64 time=0.046 ms --- 172.18.0.21 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 33ms rtt min/avg/max/mdev = 0.037/0.041/0.046/0.007 ms # But we still cannot reach netns1 $ ping -c 2 172.18.0.20 PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data. From 172.18.0.10 icmp_seq=1 Destination Host Unreachable From 172.18.0.10 icmp_seq=2 Destination Host Unreachable --- 172.18.0.20 ping statistics --- 2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 63ms pipe 2

Что-то пошло не так. По какой-то причине мы не можем подключиться из netns1 к root namespace . А из root namespace мы не можем подключиться к netns1. Однако, поскольку оба контейнера находятся в одной IP-сети 172.18.0.0/16 , есть доступ к veth1 хоста из контейнера netns0 . Интересно.

Возможно, мы столкнулись с конфликтом маршрутов. Давайте проверим таблицу маршрутизации в root namespace :

$ ip route # . omitted lines . 172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11 172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21

После добавления второй пары veth в таблице маршрутизации root namespace появился новый маршрут 172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21 , но маршрут до этой подсети уже существовал! Когда второй контейнер пытается проверить связь с устройством veth1 , используется первый маршрут и мы видим ошибку подключения. Если бы мы удалили первый маршрут sudo ip route delete 172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11 и перепроверили подключение, то увидели бы обратную ситуацию, то есть подключение netns1 будет восстановлено, но netns0 останется в подвешенном состоянии.

Пожалуй, если бы мы выбрали другую подсеть для netns1 , все бы заработало. Однако наличие нескольких контейнеров в одной подсети является допустимым вариантом применения. Попробуем разобраться .

Рассмотрим Linux Bridge — еще один виртуализированный сетевой объект! Linux Bridge ведёт себя как коммутатор. Он пересылает пакеты между подключенными к нему интерфейсами. А поскольку это коммутатор, то он работает на уровне L2 (то есть Ethernet).

Чтобы предыдущие этапы нашего эксперимента в дальнейшем не вносили путаницы, удалим существующие сетевые пространства имён:

$ sudo ip netns delete netns0 $ sudo ip netns delete netns1 # But if you still have some leftovers. $ sudo ip link delete veth0 $ sudo ip link delete ceth0 $ sudo ip link delete veth1 $ sudo ip link delete ceth1

Заново создаём два контейнера. Обратите внимание, мы не назначаем IP-адреса новым устройствам veth0 и veth1 :

$ sudo ip netns add netns0 $ sudo ip link add veth0 type veth peer name ceth0 $ sudo ip link set veth0 up $ sudo ip link set ceth0 netns netns0 $ sudo nsenter --net=/var/run/netns/netns0 $ ip link set lo up $ ip link set ceth0 up $ ip addr add 172.18.0.10/16 dev ceth0 $ exit $ sudo ip netns add netns1 $ sudo ip link add veth1 type veth peer name ceth1 $ sudo ip link set veth1 up $ sudo ip link set ceth1 netns netns1 $ sudo nsenter --net=/var/run/netns/netns1 $ ip link set lo up $ ip link set ceth1 up $ ip addr add 172.18.0.20/16 dev ceth1 $ exit

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

$ ip route default via 10.0.2.2 dev eth0 proto dhcp metric 100 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100

И, наконец, создадим bridge интерфейс:

$ sudo ip link add br0 type bridge $ sudo ip link set br0 up

Теперь подключим к нему veth0 и veth1 :

$ sudo ip link set veth0 master br0 $ sudo ip link set veth1 master br0

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

$ sudo nsenter --net=/var/run/netns/netns0 $ ping -c 2 172.18.0.20 PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data. 64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.259 ms 64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.051 ms --- 172.18.0.20 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 2ms rtt min/avg/max/mdev = 0.051/0.155/0.259/0.104 ms
$ sudo nsenter --net=/var/run/netns/netns1 $ ping -c 2 172.18.0.10 PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data. 64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.037 ms 64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.089 ms --- 172.18.0.10 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 36ms rtt min/avg/max/mdev = 0.037/0.063/0.089/0.026 ms

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

$ sudo nsenter --net=/var/run/netns/netns0 $ ip neigh 172.18.0.20 dev ceth0 lladdr 6e:9c:ae:02:60:de STALE $ exit $ sudo nsenter --net=/var/run/netns/netns1 $ ip neigh 172.18.0.10 dev ceth1 lladdr 66:f3:8c:75:09:29 STALE $ exit

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

Настраиваем сетевой доступ из контейнера во внешний мир (IP routing and masquerading)

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

$ sudo nsenter --net=/var/run/netns/netns0 $ ping 10.0.2.15 # eth0 address connect: Network is unreachable

Интерфейс eth0 не доступен. Всё очевидно, в netns0 отсутствует маршрут для этого подключения:

$ ip route 172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10

Корневое пространство имён также не может взаимодействовать с контейнерами:

# Use exit to leave netns0 first: $ ping -c 2 172.18.0.10 PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data. From 213.51.1.123 icmp_seq=1 Destination Net Unreachable From 213.51.1.123 icmp_seq=2 Destination Net Unreachable --- 172.18.0.10 ping statistics --- 2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms $ ping -c 2 172.18.0.20 PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data. From 213.51.1.123 icmp_seq=1 Destination Net Unreachable From 213.51.1.123 icmp_seq=2 Destination Net Unreachable --- 172.18.0.20 ping statistics --- 2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms

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

$ sudo ip addr add 172.18.0.1/16 dev br0

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

$ ip route # . omitted lines . 172.18.0.0/16 dev br0 proto kernel scope link src 172.18.0.1 $ ping -c 2 172.18.0.10 PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data. 64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.036 ms 64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.049 ms --- 172.18.0.10 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 11ms rtt min/avg/max/mdev = 0.036/0.042/0.049/0.009 ms $ ping -c 2 172.18.0.20 PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data. 64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.059 ms 64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.056 ms --- 172.18.0.20 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 4ms rtt min/avg/max/mdev = 0.056/0.057/0.059/0.007 ms

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

$ sudo nsenter --net=/var/run/netns/netns0 $ ip route add default via 172.18.0.1 $ ping -c 2 10.0.2.15 PING 10.0.2.15 (10.0.2.15) 56(84) bytes of data. 64 bytes from 10.0.2.15: icmp_seq=1 ttl=64 time=0.036 ms 64 bytes from 10.0.2.15: icmp_seq=2 ttl=64 time=0.053 ms --- 10.0.2.15 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 14ms rtt min/avg/max/mdev = 0.036/0.044/0.053/0.010 ms # And repeat the change for netns1

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

Отлично, нам удалось добиться сетевой связности контейнеров с корневым пространством имён. Теперь давайте попробуем подключить их к внешнему миру. По умолчанию переадресация пакетов (ip packet forwarding), то есть функциональность маршрутизатора в Linux отключена. Нам нужно её включить

# In the root namespace sudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'

Теперь самое интересное — проверка подключения:

$ sudo nsenter --net=/var/run/netns/netns0 $ ping 8.8.8.8 # hangs indefinitely long for me. 

Всё равно не работает. Мы что-то упустили? Если бы контейнер отправлял пакеты во внешний мир, сервер-получатель не смог бы отправлять пакеты обратно в контейнер, потому что IP-адрес контейнера является частным и правила маршрутизации для этого конкретного IP-адреса известны только в локальной сети. К тому же многие контейнеры в мире имеют один и тот же частный IP-адрес 172.18.0.10. Решение этой проблемы называется преобразованием сетевых адресов (NAT). Принцип работы, следующий — перед отправкой во внешнюю сеть пакеты, отправленные контейнерами, заменяют свои исходные IP-адреса (source IP addesses) на адрес внешнего интерфейса хоста. Хост также будет отслеживать все существующие сопоставления (mapping) и по прибытии будет восстанавливать IP-адреса перед пересылкой пакетов обратно в контейнеры. Звучит сложно, но у меня для вас хорошие новости! Нам нужна всего одна команда, чтобы добиться требуемого результата:

$ sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/16 ! -o br0 -j MASQUERADE

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

$ sudo nsenter --net=/var/run/netns/netns0 $ ping -c 2 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=61 time=43.2 ms 64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=36.8 ms --- 8.8.8.8 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 2ms rtt min/avg/max/mdev = 36.815/40.008/43.202/3.199 ms
$ sudo nsenter --net=/var/run/netns/netns0 $ ping -c 2 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=61 time=43.2 ms 64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=36.8 ms --- 8.8.8.8 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 2ms rtt min/avg/max/mdev = 36.815/40.008/43.202/3.199 ms

Помните, что политика iptables по умолчанию — ACCEPT для каждой цепочки, она может быть довольно опасной в реальных условиях:

sudo iptables -S -P INPUT ACCEPT -P FORWARD ACCEPT -P OUTPUT ACCEPT

В качестве хорошего примера Docker вместо этого ограничивает все по умолчанию, а затем разрешает только для известных маршрутов:

$ sudo iptables -t filter --list-rules -P INPUT ACCEPT -P FORWARD DROP -P OUTPUT ACCEPT -N DOCKER -N DOCKER-ISOLATION-STAGE-1 -N DOCKER-ISOLATION-STAGE-2 -N DOCKER-USER -A FORWARD -j DOCKER-USER -A FORWARD -j DOCKER-ISOLATION-STAGE-1 -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -o docker0 -j DOCKER -A FORWARD -i docker0 ! -o docker0 -j ACCEPT -A FORWARD -i docker0 -o docker0 -j ACCEPT -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 5000 -j ACCEPT -A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2 -A DOCKER-ISOLATION-STAGE-1 -j RETURN -A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP -A DOCKER-ISOLATION-STAGE-2 -j RETURN -A DOCKER-USER -j RETURN $ sudo iptables -t nat --list-rules -P PREROUTING ACCEPT -P INPUT ACCEPT -P POSTROUTING ACCEPT -P OUTPUT ACCEPT -N DOCKER -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE -A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 5000 -j MASQUERADE -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER -A DOCKER -i docker0 -j RETURN -A DOCKER ! -i docker0 -p tcp -m tcp --dport 5005 -j DNAT --to-destination 172.17.0.2:5000 $ sudo iptables -t mangle --list-rules -P PREROUTING ACCEPT -P INPUT ACCEPT -P FORWARD ACCEPT -P OUTPUT ACCEPT -P POSTROUTING ACCEPT $ sudo iptables -t raw --list-rules -P PREROUTING ACCEPT -P OUTPUT ACCEPT

Настроим сетевой доступ из внешнего мира в контейнеры (port publishing)

Публикация портов контейнеров для некоторых (или всех) интерфейсов хоста — популярная практика. Но что на самом деле означает публикация порта?

Представьте, что у нас есть сервис, работающий внутри контейнера:

$ sudo nsenter --net=/var/run/netns/netns0 $ python3 -m http.server --bind 172.18.0.10 5000

Если мы попытаемся отправить HTTP-запрос этому сервису с хоста, все будет работать (ну, есть связь между корневым пространством имён и всеми интерфейсами контейнера, почему бы и нет?):

# From root namespace $ curl 172.18.0.10:5000 # . omitted lines . 

Однако, если бы мы получили доступ к этому серверу из внешнего мира, какой IP-адрес мы бы использовали? Единственный IP-адрес, который мы можем знать, — это адрес внешнего интерфейса хоста eth0 :

$ curl 10.0.2.15:5000 curl: (7) Failed to connect to 10.0.2.15 port 5000: Connection refused

Таким образом, нам нужно найти способ перенаправить все пакеты, поступающие на порт 5000 интерфейса eth0 хоста, на адрес 172.18.0.10:5000 . Или, другими словами, нам нужно опубликовать порт 5000 контейнера на интерфейсе eth0 хоста.

# External traffic sudo iptables -t nat -A PREROUTING -d 10.0.2.15 -p tcp -m tcp --dport 5000 -j DNAT --to-destination 172.18.0.10:5000 # Local traffic (since it doesn't pass the PREROUTING chain) sudo iptables -t nat -A OUTPUT -d 10.0.2.15 -p tcp -m tcp --dport 5000 -j DNAT --to-destination 172.18.0.10:5000

Кроме того, нам нужно включить iptables intercepting traffic over bridged networks (перехватывать трафик bridged networks):

sudo modprobe br_netfilter
curl 10.0.2.15:5000 # . omitted lines . 

Разбираемся в работе Docker network drivers

Но что же вам сделать теперь со всеми этими бесполезными знаниями? Например, мы могли бы попытаться разобраться в некоторых сетевых режимах Docker!

Начнем с режима —network host . Попробуйте сравнить вывод следующих команд ip link и sudo docker run -it —rm —network host alpine ip link . Сюрприз, они совпадут! Таким образом host mode Docker просто не использует изоляцию сетевого пространства имён и контейнеры работают в корневом сетевом пространстве имён и совместно используют сетевой стек с хост-системой.

Следующий режим, который нужно проверить, — это —network none . Вывод команды sudo docker run -it —rm —network none alpine ip link показывает только один сетевой интерфейс обратной loopback. Это очень похоже на наши наблюдения за только что созданным сетевым пространством имен. То есть до того момента, когда мы добавляли какие-либо veth устройства.

И последнее, но не менее важное: режим —network bridge (по умолчанию), это именно то, что мы пытались воспроизвести в этой статье.

Сети и rootless контейнеры

Одной из приятных особенностей диспетчера контейнеров podman является его ориентация на rootless контейнеры. Однако, как вы, вероятно, заметили, в этой статье мы использовали много эскалаций sudo и без root-прав настроить сеть невозможно. При настройке сетей rootful контейнеров Podman очень близок к Docker. Но когда дело доходит до rootless контейнеров, Podman полагается на проект slirp4netns:

Начиная с Linux 3.8, непривилегированные пользователи могут создавать network_namespaces (7) вместе с user_namespaces (7). Однако непривилегированные сетевые пространства имен оказались не очень полезными, потому что для создания пар veth (4) в пространствах имен хоста и сети по-прежнему требуются привилегии root (иначе доступ в Интернету будет отсутствовать).

slirp4netns позволяет получить доступ из сетевое пространства имен в Интернет непривилегированным пользователям, подключая устройство TAP в сетевом пространстве имен к стеку TCP/IP usermode («slirp»).

Сеть rootless контейнера весьма ограничена: «технически сам контейнер не имеет IP-адреса, потому что без привилегий root невозможно настроить сетевое устройство. Более того, проверка связи (ping) из rootless контейнера не работает, поскольку в нем отсутствует функция безопасности CAP_NET_RAW, которая необходима для работы команды ping.

Заключение

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *