Современные приложения активно используют сети. Обычное дело, когда во время сборки apt-get/dnf/yum/apk install устанавливает пакет из репозитория пакетов дистрибутива Linux. При выполнении команды приложение может захотеть подключиться к внутренней базе данных postgres или mysql, чтобы сохранить определённое состояние при вызове listen() и accept(). При этом разработчик должен иметь возможность работать отовсюду — из дома или офиса, с мобильного устройства или через VPN. Docker Desktop помогает сделать так, чтобы сеть «просто работала» в каждом из сценариев. В статье разбираем инструменты и методы, которые обеспечивают это, начиная с всеми любимого набора протоколов: TCP/IP.

TCP/IP

TCP/IP — набор протоколов, который задаёт стандарты связи между компьютерами и содержит подробные соглашения о маршрутизации и межсетевом взаимодействии. Когда контейнер хочет подключиться к внешнему миру, он используют TCP/IP. Поскольку для Linux-контейнеров требуется ядро Linux, Docker Desktop включает вспомогательную виртуальную машину Linux. В результате трафик из контейнеров идёт от виртуальной машины Linux, а не от хоста, что вызывает серьёзную проблему.

Многие IT-отделы создают политики VPN, где говорится что-то вроде «перенаправлять через VPN только трафик, исходящий от хоста». Смысл в том, чтобы предотвратить случайное действие хоста в качестве маршрутизатора, перенаправляющего небезопасный трафик из интернета в защищенные корпоративные сети. Если программа VPN увидит трафик с виртуальной машины Linux, он не будет маршрутизироваться через VPN, что не позволит контейнерам получить доступ к внутренним ресурсам. 

Docker Desktop помогает избежать этой проблемы, перенаправляя весь трафик на уровне пользователя через vpnkit и стек TCP/IP, написанный на OCaml поверх библиотек сетевых протоколов проекта MirageOS. На диаграмме показан поток пакетов от вспомогательной виртуальной машины через vpnkit и в Интернет:

При загрузке виртуальная машина запрашивает адрес с помощью DHCP. Ethernet-фрейм, содержащий запрос, передаётся от виртуальной машины к хосту через общую память, либо через virtio на Mac, либо через AF_VSOCK на Windows. Vpnkit содержит виртуальный коммутатор ethernet (mirage-vnetif), который перенаправляет запрос на сервер DHCP (mirage/charrua).

Как только виртуальная машина получает ответ DHCP, содержащий IP-адрес виртуальной машины и IP-адрес шлюза, она отправляет запрос ARP для определения адреса сетевого шлюза (mirage/arp). После получения ответа ARP он сможет отправить пакет в Интернет.

Когда vpnkit видит исходящий пакет с новым IP-адресом, он создаёт виртуальный стек TCP/IP для удалённой машины (mirage/mirage-tcpip). Этот стек действует как одноранговый стек в Linux — принимает и обменивается пакетами. Когда контейнер вызывает connect() для установления TCP-соединения, Linux отправляет TCP-пакет с установленным флагом SYNchronize. Vpnkit наблюдает за флагом SYNchronize и сам вызывает connect() с хоста. Если connect() завершается успешно, vpnkit отвечает Linux пакетом TCP SYNchronize. В Linux connect() выполняется успешно, и данные проксируются в обоих направлениях (mirage/mirage-flow). Если connect() отклоняется, vpnkit отвечает пакетом TCP RST (reset), который заставляет connect() внутри Linux возвращать ошибку. UDP и ICMP обрабатываются аналогичным образом.

Помимо низкоуровневого TCP/IP, vpnkit имеет ряд встроенных высокоуровневых сетевых служб, например, DNS-сервер (mirage/ocaml-dns) и HTTP-прокси (mirage/cohttp). К этим службам можно обращаться напрямую — через виртуальный IP-адрес или DNS-имя, и косвенно — путем сопоставления исходящего трафика и динамического перенаправления в зависимости от конфигурации.

«Docker для админов и разработчиков»

С адресами TCP/IP трудно работать напрямую. В следующем разделе разберём, как Docker Desktop использует DNS для присвоения удобочитаемых имён сетевым службам.

DNS

Внутри Docker Desktop есть несколько DNS-серверов:

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

