SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

Настройка сети для контейнеров через Gluetun

Контейнеры с network_mode: service:gluetun не имеют своих интерфейсов. Узнайте, как пробрасывать порты и ограничивать доступ к подсетям через конфигурацию Docker Compose.

Что происходит, когда контейнер подключается к сети gluetun

Контейнер, в котором задан network_mode: service:gluetun, не имеет собственных сетевых интерфейсов. Он присоединяется к сетевому пространству имен (network namespace) gluetun, поэтому публикация портов и правила межсетевого экрана перестают быть свойствами этого контейнера и становятся свойствами сервиса gluetun. Каждый ответ ниже вытекает из этого факта.

Сетевое пространство имен — это изолированная копия сетевого стека в ядре: собственные интерфейсы, собственная таблица маршрутизации, собственные правила межсетевого экрана и собственные слушающие сокеты. По умолчанию Docker создает отдельное пространство для каждого контейнера. Когда вы указываете network_mode: service:gluetun, Docker пропускает этот шаг и помещает новый контейнер в пространство имен, которое уже принадлежит gluetun. Контейнер сохраняет собственную файловую систему, но не собственные /etc/hosts или /etc/resolv.conf: Docker предоставляет ему те же файлы, что использует gluetun, и это важно в дальнейшем.

Вы можете увидеть это напрямую.

docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrent

Эта команда выводит container:, за которым следует ID контейнера gluetun, в то время как обычный контейнер вывел бы bridge. Это руководство продолжает тему маршрутизации трафика Docker через VPN с помощью gluetun: туннель работает, но теперь никто не может связаться с контейнером.

Публикация порта в gluetun, а не в приложении

Оставьте блок ports: в сервисе, который задает network_mode, и Docker откажется создавать контейнер:

Error response from daemon: conflicting options: port publishing and the container type network mode

Причина проста. Публикация порта означает добавление правила NAT (network address translation), которое перенаправляет порт хоста в сетевое пространство имен контейнера, а у этого контейнера его нет. Перенесите отображение в сервис gluetun. Номер порта не меняется, так как приложение по-прежнему слушает этот порт внутри общего пространства имен.

services:
  gluetun:
    ports:
      - "8080:8080/tcp"   # qBittorrent web UI

  qbittorrent:
    network_mode: "service:gluetun"
    # no ports: block here

Блок expose: в зависимом сервисе приводит к такой же ошибке с conflicting options: port exposing and the container type network mode, а блок networks: там же является критическим препятствием: Compose сообщает, что сервис объявляет взаимоисключающие network_mode и networks, и полностью отказывается загружать файл.

Одно последствие проявляется позже. Каждый контейнер в пространстве имен использует общее адресное пространство портов, поэтому два приложения, которые по умолчанию используют 8080, конфликтуют, и второе запущенное приложение завершается с ошибкой address already in use. Измените порт одного из них в его собственной конфигурации, например, через переменную WEBUI_PORT в образе qBittorrent от LinuxServer, а затем опубликуйте новый номер порта в gluetun.

Как контейнеры за gluetun взаимодействуют друг с другом?

Внутри пространства имен они уже используют общий loopback-интерфейс. Контейнер, находящийся за gluetun, обращается к соседнему контейнеру по адресу 127.0.0.1:<port> без участия сети Docker.

За пределами пространства имен у контейнера нет имени. Встроенный DNS-сервер Docker преобразует имя сервиса в его адрес в пользовательской сети, но у данного контейнера нет адреса ни в одной сети. Поэтому обычный контейнер, например Sonarr, не может обратиться к торрент-клиенту по адресу http://qbittorrent:8080. Он обращается к нему по адресу http://gluetun:8080, так как сокет прослушивается в пространстве имен gluetun, на адресе gluetun. Это удивляет тех, кто знаком с тем, как работают сети Docker Compose и имена сервисов, и ожидает применения стандартных правил именования. Это также работает без публикации портов на хосте, поскольку оба контейнера находятся в одной сети Compose.

Прежде чем приступать к отладке чего-либо другого, проверьте DNS. Gluetun запускает собственный DNS-сервер внутри пространства имен, и Docker предоставляет каждому контейнеру за gluetun тот же файл /etc/resolv.conf, что и gluetun. В результате запросы вашего приложения проходят через резолвер gluetun, а не через резолвер хоста. По умолчанию этот резолвер также отклоняет ответы, указывающие на частные адреса, поэтому имя, которое успешно разрешается на хосте, может не разрешаться внутри контейнера. В документации gluetun указано, что такие имена хостов должны быть внесены в DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES.

