Главная/Docker Networking/Урок

8.2. Bridge networks

Цели

После этого материала вы сможете:

  • назвать четыре различия default bridge и user-defined bridge;
  • объяснить, почему DNS работает в одной сети и не работает в другой;
  • создать сеть с заданной подсетью и понять, когда это нужно;
  • изолировать группы сервисов так, чтобы база была недоступна из внешней сети;
  • подключать и отключать container от сети на работающем container'е;
  • прочитать вывод docker network inspect и найти в нём нужное.

Предварительные знания

Ключевые термины

ТерминОбъяснение
default bridgeСеть с именем bridge, соответствует docker0
user-defined bridgeСеть, созданная docker network create
ICCInter-container communication — связь между container'ами одной сети
--internalСеть без выхода наружу
network aliasДополнительное DNS-имя container'а в сети
embedded DNSРезолвер Docker на адресе 127.0.0.11

Теория

Четыре различия

СвойствоDefault bridgeUser-defined bridge
Разрешение имён container'овНетЕсть
ИзоляцияВсе container'ы вместеТолько участники сети
Подключение на летуЕстьЕсть
Настройка подсетиТолько через daemon.json и перезапуск демонаПри создании сети
Network aliasesНетЕсть
Передача переменных через --linkЕсть (устаревшее)Не поддерживается

Первая строка — решающая. В default bridge container'ы обращаются друг к другу только по IP-адресам, которые меняются при каждом пересоздании. Единственный штатный способ имён там — флаг --link, объявленный устаревшим.

Отсюда практическое правило: default bridge не используют. Он существует для обратной совместимости; любая многосервисная конфигурация начинается с docker network create. Compose создаёт сеть автоматически, поэтому там вопрос не возникает (раздел 09).

Почему DNS работает не везде

В user-defined сети Docker подставляет в container /etc/resolv.conf собственный резолвер на адресе 127.0.0.11. Он знает имена всех container'ов этой сети и их псевдонимы; всё остальное пересылает вышестоящим серверам (урок 8.4).

В default bridge этот резолвер тоже присутствует, но не содержит имён container'ов: там работает только пересылка запросов наружу.

Это не техническое ограничение, а сознательное решение о совместимости: поведение default bridge не меняли, чтобы не сломать конфигурации, существовавшие до появления user-defined сетей.

Изоляция

Container видит только те container'ы, с которыми состоит в общей сети. Два container'а в разных user-defined сетях не имеют между собой связи — ни по имени, ни по IP.

Отсюда стандартный приём: разделение на frontend- и backend-сеть.

text
        ┌──── frontend ────┐        ┌──── backend ────┐
        │                  │        │                 │
   ──►  │   proxy    web ──┼────────┼── web     db    │
        │                  │        │                 │
        └──────────────────┘        └─────────────────┘
                                     (--internal: нет
                                      выхода наружу)

web состоит в обеих сетях и служит единственным мостом. db находится только в backend, недостижим из frontend и, при флаге --internal, не имеет выхода в интернет.

Это даёт реальное ограничение доступа, а не декоративное: пакет из proxy к db не имеет маршрута.

Внутри одной сети изоляции нет

Важное уточнение: все container'ы одной сети видят все порты друг друга, а не только опубликованные.

ЧтоДоступно внутри сетиДоступно с host
Порт, слушающий 0.0.0.0ДаТолько если опубликован
Порт, слушающий 127.0.0.1НетНет
Опубликованный портДаДа

Публикация нужна для доступа с host и извне, а не между container'ами. Порт базы данных публиковать не требуется, если к ней обращается только приложение из той же сети — и не следует, потому что публикация делает его доступным снаружи.

Параметр com.docker.network.bridge.enable_icc=false при создании сети запрещает связь между её участниками, оставляя только опубликованные порты. Применяется редко: обычно проще разделить сети.

Создание сети

bash
docker network create appnet
docker network create --subnet 10.10.0.0/24 --gateway 10.10.0.1 appnet2
docker network create --internal backend
docker network create -o com.docker.network.bridge.name=br-app appnet3
ПараметрЗачем
--subnetФиксированная адресация; нужен для статических адресов
--gatewayЯвный шлюз внутри подсети
--ip-rangeПул для автоматической выдачи — уже подсети
--internalНет выхода наружу и нет входа снаружи
--attachableРазрешает подключать отдельные container'ы к overlay-сети
-o com.docker.network.bridge.nameЧитаемое имя bridge на host вместо br-a1b2c3d4