Остальные поисковые запросы отправляются в CoreDNS (из CNCF). Затем в зависимости от доменного имени запросы перенаправляются на один из двух DNS-серверов на хосте. Домен docker.internal  считается особенным и включает в себя DNS-имя host.docker.internal, которое преобразуется в IP-адрес для текущего хоста. Хотя предпочтительнее, когда всё контейнеризировано, иногда имеет смысл запускать часть приложения как обычный сервис хостинга. Имя host.docker.internal позволяет контейнерам связываться с этими хост-сервисами и не беспокоиться о хардкодинге IP-адресов.

Второй DNS-сервер на хосте обрабатывает остальные запросы с помощью стандартных системных библиотек ОС. Это гарантирует, что, если имя правильно разрешится в веб-браузере разработчика, оно также будет правильно разрешаться в контейнерах. Это особенно важно при сложных настройках, например, когда одни запросы отправляются через корпоративный VPN (internal.registry.mycompany), в то время как другие — через обычный интернет (docker.com).

HTTP(S)-прокси

Некоторые организации блокируют прямой доступ в интернет и требуют, чтобы весь трафик направлялся через HTTP-прокси для фильтрации и логирования. Это влияет на извлечение образов во время сборки, а также на исходящий сетевой трафик, генерируемый контейнерами.

Самый простой способ использования HTTP-прокси — указать движку Docker на прокси-сервер c помощью переменных среды. Единственный недостаток: при необходимости изменения прокси-сервера придётся перезапустить Docker для обновления переменных, что приведёт к сбою. Docker Desktop позволяет избежать этого. Он запускает собственный HTTP-прокси внутри vpnkit, который перенаправляет на восходящий прокси-сервер. При изменении восходящего прокси-сервера внутренний прокси-сервер динамически перенастраивается, что позволяет избежать перезапуска.

На Mac Docker Desktop отслеживает параметры прокси-сервера, сохраненные в системных настройках. Когда компьютер переключает сеть (например, между сетями Wi-Fi или на сотовую связь), Docker Desktop автоматически обновляет внутренний HTTP-прокси, поэтому всё продолжает работать без каких-либо действий со стороны разработчика.

Port forwarding 

Порты позволяют сетевым и подключенным к интернету устройствам взаимодействовать через указанные каналы. Хотя серверы с назначенными IP-адресами могут подключаться к интернету напрямую и делать порты публично доступными, система, находящаяся за пределами локальной сети, может оказаться недоступной из интернета. Port Forwarding — технология проброса портов, которая позволяет преодолеть это ограничение и сделать устройства публично доступными. Доступ предоставляется с помощью перенаправления трафика определённых портов с внешнего адреса маршрутизатора на адрес выбранного компьютера в локальной сети.

Поскольку Docker Desktop запускает Linux-контейнеры внутри виртуальной машины Linux, возникает разрыв: порты на виртуальной машине открыты, но инструменты работают на хосте. Нам нужно что-то для перенаправления соединений с хоста на виртуальную машину.

