Собираем docker образ DNS сервера Unbound

Собираем docker образ DNS сервера Unbound

Приветствую!

В этой заметке мы соберем свой Docker образ с современным DNS сервером — Unbound. Рассмотрим подробно Dockerfile, а также мой скрипт подготовки окружения контейнера.

Подписывайтесь на наш телеграм @r4ven_me📱, чтобы не пропустить новые публикации на сайте😉. А если есть вопросы или желание пообщаться по тематике — заглядывайте в Вороний чат @r4ven_me_chat🧐.

По сути это сказ о том, как я собирал контейнер🐳 для своей предыдущей статьи: Поднимаем свой DNS сервер Unbound и блокировщик рекламы Pihole в docker.

Сегодня нам понадобятся📑:

Если все готово, то подключаемся к серверу и приступаем🏃.

Скачивание и обзор файлов проекта

Демонстрацию из статьи я буду проводить в среде дистрибутива Debian 12. Всё +- аналогичным образом может быть воспроизведено и в других системах на базе Linux🙂.

Для скачивания файлов проекта нам понадобится утилита git из одноименной системы контроля версий. Если она у вас еще не установлена, то ставим:

sudo apt update && sudo apt install -y git

Теперь клонируем репозиторий с файлами для сборки в папку /opt:

sudo git clone https://github.com/r4ven-me/unbound /opt/unbound

Давайте подробнее рассмотрим состав проекта (ссылки ведут на GitHub):

  • docker-compose.yml — пример compose файла для запуска собранного (см. src) контейнера с Unbound через docker compose;
  • unbound.service — пример systemd unit-файла, который позволяет запускать Unbound в Docker от имени сервисного пользователя через sudo;
  • unbound_sudoers — пример файла для /etc/sudoers.d/, который разрешает сервисному пользователю unbound запускать (docker compose up/down) контейнер с unbound без пароля;

src — директория с исходными файлами сборки образа Unbound:

  • Dockerfile — сценарий для сборки образа Docker, содержащего Unbound с необходимыми конфигурациями и зависимостями;
  • unbound.sh — скрипт для запуска и управления службой Unbound;
  • unbound.conf — основной конфигурационный файл для настройки параметров работы DNS сервера;
  • root.hints — файл с перечнем корневых DNS-серверов, используемый Unbound для инициализации процесса разрешения имен;
  • root.key — ключ для DNSSEC, используемый для проверки подлинности ответов от корневых серверов;
  • forward-records.conf — пример конфигурационного файла, определяющего правила пересылки DNS-запросов к другим серверам;
  • a-records.conf — пример файла конфигурации, содержащего A-записи для сопоставления доменных имен с IP-адресами;
  • srv-records.conf – пример файла конфигурации, содержащего SRV-записи, которые указывают на расположение служб в сети;
  • check.sh — скрипт для расчета рекомендуемых значений некоторых параметров в unbound.conf;

Подробнее остановимся на файлах сценарии сборки и скрипте запуска Unbound внутри контейнера (кликайте на спойлер):

Dockerfile

Спасибо dafanasiev за твой полезный merge👍

# базовый образ - облегченный Debian 12
FROM debian:12-slim

# подавление интерактивных запросов при установке пакетов
ENV DEBIAN_FRONTEND=noninteractive

# метаданные
LABEL maintainer="Ivan Cherniy <kar-kar@r4ven.me>"

# дизейбл автоматической очистки apt-кеша в Docker
RUN rm -f /etc/apt/apt.conf.d/docker-clean; \
    echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache

# установка пакетов и настройка Unbound
RUN --mount=type=bind,target=/src,source=./ \  # монтирование исходников
    --mount=type=cache,target=/var/cache/apt,sharing=locked \  # кеш пакетов
    --mount=type=cache,target=/var/lib/apt,sharing=locked \  # кеш метаданных
    --mount=type=tmpfs,target=/var/log \  # логи в память
    --mount=type=tmpfs,target=/var/tmp \  # временные файлы в память
    --mount=type=tmpfs,target=/var/cache/debconf \  # конфиги установки в память
    --mount=type=tmpfs,target=/run \  # runtime файлы в память
    --mount=type=tmpfs,target=/tmp \  # временные файлы в память
    set -x && \  # включение режима отладки оболочки
    apt update && \  # обновление кэша apt
    # установка без рекомендуемых зависимостей
    apt install --yes --no-install-recommends --no-install-suggests \
        tini \  # миниатюрный init-процесс для контейнеров
        unbound \  # сам DNS-сервер
        unbound-anchor \  # утилита для управления корневыми ключами DNSSEC
        iproute2 \  # сетевые утилиты (ip, ss и др.)
        iputils-ping \  # утилита ping
        ldnsutils \  # утилиты для работы с DNS (drill и др.)
        bc \  # калькулятор для скриптов
        less \  # pager для просмотра файлов
        ca-certificates \  # SSL-сертификаты доверенных центров
        curl && \  # утилита для взаимодействия с веб
    # изменение UID/GID пользователя unbound
    usermod -u 14956 unbound && \
    groupmod -g 14956 unbound && \
    # инициализация корневого ключа DNSSEC
    { unbound-anchor -a /etc/unbound/root.key; true; } && \
    # загрузка списка корневых DNS-серверов от InterNIC
    curl -sSL https://www.internic.net/domain/named.cache > /etc/unbound/root.hints && \
    # копирование конфигов
    cp /src/*.conf /etc/unbound/ && \
    cp /src/*.conf /usr/share/doc/unbound/ && \
    cp /src/unbound.sh /src/check.sh / && \
    # копирование корневых файлов
    cp /etc/unbound/root.key /etc/unbound/root.hints /usr/share/doc/unbound/ && \
    # изменение владельца конфигов
    chown -R unbound:unbound /etc/unbound/ && \
    # очистка лишних пакетов
    apt purge --yes --auto-remove

# открытие портов DNS
EXPOSE 53/tcp \
       53/udp

# проверка здоровья контейнера - тестовый DNS-запрос
HEALTHCHECK --interval=5m --timeout=20s --start-period=20s \
    CMD drill @127.0.0.1 opennameserver.org > /dev/null || exit 1

# скрипт-точка входа
ENTRYPOINT ["/unbound.sh"]

# команда по умолчанию: запуск Unbound через tini
CMD ["/usr/bin/tini", "--", "/usr/sbin/unbound", "-d", "-p", "-c", "/etc/unbound/unbound.conf"]

Dockerfile — инструкция📒, по которой docker собирает образ на базе Debian 12 (Bullseye), ставит туда unbound версии 1.17.1, создаёт пользователя unbound, копирует конфиги и скрипты в контейнер, а в CMD запускает unbound.sh, который уже всё настраивает и запускает сам DNS-сервер.

Если вам необходима более свежая версия Unbound, используйте в качестве базового образа Debian Sid. Ну или изучите Dockerfile из этого GitHub репозитория — там Unbound собирается из исходников на этапе сборки Docker образа🐧.

unbound.sh

#!/bin/bash

### ПОДГОТОВКА ###
WORK_DIR="/etc/unbound"  # рабочая папка unbound
ROOT_HINTS_URL="https://www.internic.net/domain/named.cache"  # ссылка на корневые сервера
CONF_LIST=("unbound.conf" "forward-records.conf" "srv-records.conf" "a-records.conf")  # список конфигов

# копируем конфиги из шаблонов, если отсутствуют в рабочей папке
for config in "${CONF_LIST[@]}"; do
    if [[ ! -f "${WORK_DIR}"/"${config}" ]]; then
        cp /usr/share/doc/unbound/"${config}" "${WORK_DIR}"
    fi
done

# работа с root.key (ключ DNSSEC)
if [[ -f "${WORK_DIR}"/root.key ]]; then
    # делаем бэкап перед обновлением
    cp "${WORK_DIR}"/root.key{,_backup}

    # пробуем обновить, если не получится - восстанавливаем бэкап
    if ! unbound-anchor -a "${WORK_DIR}"/root.key; then
        mv "${WORK_DIR}"/root.key{_backup,}
    else
        # если обновилось - удаляем бэкап
        rm -f "${WORK_DIR}"/root.key_backup
    fi
else
    # если файла вообще нет - копируем из шаблонов
    cp /usr/share/doc/unbound/root.key "$WORK_DIR"
fi

# работа с root.hints (список корневых серверов)
if [[ -f "${WORK_DIR}"/root.hints ]]; then
    # аналогично делаем бэкап
    cp "${WORK_DIR}"/root.hints{,_backup}

    # пробуем скачать свежий список
    if ! curl -sSL "$ROOT_HINTS_URL" > "${WORK_DIR}"/root.hints; then
        # если не получилось - восстанавливаем
        mv "${WORK_DIR}"/root.hints{_backup,}
    else
        # если ок - удаляем бэкап
        rm -f "${WORK_DIR}"/root.hints_backup
    fi
else
    # если файла нет - копируем из шаблонов
    cp /usr/share/doc/unbound/root.hints "$WORK_DIR"
fi

# фиксим права на рабочую папку
chown -R unbound:unbound "$WORK_DIR"


### ЗАПУСК UNBOUND ###
echo "Запускаем Unbound DNS-сервер..."
# выполняем переданные аргументы (команду из CMD)
exec "$@" || { echo "не удалось запустить :(" >&2; exit 1; }

Небольшой bash скрипт, который на старте проверяет, есть ли нужные конфиги, если нет — копирует из /usr/share/doc/unbound/; обновляет корневой DNS-ключ (root.key) и хинты (root.hints), при этом делает резервные копии на случай неудачи; в конце запускает сам unbound.

Потихоньку переходим к практике😉.

Сборка образа

Собираем наш образ с помощью вот такой команды:

sudo docker build --tag r4venme/unbound /opt/unbound/src/

Где r4venme/unbound – произвольный тег образа. В данном случае я указал имя пользователя на Docker hub и через / название репозитория.

По завершению проверяем:

sudo docker image ls

Образ готов👍.

Сборка под разные архитектуры

При необходимости вы можете собрать образ под разные архитектуры. Чаще всего это делается с последующим пушем в репозиторий образов, например hub.docker.com (требуется предварительная авторизация в репозитории через docker login):

sudo docker buildx create --use

sudo docker buildx build \
    --platform linux/amd64,linux/arm64,linux/arm/v7 \
    --tag r4venme/unbound \
    --push /opt/unbound/src

В команде выше собирается образ для платформ amd64, arm64, armv7.

Запуск и проверка Unbound в docker

Среди файлов проекта есть пример compose файла📁. Используем его для запуска контейнера на основе собранного образа🎛️.

⚠️Обратите внимание, что контейнер, описаннй в файле docker-compose.yml намеренно ограничен в аппаратных ресурсах (директива deploy) на использование cpus: '0.70' и memory: 512M, т.е максимально разрешенное использование CPU составляет 70% одного ядра и 512 мб RAM + резерв. А также явно заданы ограничения на хранения логов контейнеров: 5 файлов по 50 мб. При необходимости скорректируйте данные параметры в соответствии со своими потребностями. Подробнее про лимиты ресурсов сервисов при использовании docker compose читайте тут, а про логирование тут.

Стартуем контейнер в фоновом режиме (detach) и смотрим логи:

sudo docker compose --file /opt/unbound/docker-compose.yml up --detach

sudo docker logs --follow unbound 

⚠️Для выхода из режима просмотра логов нажмите Ctrl+c.

Будут видны записи проверки healthcheck, которая раз в 5 мин. выполняет тестовый DNS запрос.

При необходимости можете отключить эту проверку в docker-compose.yml добавив директиву к сервису:

healthcheck:
  disable: true

Теперь давайте проверим работу DNS сервера не прерывая просмотр логов контейнера. Для этого открываем соседний терминал и устанавливаем утилиту для работы DNS — dig:

sudo apt install -y dnsutils

Выполняем тестовый запрос к IP адресу контейнера (он явно задан в docker-compose.yml):

dig @10.100.100.200 r4ven.me +short +identify

☝️Обратите внимание, что ответ на второй запрос пришел заметно быстрее — это результат работы механизма кэширования. По умолчанию он хранится в ОЗУ контейнера и сбрасывается при перезапуске. Для изменения такого поведения изучите и задайте соответствующие параметры в /opt/unbound/data/etc/unbound.conf.

Если при запуске Unbound включить подробный режим логирования, то в выводе контейнера будут видны запросы к корневым DNS серверам при первичном резолвинге. Сделать это можно изменив команду запуска контейнера в compose файле:

command:
  - /usr/sbin/unbound -vvv -d -p -c /etc/unbound/unbound.conf

Чаще всего это излишне, но иногда полезно для дебага👌.

Ну а как настроить обращения к DNS серверу в контейнере внешних клиентов я рассказывал в отдельном разделе статьи про Unbound+Pihole.

Расчет рекомендуемых значений для Unbound

Отдельно хотел рассказать про скрипт check.sh, логику которого я позаимствовал из другого проекта по сборке Unbound (см. источники в конце).

При запуске без аргументов, на основе количества доступного ОЗУ и ядер процессора данный скрипт рассчитывает значения следующих параметров:

  • msg-cache-size — объём памяти, выделенной для кэширования полных DNS-ответов (включает запрос и ответ);
  • rrset-cache-size — объём памяти для кэша Resource Record Sets (RRsets) — отдельных DNS-записей (A, MX, NS и т.д.);
  • num-threads — количество потоков (ядра CPU), которые Unbound будет использовать;
  • msg-cache-slabs — количество “шардов” (разделов) для msg-cache;
  • rrset-cache-slabs — аналогично, но для rrset-cache.

Расположен скрипт в корне контейнера и запустить его можно так:

sudo docker exec -it unbound /check.sh

Но по причине того, что контейнеры часто ограничивают в ресурсах (см. выше сноску про deploy), а сам контейнер не понимает, что его ограничили😅 (он видит реальные параметры хоста ОЗУ и CPU) — я добавил в скрипт возможность ручного указания параметров.

Например команда расчета рекомендаций для 4Гб ОЗУ (указывается в Мб) и 4 ядра ЦПУ выглядит так:

sudo docker exec -it unbound /check.sh -m 4096 -t 4

Полученные значения можно задать в основном конфиг-файле: /opt/unbound/data/etc/unbound.conf.

Еще раз подчеркну, данные расчеты носят рекомендательный характер😏.

Опционально: запуск Unbound от имени сервисного пользователя с помощью Sudo и Systemd

Я предпочитаю управлять своими сервисами с помощью системы инициализации — Systemd. Поэтому далее покажу один из способов запуска контейнера от имени ограниченного в правах пользователя с помощью sudo + systemd😌.

Останавливаем ранее запущенный сервис:

sudo docker compose --file /opt/unbound/docker-compose.yml down

Создаем сервисного пользователя:

sudo addgroup --system --gid 14956 unbound

sudo adduser --system --gecos 'Unbound DNS service' \
    --disabled-password --uid 14956 --ingroup unbound \
    --shell /sbin/nologin --home /opt/unbound/data unbound

Клик сюда для просмотра описания параметров команд

adgroup – команда создания группы;

  • --system – помечает группу, как системную и применяет к ней установленные политики;
  • --gid 14956 – указывает явный идентификатор группы (GID), как в контейнере;
  • unbound – название создаваемой группы;

adduser – команда создания пользователя

  • --system – помечает группу, как системную и применяет к ней установленные политики;
  • --gecos – позволяет задать описание пользователя;
  • --disabled-password – отключает пароль у пользователя (в таком случае под ним нельзя авторизоваться в системе с помощью пароля);
  • --uid 14956 – указывает явный идентификатор пользователя (UID), как в контейнере;
  • --ingroup unbound – добавляет пользователя в созданную ранее группу unbound;
  • --shell /sbin/nologin – в качестве оболочки пользователя устанавливает nologin – под ней невозможно авторизоваться в системе;
  • --home /opt/unbound/data – задает домашний каталог пользователя, в нашем случае это директория с файлами сервиса unbound;
  • unbound – имя создаваемого пользователя.

Копируем файл с перечнем ограниченных sudo полномочий для пользователя unbound:

sudo cp /opt/unbound/unbound_sudoers /etc/sudoers.d/90_unbound

# на всякий случай проверяем синтаксис
sudo visudo -c -f /etc/sudoers.d/90_unbound

Копируем файл сервиса Systemd, запускаем + включаем автозапуск:

sudo cp /opt/unbound/unbound.service /etc/systemd/system/

sudo systemctl daemon-reload

sudo systemctl enable --now unbound

Проверяем статус и смотрим логи:

sudo systemctl status unbound

sudo journalctl --follow --unit unbound

Вроде все.
Не забывайте про нашу телегу📱 и чат💬 там же. Всех благ!

That should be it. If not, check the logs 🙂

Используемые источники

Подписаться
Уведомить о
0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии