Как известно, Docker умеет создавать виртуальные сети для безопасного и удобного сетевого взаимодействия внутри контейнеров. В этой статье мы рассмотрим, как именно он это делает на примере базовых манипуляций с сетью в рамках одного хоста с операционной системой Linux.
Ссылка на статью на хабре – https://habr.com/ru/articles/794262/
Небольшая теоретическая часть
Для того, чтобы понимать, что будет происходить дальше в статье, надо немного окунуться в теорию. Если вы уже работали с настройкой сетей в Linux, то можете сразу переходить к следующей части.
На всякий случай в этом блоке кратко объясню, что значат строки типа 192.168.0.1/16
.
Часть /16
означает, что первые 16 бит ip-адреса предназначены для идентификации сети, а
все остальные – для идентификации узла. В приведённом ранее примере 192.168
– это
часть, идетифицирующая сеть, а 0.1
– идентифицирующая узел.
/32
означает, что указан конкретный ip-адрес узла.
Сетевые Namespace'ы (netns)
Network Namespace в Linux – это изолированная среда сетевых ресурсов. У неё свои ip-адреса, таблицы маршрутизации, firewall (о нём ниже) и так далее.
Они полезны, когда, например, у вас есть две виртуальные машины, которые вы хотите связать друг с другом, но изолировать от других сетей. Ну, или если вам нужно сделать тоже самое, но с контейнерам :).
Виртуальные сетевые интерфейсы
Виртуальные сетевые интерфейсы – это, по сути, логическое представление физических. Используются для маршрутизации, туннелирования, создания виртуальных сетей и так далее.
Мы будет использовать loopback
(или lo или localhost), veth
и bridge
, поэтому про них поподробнее.
loopback
(localhost) – Предоставляет возможность обмена трафиком в пределах самого устройства.- Используется для внутренней коммуникации приложений и тестирования.
- Доступен по адресам 127.0.0.1 - 127.255.255.255.
veth
(Virtual Ethernet) – Виртуальный ethernet кабель: связывает 2 точки виртуальной сети и делает возможным передачу трафика между ними.bridge
– По сути, это виртуальный switch.
Switch – это устройство, с помощью которого можно соединить различные сетевые устройства для обмена трафиком.
Например, подключив компьютер и принтер к одному и тому же Switch'у, компьютер сможет "говорить" принтеру, что и в каком количестве надо распечатать :).
Firewall
Firewall используется для фильтрации трафика, как входящего, так и исходящего. Он может иметь вид физического устройства или виртуальной логической реализации. Мы будем использовать второе для дополнительной изоляции сетевых namespace'ов.
NAT
NAT или Network Address Translation – это технология преобразования внутренних ip-адресов во внешний, и наоброт. Например, когда вы подключены к одному Wi-Fi с телефона и компьютера, на обоих устройствах будет единый внешний ip-адрес. NAT будет менять у входящего трафика ip-адрес роутера на внтуренний ip-адрес устройста, которому адресованы пакеты. И наоборот, ip-адрес у исходящих пакетов будет изменён на ip-адрес роутера.
Нужно всё это для экономии публичных ip-адресов, так как их количество сильно ограничено (около 4.3 миллиардов, из которых 81% уже назначены или зарезервированы).
Что делает Docker?
Docker для каждого контейнера создаёт сетевой namespace (netns), и если вы не указали
при создании контейнера сеть, к которой его надо подключить, то контейнер подключится к дефолтной сети
(которой под капотом является bridge
) парой veth
интерфейсов. Это нужно для того,
чтобы по умолчанию контейнеры могли друг с дру гом общаться.
Но только этого было бы недостаточно. Для отправки ip пакетов контейнеры должны знать, какой узел будет начальной
точкой в построении маршрута до других контейнеров, подключённых к той же docker-сети, то есть bridge
'у.
Этой начальной точкой (gateway
'ем) становится сам bridge
, к которому подключен контейнер. То есть выглядит
это как-то так: "Для трафика, отправленного на ip-адреса, которые находятся в подсети 172.17.0.0/16, задай gateway
172.17.0.1".
Мы только что рассмотрели docker network driver bridge, но есть ещё несколько драйверов, например:
--network none
– контейнер вообще не будет ни к чему подключен, и не будет иметь даже доступа в интерне т через сеть хоста. Всё что у него будет, этоloopback
интерфейс.--network host
– контейнер будет подключен к сети хоста.
Для того, чтобы у контейнеров был доступ в интернет, Docker добавляет в NAT правила подмены ip-адреса для пакетов,
которые отправляются из bridge
интерфейсов docker-сетей. Это нужно, чтобы ip-адреса контейнеров
менялись на ip-адрес выходного интерфейса (или ip-адрес хоста), через который строятся маршруты в интернет.
Также каждому контейнеру добавляется дефолтный gateway (шлюз) в таблицу маршрутизации, чтобы пакеты для
всех адресов отправлялись через bridge
интерфейс, так как у него
есть доступ к сети хоста.
Помимо этого Docker добавляет в firewall
правила, запрещающие перенаправление трафика из одной docker-сети
в другую для их изоляции друг от друга.
Когда вы публикуете порт из контейнера -p <внешний порт>:<внутренний порт>
внутренний
порт становится доступен на локальной сети хоста. Сейчас по дефолту docker для этого держит
процесс "docker-proxy", который слушает нужные порты и перенаправляет на адрес нужного контейнера,
и его внутренний порт. Это ненужное legacy, которое будет скоро полностью удалено из Docker
в пользу hairpin NAT, но пока его можно только вручную отключить, добавив демону флаг --userland-proxy=false
.
Простыми словами, hairpin NAT - это процесс, когда пакет отправляется на адрес внешнего узла, но затем направляется обратно к узлу внутри той же локальной сети.
Docker также умеет: создавать оверлейную сеть, соединяя несколько docker демонов,
создавать IPvlan сеть, объединяя контейнеры без использования bridge
интерфейса и так далее.
Это всё очень интересно, но в текущуюю статью это не войдет, слишком много информации.
Практическая часть
Создадим виртуальную сеть с доступом в интернет и выведем из неё в сеть хоста TCP порт 8000,
который будет доступен через 127.0.0.1
и другим машинам
в нашей сети (аналог docker -p <внешний порт>:<внутренний порт>
).
Большая часть объяснений будет в комментариях в кодовых блоках.
Далее по статье для модификаций Firewall и NAT я буду использовать iptables
.
Понимаю, что он уже считается устаревшим, но Docker продолжает его использовать,
поэтому и я решил использовать его, а не nftables
.
Шаг #1 – Создаём network namespace
Начнём с создания netns и запуска в нём простого питоновского http-сервера.
# Создаём netns c именем "red"
ip netns add red
# Задаём loopback интерфейсу Ipv4 адрес, так как по дефолту он не задан
ip -n red addr add 127.0.0.1/8 dev lo
# Включаем loopback интерфейс
ip -n red link set lo up
# Запускаем http-сервер в рамках нового netns
ip netns exec red python3 -m http.server
Мы можем запрашивать у сервера что-либо только изнутри netns (ip netns exec red curl 127.0.0.1:8000
).
Пока он полностью изолирован.