Современные приложения активно используют сети. Обычное дело, когда во время сборки 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).
Комментарии (35)
web3_Venture
00.00.0000 00:00хорошо если тут не знают значит не где не знают.
Вопрос про настройку контейнера с сетевым интерфейсом бридж - macvlan. Как знаете когда выбираете этот интерфейс , то ваш контейнер получает от вашего DHCP (роутера) динамический IP адрес, и вы можете из любого устройства вашей локальной сети зайти по этому адресу как будто это еще одна физическая машина (ну или виртуальная машина с режиме бридж).
Так вот , есть всетаки одно отличие , когда вы запускаете виртуальная машину то в DHCP вы видите не только выданный IP адрес, но и ИМЯ этой машины, соотвественно вы можете обратиться по имени как http://myvirtualmachine/ вместо IP.
Но когда в docker запускаешь контейнер в режиме macvlan , то имя не подхватывается , а в DHCP на роутере вижу в колонке hostname пустоту.
Как исправить?
Tzimie
А вот как при запуске контейнера в Винде узнать его адрес с точки зрения винды, не парзя вывод ipconfig?
darthslider
С точки зрения винды у всех контейнеров будет адрес VM с линукс, где эти контейнеры запускаются.
Tzimie
Странно что мой вопрос никто не понимает.
Я запускаю контейнер. В нем apache server. Я хочу к нему (внутрь) обратиться в Chrome на моей Винде. Какой адрес мне указать?
darthslider
Если делаете docker run с ключем -p, то localhost.
Можно еще ip vm, которую использует докер.
Tzimie
По localhist я попаду на iis своей винды, а не внутрь докера! Винда докеру даёт отдельный IP, который можно выцепить в выводе ipconfig.
darthslider
IIS слушает конкретный порт.
Если он занят, то чуда не произойдет. Опубликуйте докер приложение на другом порту.
AstarothAst
Если у вас докердесктоп, то он при установке прописывает себя в hosts в виде:
192.168.31.54 host.docker.internal
192.168.31.54 gateway.docker.internal
Tzimie
у меня прописал:
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
То есть не то.
AstarothAst
В смысле не то? Я пошел в свой докер-десктоп, и запустил там контейнер nginx заэкспоузив порт 30600:80, после чего вбил в адресную строку хрома host.docker.internal:30600 и получил ожидаемую «Welcome to nginx!». Я чего-то не понял в вопросе?
Tzimie
да, так как узнать этот адрес виртуалки?
вот какой у меня костыль
darthslider
А он вам зачем?
Со своей машины ходите через локалхост.
С других машин - по айпи своей машины.
У вм скорее всего адрес динамический.
Tzimie
Есть некая система, которая очень сложно конфигурируется. Соответственно демо где надо выполнить 30 пунктов никто выполнять не будет.
Я сделал все эти шаги в убунту для докера. Теперь можно скачать image, запустить и коннектиться к этой системе. Только вот незадача, выяснить ip без костылей никак(
Tzimie
Опять таки, у виртуалки докера ip НЕ localhost. Это какой то динамически выделенный новый IP в домашней сетке.
vasyakolobok77
Вас несколько раз спросили зачем вам адрес в подсети докера, который каждый раз будет разный? Чтобы пробиться с хоста в докер образ - используем localhost или например ipv6 ::1
Tzimie
Блин, да, адрес разный. И localhost НЕ РАБОТАЕТ - это локальная машина а не докер. Придется делать картинки. итак, запускаю докер.
Внутри убунта и Apache на 8080 порту. Запускаю ipconfig чтгобы понять IP контейнера:
Обращаемся по этому адресу. Все в порядке:
Для проверки зайдем на 8080 localhost:
Потому что этот однострочный сайт я только что сляпал для IIS. Это не удивительно, потому что IP моей виндовой машины:
И мой вопрос, как без костылей (парзинг вывода ipconfig) узнать у самого докера его адрес (172.19.112.1), он может меняться.
dolfinus
Если порт контейнера проброшен на хост через -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, и придумывая для каждой ОС свой набор костылей, я решительно не понимаю.
Tzimie
Да. Я рассматривал докер как виртуалку, и не пробрасывал порты из-за конфликтов. Я не знал что это является нестандартным кейсом.
Tzimie
Я вас убедил картинками что по localhost НЕ ПРОБИТЬСЯ в докер образ?
Tzimie
localhost = 192.168.1.6
docker = 172.19.112.1
"используем localhost или например ipv6 ::1 " - идет на виндовую машину но никак ни на докер
amkartashov
тебе уже несколько раз ответили, что если в случае docker-desktop на винде, порт контейнера выставленный наружу доступен на localhost винды.
Tzimie
И мои скриншоты показывают, что это не так)
amkartashov
это не так у тебя. Обратись в тех поддержку.
Tzimie
См ниже. Разобрались
ColdPhoenix
Образ был запущен с пробросом портов?
Порт был свободен?
Tzimie
Похоже я понял. Это ведь первый эксперимент с докером. Сделал образ, запустил с -p, он ругался что порт занят, потому что у меня такое же стоит на Винде
Убрал -p, он запустился, я обрадовался, нашел его IP, стал юзать, и это мне было понятно и логично как виртуалка которая работает на своем IP. Только не понимал, почему этот IP не просто узнать. Привык к этому
Вы первый спросили про -p вместо повторяющихся мантр "все должно работать по localhost". Спасибо вам
darthslider
Ну в смысле первый?
https://habr.com/ru/company/southbridge/blog/719412/comments/#comment_25278468
"Если делаете docker run с ключем -p, то localhost. "
Вот тут я сразу говорю про ключ -p же.
И дальше про вашу ситуацию:
"IIS слушает конкретный порт. Если он занят, то чуда не произойдет. Опубликуйте докер приложение на другом порту. "
Tzimie
Вы правы.
Tzimie
P.S. Который раз убеждаюсь, что сложнее всего выяснить очевидные вещи.
AstarothAst
Если это виртуалка в VirtualBox к примеру, то у нее в настройках сети есть mac-адрес, и он не меняется при перезапусках — в настройках домашнего роутера можно для него прописать конкретный ip в настройках dhcp.
Tzimie
я знаю. Но мне нужен докер. Не понимаю, почему самое первое что человеку нужно - IP адрес того что запустил - и это никак не добиться
ColdPhoenix
Если для доступа к контейнеру, который должен быть доступен извне вам нужен его адрес, значит вы запустили контейнер не правильно.
Это делается так же и в самом Linux между прочим, флагом -p
Тогда в Docker Desktop мы видим такую картину:
Что затем открывается как http://localhost:3001/dashboard, и при настройке фаерволла доступно с других машин.
Если вам нужен адрес контейнера, то тут во всех докерах одинаково все в общем-то.
И да, это не позволит естественно слушать один порт на одном адресе нескольким приложениям
ColdPhoenix
Не успел отредактировать, собственно такая же проблема у вас будет при запуске внутри виртуальной машины, вы должны знать её адрес.
Озвученная вами проблема существует и вне докера в общем-то, и он её не решает.
Tzimie
Спасибо, все заработало.
Остался один вопрос из любопытства: допустим внутри докера есть компонент, у которого есть конфиг файлы, которые должны лежать в его дереве, то есть их не вынесешь в отдельный volume. Как принято их менять? Способов много, но все неудобные
ColdPhoenix
тут зависит от точной структуры директорий.
Вы же можете спокойно любую папку/файл сделать как Volume.
Если директорий должна быть не пуста(скажем конфиг по умолчанию), то можно это сделать подменив точку входа при сборке контейнера, а в ней, инициализировать директорию конфигурацией по умолчанию если она пуста.(например pgSQL контейнер так создает БД в data Volume)
В худшем случае контейнер может быть рантаймом, а приложение быть снаружи него по сути(например какой-то контейнер для PHP-фреймфорка, был так организован). Возможно совместить с первым подходом.
Есть случаи(чаще в k8s это встречал правда), когда есть еще процесс что следить за конфигами в отдельной директории, потом конвертирует/копирует что надо основному процессу и дает ему сигнал перечитать.
Лучше же чтоб конфиг был полностью в своей директории, тогда просто монтируете ее как Volume, и проблемы нет.