docker exec qbittorrent cat /etc/resolv.conf

Как получить доступ к сервису, запущенному на Docker-хосте?

Используйте host.docker.internal. Для этого требуется два параметра, оба в сервисе gluetun, так как нарушены две разные вещи.

Сначала имя. Контейнер за gluetun считывает /etc/hosts от gluetun, а Docker отклоняет запись extra_hosts в сервисе, который устанавливает network_mode с помощью conflicting options: custom host-to-IP mapping and the network mode. Поэтому запись должна находиться в gluetun, и каждый контейнер в этом пространстве имен будет её видеть.

  gluetun:
    extra_hosts:
      - "host.docker.internal:host-gateway"

host-gateway — это специальное значение, которое Docker заменяет на внутренний адрес самого хоста. В стандартной установке Docker на Linux это адрес моста docker0, обычно 172.17.0.1. Проверьте свой адрес с помощью ip -4 addr show docker0 на VPS. Docker Desktop разрешает это имя самостоятельно, поэтому руководства, написанные для ноутбуков, пропускают строку extra_hosts, а тот же файл затем не работает на сервере.

Затем маршрут. Добавление имени лишь указывает контейнеру, какой адрес использовать. Пакет всё равно уходит через маршрут по умолчанию для gluetun, то есть через туннель, и брандмауэр gluetun отбрасывает его. Симптомом является соединение, которое зависает, а затем выдает тайм-аут, а не соединение, которое отклоняется. Отказ означает, что пакет дошел и получил ответ «нет». Тайм-аут означает, что пакет не дошел вовсе.

  gluetun:
    environment:
      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32

Затем проверьте, что сервис на хосте действительно слушает этот адрес. PostgreSQL-сервер, привязанный только к 127.0.0.1, недоступен из любого контейнера, имеющего собственное сетевое пространство имен, включая все контейнеры за gluetun, независимо от наличия туннеля, потому что 127.0.0.1 внутри пространства имен — это собственный loopback этого пространства. Только контейнер в режиме host networking использует loopback хоста, а контейнер за gluetun уже потратил свой network_mode на gluetun. Вместо этого привяжите PostgreSQL к 172.17.0.1: он будет принимать соединения от контейнеров, оставаясь при этом недоступным из публичной сети. Проверьте это с помощью ss -lntp | grep 5432 на хосте.

На что на самом деле влияет FIREWALL_OUTBOUND_SUBNETS

В документации gluetun этот параметр описывается как список подсетей, разделенных запятыми, к которым разрешен доступ для gluetun и контейнеров, использующих его сетевой стек. Важно отметить, что это влечет за собой изменения в правилах брандмауэра и маршрутизации. Оба аспекта имеют значение. Gluetun добавляет маршрут для каждой указанной подсети через шлюз Docker bridge, поэтому пакеты для этих адресов уходят через eth0, а не через VPN-туннель. Также параметр открывает доступ в брандмауэре, так как в противном случае gluetun отбрасывает весь исходящий трафик, который не направлен на VPN-сервер.

Указывайте значение без пробелов после запятых.

      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32

Два свойства часто упускают из виду. Это настройка на уровне пространства имен, поэтому она применяется ко всем контейнерам за gluetun, а не только к тому, который вы имели в виду. Кроме того, это касается только исходящего трафика: параметр управляет соединениями, которые инициирует контейнер. Соединения, поступающие на опубликованный порт, проходят по другому пути и не требуют внесения в этот список.

Доступ к веб-интерфейсу с узла Tailscale

Tailscale присваивает каждой машине адрес в 100.64.0.0/10 — диапазоне, зарезервированном для CGNAT. В рамках персональной tailnet это бесплатно, хотя ограничение бесплатного тарифа в шесть пользователей определяет, останется ли это так, когда доступ к тем же интерфейсам потребуется другим людям. Количество пользовательских устройств на любом тарифе не ограничено. Серверы и другие устройства с тегами считаются отдельно: на странице цен Tailscale указано 50 включенных ресурсов с тегами, дополнительные стоят 1 доллар в месяц за каждый. Поскольку биллинг в остальном учитывает пользователей, а не машины, итоговая стоимость платной tailnet зависит в основном от количества приглашенных людей, а не от числа контейнеров, которые вы им предоставляете. Для двух направлений доступа требуются разные подходы.

