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

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

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

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

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

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

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

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

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

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

docker run -p 3306:3306 mysql

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

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

docker port <containerId>

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

Проброс группы портов был востребован очень давно, но был добавлен в исходный код только 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 '{{ .State.Pid }}' 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.

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

comments powered by Disqus