Задавать подсеть в большинстве случаев не требуется. Это нужно, когда адреса должны быть предсказуемы (правила на внешнем оборудовании) или когда автоматический пул конфликтует с сетью организации (урок 8.1).

Статические адреса и псевдонимы

bash
docker run -d --network appnet --ip 10.10.0.50 --name fixed nginx:alpine
docker run -d --network appnet --network-alias db --name postgres-1 postgres:17-alpine

Статический адрес требует сети с явной --subnet. Он нужен редко: имена надёжнее и не ломаются при изменении адресации.

Псевдоним — второе имя container'а в этой сети. Практическая ценность: несколько container'ов могут иметь один и тот же псевдоним, и DNS вернёт все их адреса — простейшая балансировка (урок 8.4).

Подключение на лету

bash
docker network connect backend web
docker network disconnect frontend web

Работает на запущенном container'е, перезапуск не нужен. Внутри появляется новый интерфейс eth1, маршруты обновляются.

Ограничение: приложение, прочитавшее адреса при старте, об изменении не узнает. DNS-имена от этого не страдают, поэтому очередной довод в пользу имён вместо адресов.


Внутренний механизм

Что создаёт docker network create

  1. Bridge-интерфейс на host с именем br-<первые 12 символов ID>.
  2. Подсеть из пула default-address-pools, если не задана явно.
  3. Правила netfilter: MASQUERADE для исходящего трафика, изоляция от других docker-сетей.
  4. Запись во внутреннюю базу Docker.

Правило изоляции — то самое, что не даёт container'ам разных сетей общаться напрямую: трафик между двумя docker-bridge отбрасывается в цепочке DOCKER-ISOLATION-STAGE-1.

Как выглядит --internal

Для внутренней сети Docker не создаёт правило MASQUERADE и добавляет правила, отбрасывающие трафик, идущий наружу. Container сохраняет адрес и связь с соседями по сети, но не может обратиться ни к чему за её пределами — включая host.


Команды и примеры

DNS: default против user-defined

bash
docker network create demo-net > /dev/null

echo "═══ default bridge ═══"
docker run -d --name d1 python:3.13-slim sleep 300 > /dev/null
docker run -d --name d2 python:3.13-slim sleep 300 > /dev/null
sleep 1
docker exec d1 getent hosts d2 && echo "  имя разрешилось" || echo "  ✗ имя d2 не разрешается"

echo "═══ user-defined bridge ═══"
docker run -d --network demo-net --name u1 python:3.13-slim sleep 300 > /dev/null
docker run -d --network demo-net --name u2 python:3.13-slim sleep 300 > /dev/null
sleep 1
docker exec u1 getent hosts u2 | sed 's/^/  /' && echo "  ✓ имя разрешилось"

echo "═══ что в /etc/resolv.conf ═══"
printf '  default:      %s\n' "$(docker exec d1 grep nameserver /etc/resolv.conf | head -1)"
printf '  user-defined: %s\n' "$(docker exec u1 grep nameserver /etc/resolv.conf | head -1)"

Ожидаемый вывод:

text
═══ default bridge ═══
  ✗ имя d2 не разрешается
═══ user-defined bridge ═══
  172.18.0.3      u2
  ✓ имя разрешилось
═══ что в /etc/resolv.conf ═══
  default:      nameserver 127.0.0.11
  user-defined: nameserver 127.0.0.11

Резолвер 127.0.0.11 подставлен в обоих случаях — но имена container'ов знает только сеть user-defined. Различие не в наличии DNS, а в содержимом его зоны.

Изоляция между сетями

bash
docker network create net-a > /dev/null
docker network create net-b > /dev/null

docker run -d --network net-a --name in-a python:3.13-slim \
    python -m http.server 8000 --bind 0.0.0.0 > /dev/null
docker run -d --network net-b --name in-b python:3.13-slim sleep 300 > /dev/null
sleep 3

ip_a="$(docker inspect -f '{{.NetworkSettings.Networks.net_a.IPAddress}}' in-a 2>/dev/null \
        || docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' in-a)"
echo "  адрес in-a: $ip_a"

echo "═══ доступ по имени из другой сети ═══"
docker exec in-b getent hosts in-a > /dev/null 2>&1 \
    && echo "  имя разрешилось" || echo "  ✗ имя не разрешается"

