Я познакомился с 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.

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


Поделиться с друзьями
-->

Комментарии (18)


  1. grossws
    21.07.2017 19:09
    +1

    Интересно было бы почитать про современные CNI-реализации (calico, cilium, flannel, weave).


    1. r-moiseev
      21.07.2017 20:16
      +1

      В планах еще есть статья про работу overlay сети в Swarm


      1. Faight
        21.07.2017 23:21

        Спасибо за хорошую статью. Отлично, overlay — логичное продолжение.


      1. neumeika
        22.07.2017 00:07
        +1

        А нет желания описать что-то, что не влияет на производительность сети (пропускную способность, задержку), что-то типа macvlan или ipvlan? В проде оно всё-таки посимпатичнее смотрится.


        1. r-moiseev
          22.07.2017 00:59

          Имеете ввиду по сравнению с bridge? Разница не фатальна (стр. 22).


          1. neumeika
            22.07.2017 03:50

            В сравнении с overlay, особенно swarm'овским (хотя может они за год и перепилили его) :)


  1. Estagus
    22.07.2017 15:31

    Было бы интересно услышать про недокументированный INPUT в таблице nat, и его особенности


    1. r-moiseev
      22.07.2017 15:39
      +2

      Появился вот тут

      Используется для всяких хитростей

      Действительно, на данный момент не задокументирован


      1. Estagus
        22.07.2017 19:19

        Могу ошибаться т.к. давно не проверял, но по памяти:
        После установки докера на хост появлялась цепочка INPUT в nat (до этого не было). Соотв, так как у нее дефолт в ACCEPT, то все правила в INPUT у filter тупо не работают и их приходилось «дублировать». При чем я не говорю о фильтрации чего-то «в контейнерах/namespace-ах». На хосте стоит nginx на 80м порту и надо было отфильтровать что-то.


        1. r-moiseev
          22.07.2017 19:33

          Ну, docker с высокой вероятностью не имеет к этому отношения. INPUT в nat появился как раз после того самого обновления.


          1. Estagus
            22.07.2017 19:41

            Ваша правда. Только что проверил на чистой обесдокеренной виртуалке, в nat присутствует INPUT. Видать упомянутый эффект встречался в более старых ядрах/дистрибутивах. Просто я достаточно сильно уверен что в iptables -t nat -nL -v появлялся INPUT после установки докера.

            На самом деле более раздражает описанный эффект с фильтрацией… Но я не готов повторять сейчас эксперимент чтобы проверить…


            1. SchmeL
              24.07.2017 14:09

              У себя на продакшен серверах отключаю iptables докера. (--iptables=false)
              Иначе — ломало, правила.
              А на сервере парой правил — прописываю nat для контейнеров.


              1. grossws
                24.07.2017 17:41

                Аналогично, правда связано это было в первую очередь с использованием firewalld с которым dockerовское управление iptables дружит не очень. Например, тот же fail2ban прекрасно умеет работать и с firewalld и с iptables-rules (вариант статического файервола, использующего iptables-save/iptables-restore).


  1. PavelVainerman
    22.07.2017 19:46

    Можно ли в docker сделать изолированную виртуальную сеть между группой контейнеров как в VirtualBox?
    Т.е. чтобы можно было запустить две и более таких групп контейнеров с одинаковыми ip, и соответственно
    эти сети друг-другу бы не мешали.


    1. r-moiseev
      22.07.2017 20:26

      Можно всё, кроме, боюсь, одинаковых ip. Пространство адресов на хосте одно, изоляция происходит на уровне iptables. Но дело в том, что по Docker-way вам и не нужно знать ip адреса контейнеров, ибо есть алиасы.


      1. PavelVainerman
        22.07.2017 20:38

        Проблема как раз в том, что для моего сценария нужно чтобы была возможно иметь одинаковые ip.
        Есть разрабатываемая (встраиваемая) система из нескольких узлов, у неё внутри относительно жёстко вшитые ip. Хотелось иметь возможность запускать для тестирования несколько таких систем на одном тестовом стенде.
        Сейчас для этого используется vagrant+virtualbox. Хотелось двинуть в сторону docker со всеми его плюшками.
        Но вот застряли. И не совсем ясно куда «копать». Возможно ли это принципиально с docker или нет.
        Может ли тут помочь Open vSwitch + vlan-ы?


        1. r-moiseev
          22.07.2017 21:01

          С Open vSwitch по идее должно быть возможно, драйвер есть.

          Но я честно сказать так глубоко не копал, не было нужды


          1. PavelVainerman
            22.07.2017 21:47
            +1

            Эх… хотелось проще. В Virtualbox это буквально одной галочкой. Но в любом случае спасибо за участие и за статью :)
            Присоединяюсь к голосам за продолжение про overlay.