Рассмотрим отладку веб-приложения: разработчик вводит docker run -p 80:80, чтобы порт 80 контейнера был открыт на порту 80 хоста (и чтобы сделать его доступным через http://localhost). Вызов Docker API записывается в /var/run/docker.sock  на хосте, как обычно. Когда Docker Desktop запускает Linux-контейнеры, движок Docker представляет собой программу Linux, работающую внутри вспомогательной виртуальной машины Linux, а не на хосте. Поэтому Docker Desktop включает в себя прокси-сервер Docker API, который пересылает запросы с хоста на виртуальную машину. В целях безопасности запросы не пересылаются напрямую по протоколу TCP по сети. Вместо этого Docker Desktop перенаправляет соединения с доменными сокетами Unix по защищенному низкоуровневому пути через процессы, обозначенные на схеме выше как vpnkit-bridge.

Прокси Docker API может делать больше, чем просто пересылать запросы туда и обратно. Он также может декодировать и преобразовывать запросы и ответы, чтобы улучшить работу разработчика. Когда разработчик предоставляет порт с помощью docker run -p 80:80, прокси Docker API декодирует запрос и использует внутренний API для переадресации порта через процесс com.docker.backend. Если что-то на хосте уже прослушивает этот порт, разработчику возвращается удобочитаемое сообщение об ошибке. Если порт свободен, процесс com.docker.backend начинает принимать соединения и перенаправлять их в контейнер через vpnkit-forwarder, запущенный поверх vpnkit-bridge.

Docker Desktop не запускается с «root» или «Administrator» на хосте. Разработчик может использовать docker run –privileged , чтобы получить права root внутри вспомогательной виртуальной машины, но гипервизор гарантирует, что хост всегда будет защищён. Это хорошо с точки зрения безопасности, но вызывает проблему удобства использования в macOS — как разработчик может открыть порт 80 (docker run -p 80:80), когда он считается «привилегированным портом» в Unix, то есть номер порта < 1024? Решение состоит в том, что Docker Desktop включает в себя вспомогательную привилегированную службу, которая запускается от имени root из launchd  и которая говорит API «пожалуйста, привяжите этот порт». В связи с этим возникает вопрос: безопасно ли разрешать пользователю без полномочий root привязывать привилегированные порты?

Привилегированные порты изначально были функцией безопасности. Они появились во времена, когда порты использовались для аутентификации сервисов: можно было с уверенностью предположить, что вы разговариваете с HTTP-демоном хоста, потому что он привязан к порту 80, для которого требуется root. Современный способ аутентификации — с помощью сертификатов TLS и отпечатков пальцев SSH. Поэтому пока системные службы связывают свои порты до запуска Docker Desktop, macOS связывает порты при загрузке через launchd, благодаря чему не может быть путаницы или отказа в обслуживании. Соответственно, современная macOS сделала привязку привилегированных портов ко всем IP-адресам (0.0.0.0 или INADDR_ANY) непривилегированной операцией. Есть только один случай, когда Docker Desktop все ещё нуждается в использовании привилегированного помощника для привязки портов: когда запрашивается определенный IP (например, docker run -p 127.0.0.1:80:80), для которого требуется root в macOS.

Коротко о главном

Извлечение образов Docker, установка пакетов Linux, взаимодействие с серверными частями базы данных — всё это ежедневные задачи, для выполнения которых приложениям нужны надёжные сетевые подключения. Docker Desktop работает в самых разных средах: в офисе, дома и даже в поездках с нестабильным Wi-Fi. Однако на каких-то компьютерах могут быть установлены ограничительные политики брандмауэра, на каких-то — сложные конфигурации VPN. В таких случаях Docker Desktop стремится «просто работать», чтобы разработчик мог сосредоточиться на создании и тестировании своего приложения (а не на отладке Docker).

«Docker для админов и разработчиков»

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


  1. Tzimie
    00.00.0000 00:00

    А вот как при запуске контейнера в Винде узнать его адрес с точки зрения винды, не парзя вывод ipconfig?


    1. darthslider
      00.00.0000 00:00
      +3

      С точки зрения винды у всех контейнеров будет адрес VM с линукс, где эти контейнеры запускаются.


      1. Tzimie
        00.00.0000 00:00

        Странно что мой вопрос никто не понимает.

        Я запускаю контейнер. В нем apache server. Я хочу к нему (внутрь) обратиться в Chrome на моей Винде. Какой адрес мне указать?


        1. darthslider
          00.00.0000 00:00
          +1

          Если делаете docker run с ключем -p, то localhost.
          Можно еще ip vm, которую использует докер.


          1. Tzimie
            00.00.0000 00:00

            По localhist я попаду на iis своей винды, а не внутрь докера! Винда докеру даёт отдельный IP, который можно выцепить в выводе ipconfig.


            1. darthslider
              00.00.0000 00:00
              +2

              IIS слушает конкретный порт.
              Если он занят, то чуда не произойдет. Опубликуйте докер приложение на другом порту.


            1. AstarothAst
              00.00.0000 00:00

              Если у вас докердесктоп, то он при установке прописывает себя в hosts в виде:
              192.168.31.54 host.docker.internal
              192.168.31.54 gateway.docker.internal


              1. Tzimie
                00.00.0000 00:00

                у меня прописал:

                Added by Docker Desktop

                192.168.1.6 host.docker.internal
                192.168.1.6 gateway.docker.internal

                To allow the same kube context to work on the host and the container:

                127.0.0.1 kubernetes.docker.internal

                End of section

                То есть не то.


                1. AstarothAst
                  00.00.0000 00:00

                  В смысле не то? Я пошел в свой докер-десктоп, и запустил там контейнер nginx заэкспоузив порт 30600:80, после чего вбил в адресную строку хрома host.docker.internal:30600 и получил ожидаемую «Welcome to nginx!». Я чего-то не понял в вопросе?


      1. Tzimie
        00.00.0000 00:00

        да, так как узнать этот адрес виртуалки?

        вот какой у меня костыль

        # find container IP
        &ipconfig >tmp.tmp
        $n = 0
        ForEach ($line in (Get-Content "tmp.tmp")) {
          if ($n -gt 0) { $n = $n + 1 }
          if ($line -eq 'Ethernet adapter vEthernet (WSL):') { $n = 1}
          if ($n -eq 5) { break }
        }
        $http = 'http://' + $line.split(':')[1].trimStart().trimEnd() + ':8080'


        1. darthslider
          00.00.0000 00:00

          А он вам зачем?
          Со своей машины ходите через локалхост.
          С других машин - по айпи своей машины.
          У вм скорее всего адрес динамический.


          1. Tzimie
            00.00.0000 00:00

            Есть некая система, которая очень сложно конфигурируется. Соответственно демо где надо выполнить 30 пунктов никто выполнять не будет.

            Я сделал все эти шаги в убунту для докера. Теперь можно скачать image, запустить и коннектиться к этой системе. Только вот незадача, выяснить ip без костылей никак(


          1. Tzimie
            00.00.0000 00:00

            Опять таки, у виртуалки докера ip НЕ localhost. Это какой то динамически выделенный новый IP в домашней сетке.


            1. vasyakolobok77
              00.00.0000 00:00

              Вас несколько раз спросили зачем вам адрес в подсети докера, который каждый раз будет разный? Чтобы пробиться с хоста в докер образ - используем localhost или например ipv6 ::1


              1. Tzimie
                00.00.0000 00:00

                Блин, да, адрес разный. И localhost НЕ РАБОТАЕТ - это локальная машина а не докер. Придется делать картинки. итак, запускаю докер.

                Внутри убунта и Apache на 8080 порту. Запускаю ipconfig чтгобы понять IP контейнера:

                Обращаемся по этому адресу. Все в порядке:

                Для проверки зайдем на 8080 localhost:

                Потому что этот однострочный сайт я только что сляпал для IIS. Это не удивительно, потому что IP моей виндовой машины:

                И мой вопрос, как без костылей (парзинг вывода ipconfig) узнать у самого докера его адрес (172.19.112.1), он может меняться.


                1. dolfinus
                  00.00.0000 00:00
                  +1

                  Если порт контейнера проброшен на хост через -p 8000:8000, то на хосте docker api будет слушать порт 8000 на всех интерфейсах (0.0.0.0) и перенаправлять запросы на 8000 порт контейнера. Тогда запросы на localhost:8000 в браузере будут отправляться в контейнер, и при этом не нужно знать постоянно меняющийся IP виртуалки. Если запускать контейнер с -p 127.0.0.1:8000:8000, то docker api будет слушать 8000 порт на localhost, и это все также будет работать, но при этом в контейнер нельзя будет обратиться из других машин в той же локальной сети, что и хост.

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


                  1. Tzimie
                    00.00.0000 00:00

                    Да. Я рассматривал докер как виртуалку, и не пробрасывал порты из-за конфликтов. Я не знал что это является нестандартным кейсом.


              1. Tzimie
                00.00.0000 00:00

                Я вас убедил картинками что по localhost НЕ ПРОБИТЬСЯ в докер образ?


              1. Tzimie
                00.00.0000 00:00

                localhost = 192.168.1.6

                docker = 172.19.112.1

                "используем localhost или например ipv6 ::1 " - идет на виндовую машину но никак ни на докер


                1. amkartashov
                  00.00.0000 00:00

                  тебе уже несколько раз ответили, что если в случае docker-desktop на винде, порт контейнера выставленный наружу доступен на localhost винды.


                  1. Tzimie
                    00.00.0000 00:00

                    И мои скриншоты показывают, что это не так)


                    1. amkartashov
                      00.00.0000 00:00

                      это не так у тебя. Обратись в тех поддержку.


                      1. Tzimie
                        00.00.0000 00:00

                        См ниже. Разобрались


                    1. ColdPhoenix
                      00.00.0000 00:00
                      +1

                      Образ был запущен с пробросом портов?

                      Порт был свободен?


                      1. Tzimie
                        00.00.0000 00:00

                        Похоже я понял. Это ведь первый эксперимент с докером. Сделал образ, запустил с -p, он ругался что порт занят, потому что у меня такое же стоит на Винде

                        Убрал -p, он запустился, я обрадовался, нашел его IP, стал юзать, и это мне было понятно и логично как виртуалка которая работает на своем IP. Только не понимал, почему этот IP не просто узнать. Привык к этому

                        Вы первый спросили про -p вместо повторяющихся мантр "все должно работать по localhost". Спасибо вам


                      1. darthslider
                        00.00.0000 00:00
                        +1

                        Ну в смысле первый?
                        https://habr.com/ru/company/southbridge/blog/719412/comments/#comment_25278468
                        "Если делаете docker run с ключем -p, то localhost. "
                        Вот тут я сразу говорю про ключ -p же.

                        И дальше про вашу ситуацию:
                        "IIS слушает конкретный порт. Если он занят, то чуда не произойдет. Опубликуйте докер приложение на другом порту. "


                      1. Tzimie
                        00.00.0000 00:00

                        Вы правы.


                      1. Tzimie
                        00.00.0000 00:00

                        P.S. Который раз убеждаюсь, что сложнее всего выяснить очевидные вещи.


            1. AstarothAst
              00.00.0000 00:00

              Если это виртуалка в VirtualBox к примеру, то у нее в настройках сети есть mac-адрес, и он не меняется при перезапусках — в настройках домашнего роутера можно для него прописать конкретный ip в настройках dhcp.


              1. Tzimie
                00.00.0000 00:00

                я знаю. Но мне нужен докер. Не понимаю, почему самое первое что человеку нужно - IP адрес того что запустил - и это никак не добиться


                1. ColdPhoenix
                  00.00.0000 00:00
                  +1

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

                  Это делается так же и в самом Linux между прочим, флагом -p

                  Тогда в Docker Desktop мы видим такую картину:

                  Что затем открывается как http://localhost:3001/dashboard, и при настройке фаерволла доступно с других машин.

                  Если вам нужен адрес контейнера, то тут во всех докерах одинаково все в общем-то.

                  И да, это не позволит естественно слушать один порт на одном адресе нескольким приложениям


                  1. ColdPhoenix
                    00.00.0000 00:00

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

                    Озвученная вами проблема существует и вне докера в общем-то, и он её не решает.


                    1. Tzimie
                      00.00.0000 00:00

                      Спасибо, все заработало.

                      Остался один вопрос из любопытства: допустим внутри докера есть компонент, у которого есть конфиг файлы, которые должны лежать в его дереве, то есть их не вынесешь в отдельный volume. Как принято их менять? Способов много, но все неудобные


                      1. ColdPhoenix
                        00.00.0000 00:00

                        тут зависит от точной структуры директорий.

                        Вы же можете спокойно любую папку/файл сделать как Volume.

                        Если директорий должна быть не пуста(скажем конфиг по умолчанию), то можно это сделать подменив точку входа при сборке контейнера, а в ней, инициализировать директорию конфигурацией по умолчанию если она пуста.(например pgSQL контейнер так создает БД в data Volume)

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

                        Есть случаи(чаще в k8s это встречал правда), когда есть еще процесс что следить за конфигами в отдельной директории, потом конвертирует/копирует что надо основному процессу и дает ему сигнал перечитать.

                        Лучше же чтоб конфиг был полностью в своей директории, тогда просто монтируете ее как Volume, и проблемы нет.


  1. web3_Venture
    00.00.0000 00:00

    хорошо если тут не знают значит не где не знают.

    Вопрос про настройку контейнера с сетевым интерфейсом бридж - macvlan. Как знаете когда выбираете этот интерфейс , то ваш контейнер получает от вашего DHCP (роутера) динамический IP адрес, и вы можете из любого устройства вашей локальной сети зайти по этому адресу как будто это еще одна физическая машина (ну или виртуальная машина с режиме бридж).

    Так вот , есть всетаки одно отличие , когда вы запускаете виртуальная машину то в DHCP вы видите не только выданный IP адрес, но и ИМЯ этой машины, соотвественно вы можете обратиться по имени как http://myvirtualmachine/ вместо IP.

    Но когда в docker запускаешь контейнер в режиме macvlan , то имя не подхватывается , а в DHCP на роутере вижу в колонке hostname пустоту.

    Как исправить?