echo "═══ доступ по IP из другой сети ═══"
docker exec in-b python -c "
import socket
s = socket.socket(); s.settimeout(3)
try:
    s.connect(('$ip_a', 8000)); print('  соединение установлено')
except Exception as e:
    print(f'  ✗ недоступно: {type(e).__name__}')
"

echo "═══ подключаем in-b ко второй сети ═══"
docker network connect net-a in-b
sleep 1
docker exec in-b python -c "
import socket, urllib.request
print('  по имени:', socket.gethostbyname('in-a'))
r = urllib.request.urlopen('http://in-a:8000/', timeout=3)
print('  HTTP:', r.status)
"

echo "═══ интерфейсы in-b теперь ═══"
docker exec in-b sh -c 'awk "NR>2 {printf \"  %s\n\", \$1}" /proc/net/dev'

Ожидаемый вывод:

text
  адрес in-a: 172.19.0.2
═══ доступ по имени из другой сети ═══
  ✗ имя не разрешается
═══ доступ по IP из другой сети ═══
  ✗ недоступно: timeout
═══ подключаем in-b ко второй сети ═══
  по имени: 172.19.0.2
  HTTP: 200
═══ интерфейсы in-b теперь ═══
  lo:
  eth0:
  eth1:

Изоляция работает на обоих уровнях: имя не разрешается и адрес недостижим. После подключения к общей сети появился второй интерфейс eth1, и связь заработала — без перезапуска container'а.

Обратите внимание на тип отказа: таймаут, а не Connection refused. Это подпись отбрасывания пакета правилом фильтрации, в отличие от отказа в соединении (урок 8.6).

bash
docker rm -f d1 d2 u1 u2 in-a in-b > /dev/null
docker network rm demo-net net-a net-b > /dev/null

Разделение frontend и backend

bash
docker network create frontend > /dev/null
docker network create --internal backend > /dev/null

# База — только в backend
docker run -d --network backend --name db \
    -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=appdb \
    postgres:17-alpine > /dev/null

# Приложение — в обеих сетях
docker run -d --network backend --name web python:3.13-slim sleep 600 > /dev/null
docker network connect frontend web

# Прокси — только во frontend
docker run -d --network frontend --name proxy python:3.13-slim sleep 600 > /dev/null
sleep 8

echo "═══ web видит db ═══"
docker exec web python -c "
import socket
s = socket.socket(); s.settimeout(3)
try:
    s.connect(('db', 5432)); print('  ✓ web → db: соединение установлено')
except Exception as e:
    print(f'  ✗ web → db: {type(e).__name__}')
"

echo "═══ proxy не видит db ═══"
docker exec proxy python -c "
import socket
s = socket.socket(); s.settimeout(3)
try:
    s.connect(('db', 5432)); print('  ✗ proxy → db: соединение установлено (изоляции нет!)')
except socket.gaierror:
    print('  ✓ proxy → db: имя не разрешается')
except Exception as e:
    print(f'  ✓ proxy → db: {type(e).__name__}')
"

echo "═══ proxy видит web ═══"
docker exec proxy getent hosts web | sed 's/^/  /'

echo "═══ db не имеет выхода наружу (--internal) ═══"
docker exec db sh -c 'ip route | head -2' 2>/dev/null | sed 's/^/  /'
docker exec db timeout 4 wget -q -O- https://example.com > /dev/null 2>&1 \
    && echo "  ✗ выход в интернет есть" || echo "  ✓ выхода в интернет нет"

echo "═══ сети container'а web ═══"
docker inspect web --format '{{range $n, $v := .NetworkSettings.Networks}}  {{$n}}: {{$v.IPAddress}}
{{end}}'

Ожидаемый вывод:

text
═══ web видит db ═══
  ✓ web → db: соединение установлено
═══ proxy не видит db ═══
  ✓ proxy → db: имя не разрешается
═══ proxy видит web ═══
  172.20.0.3      web
═══ db не имеет выхода наружу (--internal) ═══
  172.21.0.0/16 dev eth0 scope link src 172.21.0.2
  ✓ выхода в интернет нет
═══ сети container'а web ═══
  backend: 172.21.0.3
  frontend: 172.20.0.3

Три уровня защиты видны в выводе.

proxy не может даже разрешить имя db — они в разных сетях, и DNS-зона у каждой своя.

У db в таблице маршрутов нет строки default: сеть создана с --internal, шлюз не назначен. Это отличается от блокировки правилом — маршрута просто не существует.

