Сети Docker изнутри: как Docker использует iptables и интерфейсы Linux

в 14:56, , рубрики: devops, docker, docker swarm, iptables, Сетевые технологии, системное администрирование

Я познакомился с Docker довольно давно и, как и большинство его пользователей, был мгновенно очарован его мощью и простотой использования. Простота является основным столпом, на котором основывается Docker, чья сила кроется в легких CLI-командах. Когда я изучал Docker, я захотел выяснить, что происходит у него в бэкграунде, как вообще все происходит, особенно что касается работы с сетью (для меня это одна из самых интересных областей).

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

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

Оглавление

Обзор сетей Docker

Сеть Docker построена на Container Network Model (CNM), которая позволяет кому угодно создать свой собственный сетевой драйвер. Таким образом, у контейнеров есть доступ к разным типам сетей и они могут подключаться к нескольким сетям одновременно. Помимо различных сторонних сетевых драйверов, у самого Docker-а есть 4 встроенных:

  • Bridge: в этой сети контейнеры запускаются по умолчанию. Связь устанавливается через bridge-интерфейс на хосте. У контейнеров, которые используют одинаковую сеть, есть своя собственная подсеть, и они могут передавать данные друг другу по умолчанию.
  • Host: этот драйвер дает контейнеру доступ к собственному пространству хоста (контейнер будет видеть и использовать тот же интерфейс, что и хост).
  • Macvlan: этот драйвер дает контейнерам прямой доступ к интерфейсу и суб-интерфейсу (vlan) хоста. Также он разрешает транкинг.
  • Overlay: этот драйвер позволяет строить сети на нескольких хостах с Docker (обычно на Docker Swarm кластере). У контейнеров также есть свои адреса сети и подсети, и они могут напрямую обмениваться данными, даже если они располагаются физически на разных хостах.

Сетевые драйвера Bridge и Overlay, возможно, используются чаще всего, поэтому в этой статье я буду больше уделять им внимание.

Сети типа мост (bridge)

По умолчанию для контейнеров используется bridge. При первом запуске контейнера Docker создает дефолтную bridge-сеть с одноименным названием. Эту сеть можно увидеть в общем списке по команде docker network ls:

docker network ls

Чтобы проинспектировать ее свойства, запустим команду docker network inspect bridge:

docker network inspect bridge

Вы также можете создать свои собственные bridge-сети при помощи команды docker network create, указав опцию --driver bridge.

Например, команда docker network create --driver bridge --subnet 192.168.100.0/24 --ip-range 192.168.100.0/24 my-bridge-network создает еще одну bridge-сеть с именем “my-bridge-network” и подсетью 192.168.100.0/24.

Bridge-интерфейсы в Linux

Каждая bridge-сеть имеет свое представление в виде интерфейса на хосте. С сетью “bridge”, которая стоит по умолчанию, обычно ассоциируется интерфейс docker0, и с каждой новой сетью, которая создается при помощи команды docker network create, будет ассоциироваться свой собственный новый интерфейс.

ifconfig docker0

Чтобы найти интерфейс, который ассоциируется с сетью, которую вы создали, введите команду ifconfig, чтобы вывести все интерфейсы, а затем найти тот интерфейс, который относится к созданной вами подсети. Например, если нам надо найти интерфейс для сети my-bridge-network, которую мы только что создали, то можно запустить такую команду:

ifconfig

Bridge-интерфейсы Linux похожи на свичи тем, что они присоединяют несколько интерфесов к одной подсети и перенаправляют трафик на основе MAC-адресов. Как будет видно ниже, у каждого контейнера, привязанного к bridge-сети, будет свой собственный виртуальный интерфейс на хосте, и все контейнеры в одной сети будут привязаны к одному интерфейсу, что позволит им передавать друг другу данные. Можно получить больше данных о статусе моста при помощи утилиты brctl:

brctl

Как только мы запустим контейнеры и привяжем их к этой сети, интерфейс каждого из этих контейнеров будет выведен в списке в отдельной колонке. А если включить захват трафика в bridge-интерфейсе, то можно увидеть, как передаются данные между контейнерами в одной подсети.

Виртуальные интерфейсы Linux

Container Networking Model дает каждому контейнеру свое собственное сетевое пространство. Если запустить команду ifconfig внутри контейнера, то можно увидеть его интерфейсы такими, какими их видит сам контейнер:

ifconfig

Впрочем, eth0, который представлен в этом примере, можно увидеть только изнутри контейнера, а снаружи, на хосте, Docker создает соответствующую копию виртуального интерфейса, которая служит связью с внешним миром. Затем эти виртуальные интерфейсы соединяются с bridge-интерфейсами, о которых мы говорили выше, чтобы легче установить связь между разными контейнерами в одной подсети.

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

До запуска каких-либо контейнеров у bridge-интерфейса docker0 нет никаких других присоединенных интерфейсов:

docker0

Затем я запустил два контейнера на образе ubuntu:14.04:

docker ps

Сразу стало видно, что два интерфейса присоединены к bridge-интерфейсу docker0 (по одному на каждый контейнер):

sudo brctl show docker0

Если начать пинговать Google с одного из контейнеров, то захват трафика с хоста на виртуальном интерфейсе контейнера покажет нам трафик контейнеров:

ping google.com

Аналогично можно выполнить пинг от одного контейнера к другому.

Во-первых, надо получить IP-адрес контейнера. Это можно сделать либо при помощи команды ifconfig, либо при помощи docker inspect, что позволяет проинспектировать контейнер:

docker inspect

Затем начинаем пинг от одного контейнера к другому:

docker exec ping

Чтобы увидеть трафик с хоста, мы можем сделать захват на любом из виртуальных интерфейсов, которые соотносятся с контейнерами, либо на bridge-интерфейсе (в данном случае docker0), что покажет нам все коммуникации внутри контейнеров данной подсети:

sudo tcpdump

Находим Veth-интерфейс в контейнере

Если вы хотите узнать, какой veth-интерфейс хоста привязан к интерфейсу внутри контейнера, то простого способа вы не найдете. Однако, есть несколько методов, которые можно найти на разных форумах и в обсуждениях на github. Самый простой, на мой взгляд, способ я почерпнул из этого обсуждения на github, немного его изменив. Он зависит от того, присутствует ли ethtool в контейнере.

Например, у меня в системе запущены 3 контейнера:

docker ps

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

docker exec

Затем на хосте я использую peer_ifindex, чтобы узнать имя интерфейса:

sudo ip link

В данном случае интерфейс называется veth7bd3604.

iptables

Docker использует linux iptables, чтобы контролировать коммуникации между интерфейсами и сетями, которые он создает. Linux iptables состоят из разных таблиц, но нам в первую очередь интересны только две из них: filter и nat. Таблица filter содержит правила безопасности, которые решают, допускать ли трафик к IP-адресам или портам. С помощью таблицы nat Docker дает контейнерам в bridge-сетях связываться с адресатами, которые находятся снаружи хоста (иначе пришлось бы добавлять маршруты к контейнерным сетям в сети хоста).

iptables:filter

Таблицы в iptables состоят из различных цепочек, которые соответствуют разным состояниям или стадиям обработки пакета на хосте. По умолчанию, у таблицы filter есть 3 цепочки:
Input для обработки входящих пакетов, предназначенных для того же хоста, на который они приходят;
Output для пакетов, возникающих на хосте, предназначенных для внешнего адресата;
Forward для обработки входящих пакетов, предназначенных для внешнего адресата.

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

Чтобы увидеть текущие правила цепочки и дефолтные установки в таблице filter, запустите команду iptables -t filter -L или iptables -L, если таблица filter используется по умолчанию и не указана никакая другая таблица:

sudo iptables -t filter -L

Жирным выделены разные цепочки и дефолтные установки для каждой из них (у кастомных цепочек дефолтных установок нет). Также видно, что Docker добавил две кастомные цепочки: Docker и Docker-Isolation, также добавил правила в цепочку Forward, целью которых являются эти две новые цепочки.

Цепочка Docker-isolation

Docker-isolation содержит правила, которые ограничивают доступ между разными сетями. Чтобы узнать подробности, добавляйте -v, когда запускаете iptables:

sudo iptables -t filter -L -v

Можно увидеть несколько правил DROP, которые блокируют трафик между всеми bridge-интерфейсами, которые создал Docker, и таким образом не дают сетям обмениваться данными.

icc=false

Одна из опций, которую можно передать команде docker network create, — это опция, которая отвечает за передачу данных внутри контейнера: com.docker.network.bridge.enable_icc. Если задать ей значение false, то передача данных между контейнерами внутри одной сети будет заблокирована. Для этого нужно добавить DROP-правило в цепочку forward, которое соответствует пакетам, приходящим от bridge-интерфейса, связанного с сетью для данного интерфейса.

Например, если создать новую сеть при помощи команды docker network create --driver bridge --subnet 192.168.200.0/24 --ip-range 192.168.200.0/24 -o "com.docker.network.bridge.enable_icc"="false" no-icc-network, то мы получим следующее:

ifconfig

iptables:nat

С помощью nat можно поменять IP-адрес или порт пакета. В данном случае он используется, чтобы за IP-адресом хоста спрятать адреса источников пакетов, которые приходят от bridge-сетей (например, хосты в подсети 172.18.0.0/24) и направлены во внешний мир. Эта фича контролируется опцией com.docker.network.bridge.enable_ip_masquerade, которую можно передать docker network create (если не задать ничего специфического, то по умолчанию будет значение true).

Результат этой команды можно увидеть в таблице nat:

sudo iptables -t nat -l

В этой цепочке postrouting можно увидеть все сети, которые созданы под действием masquerade, которое применяется, когда они передают данные любому хосту вне своей собственной сети.

Итог

  • У bridge-сети есть соответствующий bridge-интерфейс в Linux на хосте, который действует как layer2 свич и который соединяет разные контейнеры одной подсети.
  • У каждого интерфейса сети есть соответствующий виртуальный интерфейс на хосте, который создается во время работы контейнера.
  • Захват трафика с хоста на bridge-интерфейсе равноценен созданию SPAN-порта в свиче, в котором можно увидеть все внутренние коммуникации между контейнерами данной сети.
  • Захват трафика с хоста на виртуальном интерфейсе (veth-*) покажет весь трафик, исходящий из контейнера по конкретной подсети.
  • Правила iptables в цепочке filter используются, чтобы не давать разным сетям (и иногда еще хостам внутри сети) обмениваться данными. Эти правила обычно добавляют в цепочку Docker-isolation.
  • Контейнеры, которые обмениваются данными с внешним миром через bridge-интерфейс, прячут свой IP за адресом хоста. Для этого добавляются необходимые правила nat-таблицу в iptables.

Ссылки/ресурсы

Автор: Роман Моисеев

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js