Сетевые namespace и мосты: что на самом деле подключает 'docker run'
Сеть контейнера — это netns Linux со своими интерфейсами и маршрутами, соединённый с хостом veth-парой на мосту docker0, а iptables MASQUERADE NAT-ит исходящий трафик. Знание четырёх примитивов превращает большинство сетевых багов в диагноз одной командой.
Алерт дежурного был расплывчатым: один сервис не мог достучаться до другого, но только иногда. Команда час пялилась в логи приложения, счётчики ретраев и конфиг DNS, прежде чем кто-то выполнил ip netns на хосте и увидел реальную картину. Вся сеть контейнера — его интерфейсы, маршруты, ARP-таблица, его представление iptables — живёт в сетевом namespace Linux, объекте ядра таком же конкретном, как процесс. Внутри сбоящего контейнера ip addr показывал eth0 с адресом из 172.17.0.0/16, но в ip route не было маршрута по умолчанию к мосту. Кривой кастомный entrypoint при старте сбросил таблицу маршрутизации. «Перемежающийся» паттерн был лишь тем, какой контейнер рестартовал последним. Починка заняла тридцать секунд, как только перестали дебажить приложение и начали дебажить namespace — потому что всё, что Docker делает с сетью, это четыре примитива ядра, которые можно осмотреть руками: namespace, veth-пара, мост и правило NAT.
Namespace — это граница
Контейнер — это в основном набор namespace, и сетевой namespace (netns) владеет сетью. У него собственный список интерфейсов, таблица маршрутизации, ARP-кэш, /proc/net, представление conntrack и правила iptables — полностью изолированные от хоста и от соседних контейнеров. Когда docker run запускает контейнер в дефолтной сети bridge, демон создаёт свежий netns; внутри контейнер видит ровно два интерфейса:
$ docker run --rm alpine ip addr
1: lo: <LOOPBACK,UP> mtu 65536 # loopback, 127.0.0.1
2: eth0@if47: <UP> mtu 1500 # один конец veth-пары
inet 172.17.0.2/16 ... # адрес из подсети docker0Источник истины здесь — хост, а не самоотчёт контейнера. Когда видишь eth0@if47, нотация уже подсказывает: eth0 — одна половина veth-пары (виртуального кабеля, соединяющего netns с хостом), а if47 — индекс её второй половины, живущей на хосте. С хоста ip netns и nsenter --net=/proc/<pid>/ns/net ip addr позволяют шагнуть внутрь сетевого представления контейнера вообще без шелла в нём — диагностика, завершающая большинство споров «это приложение или сеть».
veth-пары и мост
veth-пара — это виртуальный Ethernet-кабель: два интерфейса, соединённые спина к спине, где кадр, записанный в один, выходит из другого. Один конец (eth0) сидит внутри netns контейнера; другой (с именем вида vethXXXX) остаётся в netns хоста и подчинён мосту docker0. Мост — это программный коммутатор Layer-2: хостовый veth каждого контейнера втыкается в него, поэтому контейнеры на одном мосту достигают друг друга напрямую по IP на L2, без маршрутизации и NAT.
Числа по умолчанию, которые стоит запомнить: docker0 несёт подсеть 172.17.0.0/16, сам мост на 172.17.0.1 (шлюз по умолчанию для контейнеров), и каждый veth-интерфейс по умолчанию имеет MTU 1500 под стандартный Ethernet. Этот MTU — классический источник сбоев: запусти Docker внутри overlay или VPN с эффективным MTU 1450, оставь veth на 1500, и крупные пакеты молча дропаются или фрагментируются — TLS-рукопожатия зависают ровно в точке отправки сертификата (большой кадр). Драйвер моста не определяет MTU подложки автоматически; задаёшь com.docker.network.driver.mtu или он тебя укусит.
Контейнер показывает eth0@if47 с 172.17.0.2/16, но не может достучаться ни до чего вне хоста. ip route внутри контейнера пуст. Какой единственный примитив сломан?
▸Почему это работает
Почему мост плюс veth-пары, а не просто выдать каждому контейнеру хостовый интерфейс? Изоляция и плотность. Netns даёт каждому контейнеру собственный приватный L3-стек, так что порт 8080 в одном никогда не сталкивается с портом 8080 в другом, а мост позволяет сотням контейнеров делить одну хостовую сетевую карту, всё ещё общаясь друг с другом на L2 — всё программно, без лишнего железа, без IP из физической сети. Цена — лишний прыжок через мост и правило NAT на исходящем, и это ровно та механика, которую разбирает следующий раздел.
MASQUERADE: как исходящий трафик покидает хост
Подсеть моста 172.17.0.0/16 приватна и не маршрутизируется в физической сети, поэтому контейнеру, говорящему с интернетом, нужно переписать адрес источника на хостовый. Docker ставит правило iptables MASQUERADE в цепочку POSTROUTING таблицы nat:
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADEЧитай точно: для любого пакета из подсети моста, уходящего через интерфейс, отличный от docker0 (то есть в реальную сетевую карту), переписать IP источника на адрес этой карты. MASQUERADE — это SNAT, автоматически берущий текущий IP исходящего интерфейса, что верно для DHCP-хостов. Таблица conntrack (таблица отслеживания соединений ядра) запоминает каждый поток, чтобы ответы были раз-NAT-лены обратно нужному контейнеру. Эта таблица конечна (net.netfilter.nf_conntrack_max, часто 262144 на дефолтном хосте): машина с десятками тысяч короткоживущих исходящих соединений в секунду может её исчерпать, и тогда новые соединения падают с nf_conntrack: table full, dropping packet, хотя CPU и память выглядят простаивающими. Исходящий трафик, «случайно отказывающий под нагрузкой», — это обычно conntrack, а не приложение.
Два контейнера на дефолтном мосту говорят друг с другом по IP без NAT, но трафик каждого в интернет NAT-ится на IP хоста. Почему асимметрия?
- 01Проследи, что 'docker run' подключает для контейнера на дефолтном мосту, назвав все четыре примитива и числа по умолчанию.
- 02Дай режим отказа и однострочный диагноз для каждого примитива: маршрутизация, veth/MTU и conntrack.
Сеть контейнера — не чёрный ящик; это четыре примитива ядра Linux, которые можно осмотреть руками. Сетевой namespace — это граница: каждый контейнер получает свои интерфейсы, таблицу маршрутизации, ARP-кэш, представление conntrack и правила iptables, изолированные от хоста и соседей, поэтому порты никогда не сталкиваются между контейнерами. veth-пара — кабель, соединяющий этот namespace с хостом: один конец — eth0 контейнера с адресом из 172.17.0.0/16, другой — хостовый vethXXXX, подчинённый мосту docker0, программному L2-свитчу на 172.17.0.1, позволяющему контейнерам на одном мосту достигать друг друга напрямую без NAT. Оба конца veth по умолчанию MTU 1500, и рассогласование с меньшим MTU подложки — классический баг молчаливого дропа, вешающий TLS-рукопожатия. Исходящий наружу SNAT-ится правилом iptables MASQUERADE в nat/POSTROUTING, срабатывающим лишь для пакетов, уходящих через реальную сетевую карту (защита ’! -o docker0’ — причина, по которой трафик той же подсети не тронут), а таблица conntrack ядра запоминает каждый поток, чтобы ответы вернулись нужному контейнеру — конечная таблица (дефолт ~262144), исчерпывающаяся при высокой текучести соединений и дропающая новые, пока машина выглядит простаивающей. Senior-ход — перестать дебажить приложение и определить, какой примитив отказал: пропавший маршрут, мёртвый veth, рассогласование MTU или исчерпание conntrack — у каждого есть диагноз одной командой с хоста. Теперь, когда видишь контейнер, который «ни до чего не достучаться», первая мысль — ip route внутри netns, а не ещё один час в логах приложения.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.