Входящий доступ — самый простой. Публикация 8080:8080 в gluetun привязывает этот порт ко всем адресам хоста, включая интерфейс tailscale0, поэтому узел открывает http://<machine-name>:8080 и получает доступ к контейнеру. Gluetun не участвует в этом пути, так как правило NAT в Docker находится на хосте, вне пространства имен контейнера.

Чтобы сделать интерфейс доступным только через tailnet, привяжите опубликованный порт к адресу Tailscale на хосте, а не ко всем адресам.

    ports:
      - "100.101.102.103:8080:8080/tcp"

Узнайте этот адрес с помощью tailscale ip -4 на хосте. Привязка здесь является более строгим контролем, чем правило брандмауэра, так как порт вообще не открывается на публичном интерфейсе. Если вы предпочитаете обращаться к интерфейсу по HTTPS-имени, а не по хосту и порту, tailscale serve может выступать фронтендом для этого опубликованного порта, хотя serve и funnel различаются тем, кто в итоге получает доступ, и только один из них оставляет интерфейс внутри tailnet. Это также позволяет обойти проблему, описанную в публикации портов Docker в обход ufw. Паттерн sidecar-контейнера Tailscale для Compose идет дальше и полностью исключает порт хоста, но он работает путем помещения приложения в пространство имен сети sidecar-контейнера, а контейнер за gluetun уже использует свой единственный network_mode для gluetun.

Исходящий доступ — это место, где возвращается FIREWALL_OUTBOUND_SUBNETS. Если контейнеру нужно обратиться к узлу, добавьте адрес этого узла и отдайте предпочтение /32 для каждого узла, а не всему /10. Если машина, к которой вы обращаетесь, находится в частной сети, доступной через VPS, анонсирующий эту подсеть в вашу tailnet, укажите анонсированный диапазон вместо собственного адреса 100.x маршрутизатора и убедитесь, что сам хост принял эти маршруты. Имена MagicDNS по умолчанию не разрешаются внутри контейнера, так как контейнер использует DNS-сервер gluetun, а не резолвер хоста, поэтому используйте числовой адрес 100.x или закрепите имя с помощью строки extra_hosts в сервисе gluetun. Опция DNS_UPSTREAM_PLAIN_ADDRESSES в gluetun позволяет пересылать запросы другому резолверу, но она отправляет каждый запрос в открытом виде, и документация gluetun не рекомендует это делать, так как запросы к частным адресам утекают из VPN-туннеля. То же самое относится к случаю, когда вы используете собственный сервер управления Tailscale с помощью Headscale.

Полный compose-файл для типичной конфигурации

Клиент для загрузки за VPN, два веб-интерфейса, доступных только в tailnet, и один контейнер, который считывает данные из базы данных PostgreSQL, запущенной на хосте.

services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    ports:
      - "127.0.0.1:8000:8000/tcp"        # gluetun control server, host only
      - "100.101.102.103:8080:8080/tcp"  # qBittorrent UI, tailnet only
      - "100.101.102.103:9696:9696/tcp"  # Prowlarr UI, tailnet only
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - ./gluetun:/gluetun
    environment:
      - VPN_SERVICE_PROVIDER=mullvad
      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
      - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
      - SERVER_CITIES=Amsterdam
      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,100.64.7.9/32
      - TZ=Europe/Amsterdam
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Amsterdam
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent:/config
      - /srv/downloads:/downloads
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Amsterdam
      - PROWLARR__POSTGRES__HOST=host.docker.internal
      - PROWLARR__POSTGRES__PORT=5432
      - PROWLARR__POSTGRES__USER=prowlarr
      - PROWLARR__POSTGRES__PASSWORD=${PROWLARR_DB_PASSWORD}
      - PROWLARR__POSTGRES__MAINDB=prowlarr-main
      - PROWLARR__POSTGRES__LOGDB=prowlarr-log
    volumes:
      - ./prowlarr:/config
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

Изучите файл, чтобы понять структуру, а не конкретные продукты. Оба веб-интерфейса опубликованы через gluetun и привязаны к tailnet-адресу хоста, поэтому они отвечают только в сети Tailscale. Строка extra_hosts также находится в gluetun, так как Prowlarr считывает /etc/hosts из gluetun при разрешении host.docker.internal, а Docker отклонил бы эту строку в конфигурации самого Prowlarr. FIREWALL_OUTBOUND_SUBNETS указывает два отдельных адреса: адрес Docker-моста хоста, чтобы Prowlarr мог установить соединение с базой данных, и адрес одного узла tailnet.