web имеет по адресу в каждой сети и служит единственным мостом.

bash
docker rm -f db web proxy > /dev/null
docker network rm frontend backend > /dev/null

Порты видны внутри сети без публикации

bash
docker network create portnet > /dev/null
docker run -d --network portnet --name server python:3.13-slim \
    python -m http.server 8000 --bind 0.0.0.0 > /dev/null
docker run -d --network portnet --name client python:3.13-slim sleep 300 > /dev/null
sleep 3

echo "═══ опубликованные порты ═══"
docker port server 2>/dev/null | sed 's/^/  /' || echo "  портов не опубликовано"

echo "═══ доступ с host ═══"
curl -s -m 3 -o /dev/null -w '  HTTP %{http_code}\n' http://localhost:8000/ 2>/dev/null \
    || echo "  ✓ с host недоступен"

echo "═══ доступ из соседнего container ═══"
docker exec client python -c "
import urllib.request
print('  ✓ из сети доступен, HTTP', urllib.request.urlopen('http://server:8000/', timeout=3).status)
"

docker rm -f server client > /dev/null
docker network rm portnet > /dev/null

Ожидаемый вывод:

text
═══ опубликованные порты ═══
  портов не опубликовано
═══ доступ с host ═══
  ✓ с host недоступен
═══ доступ из соседнего container ═══
  ✓ из сети доступен, HTTP 200

Порт не публиковался — и снаружи недоступен, а внутри сети работает. Это и есть причина не публиковать порт базы данных: приложению он доступен и так, а публикация открыла бы его всем.

Своя подсеть и читаемое имя bridge

bash
docker network create \
    --subnet 10.77.0.0/24 \
    --gateway 10.77.0.1 \
    --ip-range 10.77.0.128/25 \
    -o com.docker.network.bridge.name=br-custom \
    custom-net > /dev/null

echo "═══ параметры сети ═══"
docker network inspect custom-net --format '
  подсеть:  {{range .IPAM.Config}}{{.Subnet}}{{end}}
  шлюз:     {{range .IPAM.Config}}{{.Gateway}}{{end}}
  пул:      {{range .IPAM.Config}}{{.IPRange}}{{end}}
  драйвер:  {{.Driver}}
  internal: {{.Internal}}'

echo "═══ bridge на host ═══"
ip -brief addr show br-custom 2>/dev/null | sed 's/^/  /'

echo "═══ адреса выдаются из --ip-range ═══"
for n in 1 2 3; do
    docker run -d --network custom-net --name "c$n" alpine:3.21 sleep 60 > /dev/null
done
sleep 1
for n in 1 2 3; do
    printf '  c%s: %s\n' "$n" \
        "$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "c$n")"
done

echo "═══ статический адрес вне пула ═══"
docker run -d --network custom-net --ip 10.77.0.10 --name static alpine:3.21 sleep 60 > /dev/null
printf '  static: %s\n' \
    "$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' static)"

docker rm -f c1 c2 c3 static > /dev/null
docker network rm custom-net > /dev/null

Ожидаемый вывод:

text
═══ параметры сети ═══
  подсеть:  10.77.0.0/24
  шлюз:     10.77.0.1
  пул:      10.77.0.128/25
  драйвер:  bridge
  internal: false
═══ bridge на host ═══
  br-custom        UP             10.77.0.1/24
═══ адреса выдаются из --ip-range ═══
  c1: 10.77.0.129
  c2: 10.77.0.130
  c3: 10.77.0.131
═══ статический адрес вне пула ═══
  static: 10.77.0.10

Автоматические адреса берутся из --ip-range (10.77.0.128/25), а нижняя половина подсети остаётся свободной для статических назначений. Это стандартный приём, когда часть адресов должна быть фиксированной.

Имя br-custom вместо br-a1b2c3d4e5f6 заметно упрощает чтение tcpdump и правил netfilter.

Правило изоляции в netfilter

bash
echo "═══ цепочки изоляции ═══"
sudo iptables -S DOCKER-ISOLATION-STAGE-1 2>/dev/null | head -5 | sed 's/^/  /' \
    || echo "  (используется nftables-backend; см. nft list ruleset)"

Ожидаемый вывод:

text
═══ цепочки изоляции ═══
  -N DOCKER-ISOLATION-STAGE-1
  -A DOCKER-ISOLATION-STAGE-1 -i br-custom ! -o br-custom -j DOCKER-ISOLATION-STAGE-2
  -A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
  -A DOCKER-ISOLATION-STAGE-1 -j RETURN

