Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Современные приложения активно используют сети. Обычное дело, когда во время сборки 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-имя, и косвенно — путем сопоставления исходящего трафика и динамического перенаправления в зависимости от конфигурации.
«RabbitMQ для админов и разработчиков»
С адресами 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).
«RabbitMQ для админов и разработчиков»