Сервер PostgreSQL намеренно отсутствует в файле. Он работает на VPS как обычный системный сервис, слушающий 172.17.0.1:5432. Это та же схема уровней, что и в стеке arr на Docker Compose, но база данных вынесена за пределы Docker.

Не храните закрытый ключ WireGuard в compose-файле. ${WIREGUARD_PRIVATE_KEY} считывает его из файла .env, расположенного рядом; этот подход описан в env-файлы и секреты для Docker Compose. Блок condition: service_healthy использует проверку работоспособности (healthcheck), встроенную в образ gluetun, поэтому службы не запустятся, пока туннель не сообщит о готовности. В Compose healthchecks описан общий принцип работы.

Публикация на всех адресах вместо только tailnet

Удалите префикс адреса и привязки портов в 0.0.0.0, что включает публичный IP-адрес VPS. Делайте это только при наличии настроенного вами межсетевого экрана и предварительно ознакомившись с примечанием о ufw выше.

    ports:
      - "8080:8080/tcp"

Подтверждение того, что туннель передает трафик

Выполните один и тот же запрос дважды: сначала изнутри пространства имен, а затем с хоста, после чего сравните результаты.

docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.org

Первый запрос должен вывести выходной IP-адрес вашего VPN-провайдера. Второй — IP-адрес вашего VPS. Если они совпадают, значит, трафик контейнера не проходит через туннель, и все исправления из этого руководства не имеют смысла, пока это не будет исправлено.

Правила маршрутизации и таблицы показывают, какой трафик идет через туннель, а какой — нет.

docker run --rm --network=container:gluetun alpine:3.22 sh -c 'ip rule show; ip route show table 51820; ip route show table 199'

При настройке WireGuard, описанной в этом руководстве, gluetun называет интерфейс туннеля wg0. Имя tun0 является значением по умолчанию только для OpenVPN. Gluetun также выносит маршрут по умолчанию для туннеля из основной таблицы маршрутизации. ip rule show должен содержать правило, которое направляет весь трафик, не помеченный самим WireGuard, в таблицу 51820, а эта таблица должна содержать маршрут по умолчанию через wg0. Таблица 199 содержит по одному маршруту для каждой записи в FIREWALL_OUTBOUND_SUBNETS, каждый из которых указывает на шлюз Docker bridge по адресу eth0, а правило для этой таблицы проверяется непосредственно перед правилом туннеля. Обычная команда ip route show по-прежнему показывает маршрут по умолчанию для Docker через eth0. Это ожидаемое поведение, и утечкой оно не является, так как оба правила gluetun проверяются перед основной таблицей. Любой маршрут в таблице 199, который вы не указывали, означает, что трафик идет в обход VPN.

Сервер управления gluetun может сообщать тот же публичный IP-адрес на порту 8000 по адресу /v1/publicip/ip, но по умолчанию это возможно только после настройки аутентификации. В документации gluetun указано, что все маршруты сервера управления теперь по умолчанию являются приватными, поэтому обычный запрос по этому пути завершится ошибкой. Настройте его с помощью файла аутентификации, подключенного (bind mount) по пути /gluetun/auth/config.toml, используя метод basic или apikey, а затем перезапустите gluetun. В приведенном выше файле compose уже настроено монтирование ./gluetun в /gluetun, поэтому файл на хосте должен располагаться по пути ./gluetun/auth/config.toml. Переменная HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE — более быстрый способ. Установка для нее значения {"auth":"none"} полностью отключает аутентификацию, что в документации отмечено как крайне нежелательное действие. В версиях до v3.39.1 сервер не защищен таким образом, и документация предупреждает, что доверять следует любому, кто имеет к нему доступ.

Утечка из-за неверной подсети

FIREWALL_OUTBOUND_SUBNETS — это намеренно созданная брешь в межсетевом экране, поэтому размер риска прямо пропорционален размеру этой бреши. Четыре способа сделать её чрезмерно большой:

  • 0.0.0.0/0 отправляет весь трафик вне туннеля. Две проверки IP, описанные выше, выявят это при первом же запуске, так как они вернут одинаковый адрес.
  • Диапазон шире, чем требуется для целевого узла. Открытие 10.0.0.0/8 для доступа к одной машине по адресу 10.0.1.7 также открывает доступ ко всем адресам, которые может анонсировать участник торрент-раздачи в этом диапазоне. Указывайте 10.0.1.7/32.
  • Диапазон, перекрывающий собственные адреса туннеля. Документация gluetun предупреждает, что это заставляет gluetun отправлять VPN-трафик через мост, что нарушает перенаправление портов. Проверьте значение WIREGUARD_ADDRESSES перед открытием любого частного диапазона.
  • 100.64.0.0/10 для Tailscale. Это открывает примерно четыре миллиона адресов ради доступа к одному узлу. Перечисляйте нужные узлы как записи /32.