Правило читается так: трафик, вошедший через один docker-bridge и уходящий не через него же, направляется на вторую стадию, где отбрасывается, если адресован другому docker-bridge.

Это и есть механизм изоляции сетей — обычные правила фильтрации, которые можно посмотреть и понять.


Практическое упражнение

Задание. Постройте трёхуровневую конфигурацию с проверяемой изоляцией.

Компоненты: proxy (внешний вход), api (приложение), db (база), cache (Redis).

Требования:

  1. proxy доступен с host по опубликованному порту.
  2. api доступен из proxy по имени, но не с host.
  3. db и cache доступны из api, но не из proxy и не с host.
  4. db и cache не имеют выхода в интернет.
  5. Ни один порт, кроме порта proxy, не опубликован.
  6. Каждое утверждение подтверждено проверкой; скрипт возвращает ненулевой код при нарушении.

Подсказки

Подсказка 1

Требования 2 и 3 задают, кто в какой сети состоит. Нарисуйте схему до написания команд.

Подсказка 2

Требование 4 — это флаг при создании сети, а не правило фильтрации.

Подсказка 3

Проверка «недоступен» должна отличать таймаут от отказа: и то и другое означает недоступность, но проверять надо оба варианта.

Решение

Показать решение
bash
mkdir -p /tmp/netiso && cd /tmp/netiso

cat > setup.sh <<'SH'
#!/usr/bin/env bash
# Три сети: edge (вход), app (приложение), data (хранилища, без интернета).
set -euo pipefail

docker network create edge > /dev/null
docker network create app  > /dev/null
docker network create --internal data > /dev/null

# db и cache — только в data
docker run -d --network data --name db \
    -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=appdb \
    postgres:17-alpine > /dev/null
docker run -d --network data --name cache redis:8-alpine > /dev/null

# api — в app и data (мост между уровнями)
docker run -d --network app --name api python:3.13-slim \
    python -m http.server 8000 --bind 0.0.0.0 > /dev/null
docker network connect data api

# proxy — в edge и app, единственный с опубликованным портом
docker run -d --network edge --name proxy -p 18080:8000 python:3.13-slim \
    python -m http.server 8000 --bind 0.0.0.0 > /dev/null
docker network connect app proxy
SH
chmod +x setup.sh

cat > check.sh <<'SH'
#!/usr/bin/env bash
# Проверяет все шесть требований. Ненулевой код при нарушении изоляции.
set -uo pipefail
fail=0
ok()  { printf '  ✓ %s\n' "$1"; }
bad() { printf '  ✗ %s\n' "$1"; fail=1; }

# Возвращает: ok | refused | timeout | dnsfail
probe() {  # probe <container> <host> <port>
    docker exec "$1" python - "$2" "$3" <<'PY' 2>/dev/null
import socket, sys
host, port = sys.argv[1], int(sys.argv[2])
s = socket.socket(); s.settimeout(3)
try:
    s.connect((host, port)); print("ok")
except socket.gaierror:
    print("dnsfail")
except ConnectionRefusedError:
    print("refused")
except OSError:
    print("timeout")
finally:
    s.close()
PY
}

printf '\n═══ 1. proxy доступен с host ═══\n'
code="$(curl -s -m 5 -o /dev/null -w '%{http_code}' http://localhost:18080/)"
[ "$code" = "200" ] && ok "HTTP $code с host" || bad "получено '$code'"

printf '\n═══ 2. api: доступен из proxy, недоступен с host ═══\n'
[ "$(probe proxy api 8000)" = "ok" ] && ok "proxy → api" || bad "proxy не видит api"
if curl -s -m 3 -o /dev/null http://localhost:8000/ 2>/dev/null; then
    bad "api отвечает на host:8000"
else
    ok "api недоступен с host"
fi

printf '\n═══ 3. db и cache: из api — да, из proxy — нет ═══\n'
[ "$(probe api db 5432)" = "ok" ]      && ok "api → db"    || bad "api не видит db"
[ "$(probe api cache 6379)" = "ok" ]   && ok "api → cache" || bad "api не видит cache"
r="$(probe proxy db 5432)"
[ "$r" != "ok" ] && ok "proxy ✗ db ($r)"    || bad "proxy достучался до db"
r="$(probe proxy cache 6379)"
[ "$r" != "ok" ] && ok "proxy ✗ cache ($r)" || bad "proxy достучался до cache"

