Как стать автором
Поиск
Написать публикацию
Обновить

Создаём виртуальную сеть, как это делает Docker

Уровень сложностиПростой
Время на прочтение8 мин
Количество просмотров28K

Как известно, Docker умеет создавать виртуальные сети для безопасного и удобного сетевого взаимодействия внутри контейнеров. В этой статье мы рассмотрим, как именно он это делает на примере базовых манипуляций с сетью в рамках одного хоста с операционной системой Linux.

networking
networking

Небольшая теоретическая часть

Для того, чтобы понимать, что будет происходить дальше в статье, надо немного окунуться в теорию. Если вы уже работали с настройкой сетей в 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 networking
docker networking

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). Пока он полностью изолирован.

Шаг #2 – Создаём bridge интерфейс

Теперь нам нужно создать тот самый bridge интерфейс и подключить к нему ранее созданный netns red.

Помимо этого мы также зададим ip-адреса для наших netns и bridge вместе с указанием подсети, которая должна у них совпадать.

# Создаём bridge интерфейс с именем br0
ip link add br0 type bridge
# Задаём bridge интерфейсу ip-адрес 10.100.0.1. 
# При этом также указывая, что первые 24 бита 
# предназначены для идентификации сети, то есть часть "10.100.0".
ip addr add 10.100.0.1/24 dev br0
# Включаем интерфейс br0
ip link set br0 up

# Создаём пару veth интерфейсов с именами red0 и red0.br0
ip link add red0 type veth peer name red0.br0

# Подключаем один из veth интерфейсов к bridge'у и включаем его
ip link set red0.br0 master br0
ip link set red0.br0 up

# Второй veth устанавливаем для нашего red netns
ip link set red0 netns red
# Устанавливаем ему ip-адрес и включаем
ip -n red addr add 10.100.0.2/24 dev red0
ip -n red link set red0 up

Теперь с сети хоста можно пинговать netns и отправлять запросы http-серверу.

ping 10.100.0.2
curl 10.100.0.2:8000

Для эксперимета можете по аналогии создать второй netns, подключить его к bridge и попробовать попинговать один netns из другого и наоборот.

ip netns exec blue ping 10.100.0.2
ip netns exec red ping 10.100.0.3

Вы увидете, что ping проходит, так как оба netns подключены к одному и тому же bridge.

Шаг #3 – Необходимые системные параметры

Чтобы всё у нас корректно работало нам нужно поменять два системных параметра:

  1. net.ipv4.ip_forward – ip port forwarding нужен для того, чтобы можно было перенаправлть трафик между интерфейсами.

  2. net.ipv4.conf.<interface>.route_localnet – route_localnet=1 разрешает перенаправления трафика на локальные сети. В нашем случае нам нужно разрешить это для нашего bridge интерфейса.

sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.conf.br0.route_localnet=1

Шаг #4 – Даём доступ в интернет из network namespace

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

Также через тот же iptables необходимо добавить правила, разрешающие forward трафика из нашего основного сетевого интерфейса в bridge интерфейс и наоборот, в таблицу FILTER.

И наконец, добавить дефолтный gateway для netns, чтобы по умолчанию пакеты, адресованные во внешний интернет, шли через адрес bridge интерфейса.

# Правило для таблицы NAT
iptables -t nat -A POSTROUTING -s 10.100.0.0/24 -o eth0 -j MASQUERADE

# Правило для таблицы FILTER (firewall)
iptables -A FORWARD -o eth0 -i br0 -j ACCEPT
iptables -A FORWARD -i eth0 -o br0 -j ACCEPT

# Для нашего netns'а задаём дефолтным gateway'ем наш bridge
ip netns exec red ip route add default via 10.100.0.1

Пробуем пропинговать внешний ip-адрес:

ip netns exec red ping 8.8.8.8

Шаг #5 – Выводим TCP порт в сеть хоста

На этом этапе мы добавим возможность стучать в наш http-сервер через http://localhost:8000 и вместе с этим сделаем возможным обращаться к нему машинкам извне, то есть другим реальным компьютерам, которые подключены к вашей приватной сети, или, если у вас напрямую подключение к публичной сети, тогда вообще для всех компьютеров в интернете.

### Правила для таблицы NAT

# Для пакетов извне (с других компьютеров) (относится к цепочке PREROUTING) добавляем правило, 
# которое для всех пакетов, адресованных нашей локальной сети по tcp порту 8000,
# меняет destionation адрес на адрес нашего namespace'а.
iptables -t nat -A PREROUTING -p tcp -m addrtype --dst-type LOCAL -m tcp --dport 8000 -j DNAT --to-destination 10.100.0.2:8000

# Для пакетов, сгенерированных хостом (относится к цепочке OUTPUT), добавляем правило, 
# которое для всех пакетов, адресованных нашей локальной сети по tcp порту 8000,
# меняет destionation адрес на адрес нашего namespace'а.
iptables -t nat -A OUTPUT -p tcp -m addrtype --dst-type LOCAL -m tcp --dport 8000 -j DNAT --to-destination 10.100.0.2:8000

# Добавляем действие MASQUERADE, которое применяется к пакетам,
# проходящим через интерфейс br0
# и которые были отправлены с локальной сети (то есть прошедшие через цепочку OUTPUT).
# MASQUERADE заменит их ip-адрес на ip-адрес шлюза,
# через который они покинули сеть, в нашем случае на ip-адрес br0.
iptables -t nat -A POSTROUTING -o br0 -m addrtype --src-type LOCAL -j MASQUERADE

# Добавляем действие MASQUERADE ко всем TCP пакетам 
# с портом 8000, адресованным локальной сети.
# Нужно для пакетов, пришедших от других компьютеров (вышедших из цепочки PREROUTING). 
iptables -t nat -A POSTROUTING -m addrtype --dst-type LOCAL -p tcp -m tcp --dport 8000 -j MASQUERADE

### Правила для таблицы FILTER (firewall)

# Разрашаем роутинг, пакетам проходящим через bridge интерфейс, которые относятся к установленным соединениям.
iptables -A FORWARD -o br0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# Разрешаем роутинг пакетам, не входящим с интерфейса bridge и адресованным этому интерфейсу по TCP порту 8000. 
iptables -A FORWARD -d 10.100.0.2/32 ! -i br0 -o br0 -p tcp -m tcp --dport 8000 -j ACCEPT

# Разрешаем трафик, который приходит с bridge интерфейса
iptables -A FORWARD -i br0 -j ACCEPT

Теперь можно попробовать постучаться в наш http-сервер из сети хоста:

curl 127.0.0.1:8000

И также поотправлять в него запросы с других машин через ip-адрес хоста:

# замените ip на свой x)
curl 192.168.1.15:8000

Всё!

По итогу мы получили:

  1. http-сервер, запущенный в изолированном сетевом пространстве.

  2. Доступ к этому серверу по порту 8000 из loopback (localhost) интерфейса хоста.

  3. Перенаправление пакетов от других машин по tcp порту 8000 в наш http-сервер.


Спасибо за чтение :)

Теги:
Хабы:
Всего голосов 23: ↑22 и ↓1+26
Комментарии7

Публикации

Ближайшие события