Помните, что эта настройка охватывает всё пространство имён. Открытие подсети для того, чтобы индексатор мог обратиться к хост-сервису, открывает ту же подсеть и для торрент-клиента, использующего это же пространство имён. Повторно запускайте проверку публичного IP после каждого изменения этой переменной, так как это единственный тест, показывающий, привело ли изменение к ожидаемому результату.

Что нарушается при перезапуске gluetun

Gluetun управляет пространством имен, поэтому жизненный цикл gluetun определяет жизненный цикл всего пространства имен. Запуск зависимого контейнера при неработающем gluetun завершается немедленной ошибкой:

Error response from daemon: cannot join network namespace of a non running container: container <id> is exited

Перезапуск gluetun на месте приводит к более скрытой ошибке. Зависимые контейнеры продолжают работать, в то время как пространство имен, к которому они подключены, пересоздается под ними. В результате docker ps сообщает, что всё в порядке, но запросы остаются без ответа. После любых изменений в сервисе gluetun пересоздавайте всю группу целиком, а не перезапускайте её отдельные части.

docker compose up -d --force-recreate

То же самое относится к обновлению образов. Загрузка нового образа gluetun и пересоздание только этого сервиса приводит к тому, что остальные контейнеры ссылаются на пространство имен, которое больше не существует.

FAQ

Почему Docker выдает ошибку о публикации портов и сетевом режиме контейнера?

Потому что блок ports: все еще присутствует в сервисе, для которого также задан параметр network_mode: service:gluetun. Публикация порта добавляет правило NAT, которое перенаправляет порт хоста в собственное сетевое пространство имен контейнера, а у контейнера в этом режиме его нет. Удалите блок ports: из этого сервиса и добавьте идентичное отображение в сервис gluetun. Номер порта остается прежним, так как приложение по-прежнему ожидает соединений на нем внутри общего пространства имен.

Как другие контейнеры могут обратиться к сервису, который находится за gluetun?

Контейнеры внутри одного пространства имен обращаются друг к другу по адресу 127.0.0.1. Контейнеры вне его используют имя сервиса gluetun, поэтому http://gluetun:8080 работает там, где http://qbittorrent:8080 не работает. Контейнер приложения не имеет адреса ни в одной сети Docker, поэтому встроенному DNS-серверу нечего разрешать для его имени. Публикация портов для этого не требуется, если оба контейнера находятся в одной сети Compose.

Что нужно указать в FIREWALL_OUTBOUND_SUBNETS?

Только те адреса, к которым контейнер за gluetun должен инициировать соединение, указанные максимально узко. Один компьютер — это /32. Две типичные записи — это хост Docker по адресу 172.17.0.1/32 и один /32 для каждого узла Tailscale, к которому вы обращаетесь. Никогда не добавляйте 0.0.0.0/0 и не добавляйте диапазон, который перекрывается с адресами вашего собственного VPN-туннеля. Входящие соединения на опубликованный порт здесь не требуют записей.

Почему контейнер не может разрешить мои имена Tailscale MagicDNS?

MagicDNS работает путем перенаправления резолвера хоста на DNS-сервер Tailscale, а контейнер не использует резолвер хоста. Он использует /etc/resolv.conf, который разделяет с gluetun, указывающий на собственный DNS-сервер gluetun. Проверьте это с помощью docker exec <container> cat /etc/resolv.conf. Используйте числовой адрес 100.x узла или закрепите имя с помощью записи extra_hosts в сервисе gluetun. Gluetun может пересылать запросы другому резолверу с помощью DNS_UPSTREAM_PLAIN_ADDRESSES, но это отправляет их в открытом виде, и документация не рекомендует так делать.

Как убедиться, что трафик по-прежнему идет через VPN?

Выполните один запрос изнутри пространства имен и такой же запрос с хоста, затем сравните ответы. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org должен вернуть выходной адрес вашего VPN-провайдера, в то время как curl -s https://api.ipify.org на VPS вернет адрес VPS. Если два ответа совпадают, значит, туннель не передает трафик контейнера. Выполняйте эту проверку снова после каждого изменения в FIREWALL_OUTBOUND_SUBNETS.

Была ли эта статья полезной?