printf '\n═══ 4. data-сеть без выхода в интернет ═══\n'
for c in db cache; do
    if docker exec "$c" timeout 4 wget -q -O- https://example.com > /dev/null 2>&1; then
        bad "$c имеет выход в интернет"
    else
        ok "$c без выхода наружу"
    fi
done
# Подтверждаем причину: отсутствие маршрута по умолчанию
for c in db cache; do
    if docker exec "$c" sh -c 'ip route' 2>/dev/null | grep -q '^default'; then
        bad "$c имеет маршрут по умолчанию"
    else
        ok "$c: маршрута по умолчанию нет (--internal)"
    fi
done

printf '\n═══ 5. опубликован только порт proxy ═══\n'
published=0
for c in proxy api db cache; do
    p="$(docker port "$c" 2>/dev/null)"
    if [ -n "$p" ]; then
        printf '    %-6s %s\n' "$c" "$(echo "$p" | tr '\n' ' ')"
        published=$((published + 1))
        [ "$c" = "proxy" ] || bad "$c публикует порты"
    fi
done
[ "$published" -eq 1 ] && ok "публикует порты ровно один container" || bad "публикующих container'ов: $published"

printf '\n═══ карта сетей ═══\n'
for c in proxy api db cache; do
    nets="$(docker inspect "$c" --format '{{range $n, $v := .NetworkSettings.Networks}}{{$n}} {{end}}')"
    printf '    %-6s %s\n' "$c" "$nets"
done

printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo "  изоляция подтверждена" || echo "  ЕСТЬ НАРУШЕНИЯ"
exit "$fail"
SH
chmod +x check.sh

cat > teardown.sh <<'SH'
#!/usr/bin/env bash
docker rm -f proxy api db cache > /dev/null 2>&1
docker network rm edge app data > /dev/null 2>&1
SH
chmod +x teardown.sh

./setup.sh
sleep 10
./check.sh
echo "КОД: $?"
./teardown.sh
cd /tmp && rm -rf /tmp/netiso

Ожидаемый вывод:

text
═══ 1. proxy доступен с host ═══
  ✓ HTTP 200 с host

═══ 2. api: доступен из proxy, недоступен с host ═══
  ✓ proxy → api
  ✓ api недоступен с host

═══ 3. db и cache: из api — да, из proxy — нет ═══
  ✓ api → db
  ✓ api → cache
  ✓ proxy ✗ db (dnsfail)
  ✓ proxy ✗ cache (dnsfail)

═══ 4. data-сеть без выхода в интернет ═══
  ✓ db без выхода наружу
  ✓ cache без выхода наружу
  ✓ db: маршрута по умолчанию нет (--internal)
  ✓ cache: маршрута по умолчанию нет (--internal)

═══ 5. опубликован только порт proxy ═══
    proxy  8000/tcp -> 0.0.0.0:18080 
  ✓ публикует порты ровно один container

═══ карта сетей ═══
    proxy  app edge 
    api    app data 
    db     data 
    cache  data 

═══ ИТОГ ═══
  изоляция подтверждена
КОД: 0

Все шесть требований выполнены.

Три решения, определяющие качество.

Функция probe различает четыре исхода, а не два. Проверка «соединение не установилось» прошла бы и при опечатке в имени container'а — тест был бы зелёным, ничего не проверяя. Возвращая dnsfail, refused или timeout, проверка сообщает причину, и в выводе видно, что изоляция сработала на уровне DNS, а не из-за ошибки в скрипте.

Требование 4 проверяется дважды: попыткой выхода и отсутствием маршрута. Первая проверка отвечает на вопрос «работает ли»; вторая — «почему». Если бы wget не сработал по другой причине (нет самого wget, нет DNS вовне), первая проверка дала бы ложноположительный результат. Отсутствие строки default в таблице маршрутов — прямое следствие --internal и подтверждает именно его.

Требование 5 проверяется обходом всех container'ов, а не только ожидаемого. Скрипт не спрашивает «публикует ли proxy порт», он спрашивает «кто вообще публикует порты» и считает их. Так обнаружится случайно оставленный -p 5432:5432 у базы — самая частая утечка в таких конфигурациях.

Чего решение не делает. Изоляция здесь действует на уровне сетей Docker и защищает от обращений между container'ами. Она не мешает процессу, получившему выполнение кода внутри api, обратиться и к db, и наружу — api состоит в обеих сетях по определению задачи. Не покрыт и доступ с host: db недоступна по опубликованному порту, но администратор host может обратиться к 172.x.x.x:5432 напрямую, поскольку маршрут к docker-сетям на host существует. Полная изоляция от host требует правил фильтрации на самом host (раздел 12).

Проверка результата

bash
docker network create checknet > /dev/null
docker run -d --network checknet --name a alpine:3.21 sleep 60 > /dev/null
docker run -d --network checknet --name b alpine:3.21 sleep 60 > /dev/null
docker exec a getent hosts b
docker rm -f a b > /dev/null; docker network rm checknet > /dev/null

Ожидается строка с IP-адресом и именем b.

Типичные ошибки

ОшибкаПричинаИсправление
Использование default bridgeРаботает без настройкиНет DNS по именам; создать свою сеть
--link для связи container'овСтарые руководстваУстарело; user-defined сеть плюс имена
Публикация порта базы данных«Чтобы приложение достучалось»Внутри сети порт доступен и без публикации
Обращение по IP вместо имениКажется надёжнееАдрес меняется при пересоздании
Все сервисы в одной сетиПрощеНет изоляции; разделить на уровни
--internal для сети приложенияХотели «побезопаснее»Пропадёт доступ к внешним API и обновлениям
Статический адрес без --subnetНе знали о требованииDocker откажет: адрес вне управляемой подсети
Ожидают, что изоляция защитит и от hostСчитают её полнойНа host есть маршрут к docker-сетям
Таймаут и отказ считают однимОба «не работает»Таймаут — фильтрация, отказ — хост ответил

Контрольные вопросы

На понимание:

  1. Назовите четыре различия default bridge и user-defined bridge.
  2. Почему 127.0.0.11 присутствует в обеих сетях, но имена работают только в одной?
  3. Почему порт базы данных не нужно публиковать?
  4. Что именно делает --internal?
  5. Чем таймаут отличается от Connection refused при проверке изоляции?

На применение:

  1. Как сделать базу недоступной из внешней сети, но доступной приложению?
  2. Как подключить работающий container ко второй сети?
  3. Как задать сети конкретную подсеть и читаемое имя bridge?

На диагностику:

  1. Два container'а в Compose видят друг друга, а запущенные docker run — нет. Почему?
  2. Приложение обращается к db:5432 и получает Name or service not known. Версия?

Краткое резюме

  1. В default bridge нет разрешения имён container'ов — только IP или устаревший --link.
  2. User-defined сеть даёт DNS, изоляцию, псевдонимы и настраиваемую адресацию.
  3. Резолвер 127.0.0.11 есть в обеих сетях; различается содержимое его зоны.
  4. Container'ы разных сетей не видят друг друга ни по имени, ни по адресу.
  5. Внутри одной сети доступны все порты соседей, а не только опубликованные.
  6. Публикация нужна для доступа с host и извне, а не для связи между container'ами.
  7. --internal убирает маршрут по умолчанию: нет ни выхода, ни входа.
  8. docker network connect работает на запущенном container'е и добавляет интерфейс.
  9. Container в двух сетях служит мостом между уровнями — основа схемы frontend/backend.
  10. --subnet требуется для статических адресов; --ip-range отделяет автоматический пул.
  11. Изоляцию сетей реализуют цепочки DOCKER-ISOLATION-STAGE-1 и -2.
  12. Изоляция действует между container'ами, но не защищает от доступа с самого host.

Официальные источники

ИсточникСсылкаЧто подтверждает
Docker: bridge networkshttps://docs.docker.com/engine/network/drivers/bridge/Различия default и user-defined, параметры создания
Docker: network tutorialshttps://docs.docker.com/engine/network/tutorials/standalone/Практика с user-defined сетями
Docker: docker network createhttps://docs.docker.com/reference/cli/docker/network/create/--subnet, --internal, --ip-range, опции драйвера
Docker: docker network connecthttps://docs.docker.com/reference/cli/docker/network/connect/Подключение на лету, псевдонимы
Docker: packet filtering and firewallshttps://docs.docker.com/engine/network/packet-filtering-firewalls/Цепочки изоляции, взаимодействие с host
Docker: legacy container linkshttps://docs.docker.com/engine/network/links/Статус --link

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Port publishing
Главное оглавление

Markdown на GitHub ↗