Поднимаем свой DNS сервер Unbound и блокировщик рекламы Pihole в docker
Приветствую!

Как вы поняли из названия, сегодня мы установим и запустим в docker локальный DNS сервер Unbound в связке с блэкджеком.. блокировщиком рекламы посредством DNS запросов — Pi-hole. Все управление этим хозяйством будет производиться через удобный веб GUI программы Pi-hole. А в конце статьи покажу, как этот локальный DNS подключить к нашему VPN серверу на базе OpenConnect, который мы настроили ранее. Будет интересно и не сложно, почти)

Все действия были протестированны в среде дистрибутивов Debian 12 и Ubuntu 22.04.

Введение

Unbound — это Open source продукт от компании NLnet Labs, представляющий собой удостоверяющий, рекурсивный и кэширующий DNS сервер. Распространяется под лицензией BSD.

Pi-hole — это Linux приложение для блокировки рекламы и интернет-трекеров на сетевом уровне, которое действует как DNS-sinkhole и, опционально, как DHCP-сервер. Обычно используется в частных сетях. Также имеет открытый код и распространяется под лицензией EUPL.

Те, кто уже знаком с Pihole — знают, что он тоже в своем арсенале имеет легковесный DNS сервер: Dnsmasq. Но он не поддерживает рекурсивные DNS запросы. Т.е. он изначально рассчитан на перессылку запросов на сторонний, рекурсивный DNS сервер, которым в нашем случае будет выступать Unbound, для получения данных об неизвестных адресах. Но Dnsmasq умеет кэшировать ответы и управлять локальными DNS записями.

Вот грубая схема взаимодействия:

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

Но стоит отметить, что такая схема работы имеет очевидный недостаток в виде задержки при выполнении рекурсивных запросов. Ускорить процесс можно настроив перенаправление на ближайший к вам сторонний публичный DNS сервер. Такой вариант тоже имеет место быть, особенно если шифровать запросы с помощью TLS. Но в таком случае вам придется довериться выбранному публичному DNS резолверу. Тут уже на ваше усмотрение.

Прошу обратить внимание, что данная статья не ставит целью разобрать все тонкости работы системы доменных имён и в частности сервера Unbound. Мы лишь разворачиваем личные сервисы 😉. Подразумевается, что у вас есть минимальное представление о технологии DNS.

Подготовка

Для развертывания сервисов нам требуется установленный Docker engine. Если он еще не установлен, то инструкция воть: Установка Docker engine на Linux сервер под управлением Debian

Теперь выполним некоторые подготовительные действия:

BASH
sudo -s

addgroup --system --gid 14956 pihole

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

cd /opt/pihole
Нажмите, чтобы развернуть и увидеть больше

BASH
docker network create \
    --opt com.docker.network.bridge.name=br_vpn \
    --driver bridge --subnet 10.10.11.0/24 vpn_network

docker network ls
Нажмите, чтобы развернуть и увидеть больше

Как видно, мы создали сеть с адресацией 10.10.11.0/24, названием vpn_network и именем виртуального сетевого устройства-моста (bridge) — br_vpn:

На этом этапе подготовка завершена, приступаем к созданию файла описания сервисов-контейнеров docker.

Создание docker-compose.yml

Находясь в директории /opt/pihole открываем новый файл на редактирование любым удобным консольным редактором. Мои читатели знают, что в качестве такового я предпочитаю Vim/Neovim.

Не премину сообщить, что у меня на сайте есть цикл статей про данный редактор. Найдёте их у старого дуба по 😉

BASH
vim docker-compose.yml
Нажмите, чтобы развернуть и увидеть больше

И вставляем в него следующее содержимое:

YAML
---

networks:
  vpn_network:
    external: true

services:

  unbound:
    image: r4venme/unbound:1.17
    container_name: unbound
    restart: on-failure
    stop_grace_period: 30s
    # healthcheck:
    #   disable: true
    deploy: &default_deploy
      resources:
        limits:
          cpus: '0.70'
          memory: 512M
        reservations:
          cpus: '0.2'
          memory: 256M
    logging: &default_logging
      driver: json-file
      options:
        max-size: "50m"
        max-file: "5"
    cap_add:
      - NET_ADMIN
    hostname: "unbound"
    environment:
      TZ: "Europe/Moscow"
      PUID: 14956
      PGID: 14956
    volumes:
      - "./data/unbound/:/etc/unbound/"
    expose:
      - "53/udp"
      - "53/tcp"
    # ports:
    #   - "53:53/udp"
    #   - "53:53/tcp"
    networks:
      vpn_network:
        ipv4_address: 10.10.11.200
        aliases:
          - unbound-server

  pihole:
    # depends_on: [unbound]
    container_name: pihole
    image: pihole/pihole:2025.03.0
    restart: on-failure
    stop_grace_period: 30s
    deploy: *default_deploy
    logging: *default_logging
    cap_add:
      - NET_ADMIN
      # - SYS_TIME
      # - SYS_NICE
    hostname: pihole
    dns:
      - 10.10.11.200
      - 10.10.11.200
    environment:
      TZ: "Europe/Moscow"
      PIHOLE_UID: 14956
      PIHOLE_GID: 14956
      FTLCONF_webserver_api_password: 'pihole_password'
      FTLCONF_dns_listeningMode: 'all'
      FTLCONF_dns_upstreams: 10.10.11.200;10.10.11.200
    volumes:
      - "./data/pihole/:/etc/pihole/"
      # - "./data/dnsmasq/:/etc/dnsmasq.d/"
    expose:
      - "53/tcp"
      - "53/udp"
      - "80/tcp"
    ports:
      - "127.0.0.1:8081:80"
      # - "53:53/tcp"
      # - "53:53/udp"
      # - "80:80/tcp"
      # - "443:443/tcp"
      # - "67:67/udp"
      # - "123:123/udp"
    networks:
      vpn_network:
        ipv4_address: 10.10.11.100
        aliases:
          - pihole-dns
Нажмите, чтобы развернуть и увидеть больше

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

Актуальная версия файла docker-compose.yml также доступна на GitHub.

Кратко пробежимся по содержимому.

На всякий случай ссылку на справку по наполнению файлов compose оставлю тут и в конце статьи.

Ах да, еще один момент. В переменной FTLCONF_webserver_api_password: 'pihole_password' задается пароль админа при входе в GUI pihole. Если оставить его пустым, то пароль сгенерируется автоматически.

Теперь сохраняем файл и переходим к запуску контейнеров.

Запуск Pihole + Unbound

В директории проекта запускаем наши контейнеры такой командой:

BASH
docker compose up -d && docker compose logs -f
Нажмите, чтобы развернуть и увидеть больше

Вы должны увидеть статус запуска Pi-hole и работу процесса разрешения имен проходящих через Unbound:

Если все запустилось корректно, выходим из режима просмотра вывода контейнеров нажмитем Ctrl+c.

Для просмотра статуса запущенных контейнеров воспользуемся командой:

BASH
docker compose ps
Нажмите, чтобы развернуть и увидеть больше

Сервисы зааущены, идем дальше.

Немного про конфигурацию Unbound

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

При первом запуске контейнера Unbound в директории /opt/pihole/data/unbound создаются файлы, необходимые для его работы:

Следующие файлы по умолчанию не используются и приведены в качестве примеров. Их можно подключить через директиву include в основном конфиге unbound.conf:

Например, чтобы настроить перенаправление всех запросов на сторонний сервер в файле unbound.conf подключите файл forward-records.conf:

YAML
include: "/etc/unbound/forward-records.conf"
Нажмите, чтобы развернуть и увидеть больше

В котором настройте перенаправление. Ниже пример перенаправления на opennameserver.org с использованием шифрования TLS (DOT):

YAML
forward-zone:
    name: "."
    forward-tls-upstream: yes
    forward-addr: 217.160.70.42@853#ns1.opennameserver.org
    forward-addr: 213.202.211.221@853#ns2.opennameserver.org
Нажмите, чтобы развернуть и увидеть больше

Для применения настроек нужно перезапустить контейнер Unbound:

YAML
docker compose restart unbound
Нажмите, чтобы развернуть и увидеть больше

Проверка работы DNS с локального хоста

Проверить работу DNS с явным указанием резолвера можно с помощью утилиты dig из пакета dnsutils:

BASH
# установка dig
apt update && apt install dnsutils

# запрос через pihole
dig @10.10.11.100 r4ven.me +short +answer +identify

# запрос через unbound
dig @10.10.11.200 r4ven.me +short +answer +identify
Нажмите, чтобы развернуть и увидеть больше

Обратите внимание на время получения ответа от первого запроса и последующего: это работа механизма кэширования DNS.

В логах Unbound должны быть видны запросы от контейнера Pihole (10.10.11.100) и наш прямой запрос с хоста, который до Unbound (10.10.11.200) идет через bridge (10.10.11.1) соединение, которое мы создали ранее:

Если включить подробный уровень логирования Unbound (параметр verbosity: 3 в unbound.conf), то при первичных запросах вы увидите, как происходят рекурсивные запросы от Unbound к корневым серверам DNS.

Ранее я упоминал, что используемая в этой статье конфигурация Unbound поддерживает работу DNSSEC, которая обеспечивает достоверность получаемых ответов (при наличии соответствующей подписи у домена) с помощью проверки их подписи ключом root.key.

Проверить, что Unbound использует DNSSEC можно выполнив запрос командой dig непосредственно через контейнер Unbound. В ответе должен быть флаг ad (authenticated data):

BASH
# нет dnssec (надо бы добавить)
dig @10.10.11.200 r4ven.me +answer +identify +dnssec | grep ';; flags'
# ничего не выведет
dig @10.10.11.200 r4ven.me +short DNSKEY

# есть подпись dnssec
dig @10.10.11.200 opennameserver.org +answer +identify +dnssec | grep ';; flags'
# выведет ключ подписи
dig @10.10.11.200 +short DNSKEY opennameserver.org
Нажмите, чтобы развернуть и увидеть больше

Обратите внимание, что DNSSEC не подразумевает шифрование передаваемых данных. За это отвечает механизм передачи DNS over TLS (DOT) или HTTPS (DOH).

Проверка работы DNS с внешних хостов

Если вам необходимо отправлять запросы на ваш DNS сервер извне, например из локальной сети, то раскомментируйте эти строки у сервиса pihole в docker-compose.yml:

YAML
...
ports:
  - "53:53/tcp"
  - "53:53/udp"
...
Нажмите, чтобы развернуть и увидеть больше

И перезапустите сервисы:

BASH
docker compose down

docker compose up -d
Нажмите, чтобы развернуть и увидеть больше

Теперь к вашему DNS серверу можно обращаться по доступному IP вашего хоста. В моем примере хост, на котором развернут Unbound+Pihole доступен из локальной сети по адресу 192.168.122.192:

BASH
# запрос через pihole
dig @192.168.122.192 r4ven.me +short +answer +identify
Нажмите, чтобы развернуть и увидеть больше

Всё работает👌.

Если ваша система, на которой подняты Pihole + Unbound использует systemd-resolved, то вероятнее всего у вас не получиться забиндить 53 порт, т.к. он уже прослушивается на локальном интерфейсе: 127.0.0.1:53. В таком случае контейнер с pihole не запуститься. Тут у нас есть два варианта:

  1. отключить systemd-resolved;
  2. изменить локальный IP адрес прослушивания на внешний и перенаправить systemd-resolved на наш контейнеры с pihole и unbound — рассмотрим этот вариант.

Открываем конфиг:

BASH
vim /etc/systemd/resolved.conf
Нажмите, чтобы развернуть и увидеть больше

И меняем следующие параметры:

PLAINTEXT
[Resolve]
DNS=10.10.11.100
FallbackDNS=10.10.11.200
DNSStubListenerExtra=192.168.122.192
Нажмите, чтобы развернуть и увидеть больше

Где:

Перезапускаем службу systemd-resolved и проверяем изменился ли адрес:

BASH
systemctl restart systemd-resolved

ss -tuln | grep 53
Нажмите, чтобы развернуть и увидеть больше

Если у вас на сервере установлен фаервол, то необходимо открыть доступ к порту 53.

Также проверяем резолвинг DNS извне:

BASH
dig @192.168.122.192 r4ven.me +short +answer +identify
Нажмите, чтобы развернуть и увидеть больше

Ответ есть.

Ближе к концу статьи мы рассмотрим способы настройки DNS у клиентов. А пока настроим автозапуск нашего файла compose от сервисного пользователя pihole с помощью системы инициализации Systemd.

Настройка автозапуска с Systemd

Запускать сервисы будем от имени сервисной УЗ, которой предоставим возможность запускать две строго заданные команды от имени привилегированной группы docker.

Создаём файл-юнит сервиса:

BASH
vim /etc/systemd/system/pihole.service
Нажмите, чтобы развернуть и увидеть больше

И наполняем его:

PLAINTEXT
[Unit]
Description=Pihole and Unbound DNS service
Requires=docker.service
After=docker.service

[Service]
Restart=on-failure
RestartSec=5
User=pihole
Group=pihole
ExecStart=/usr/bin/sudo --group=docker /usr/bin/docker compose -f /opt/pihole/docker-compose.yml up
ExecStop=/usr/bin/sudo --group=docker /usr/bin/docker compose -f /opt/pihole/docker-compose.yml down

[Install]
WantedBy=multi-user.target
Нажмите, чтобы развернуть и увидеть больше

Теперь создаем файл с перечнем ограниченных прав sudoers:

BASH
visudo -f /etc/sudoers.d/90_pihole
Нажмите, чтобы развернуть и увидеть больше

Команда visudo позволяет безопасно редактировать файлы sudoers: в случае ошибки синтаксиса она выведет предупреждение и не позволит сохранить файл.

И наполняем:

BASH
Cmnd_Alias PIHOLE_DOCKER = \
    /usr/bin/docker compose -f /opt/pihole/docker-compose.yml up, \
    /usr/bin/docker compose -f /opt/pihole/docker-compose.yml down

pihole ALL = (:docker) NOPASSWD: PIHOLE_DOCKER
Нажмите, чтобы развернуть и увидеть больше

Для лучшего понимания механизма работы sudo и файлов sudoers рекомендую к прочтению мою статью: Командная строка Linux, повышение привилегий: команды su, sudo 😌.

Останавливаем запущенные вручную сервисы и активируем {авто}запуск уже с помощью Systemd:

BASH
docker compose down

systemctl enable --now pihole

systemctl status pihole

docker compose ps
Нажмите, чтобы развернуть и увидеть больше

На вид все рабочее👍 мы молодцы, переходим к веб GUI Pihole.

Подключение к веб GUI Pihole

Если вы разворачивали DNS сервер на машине, с которой и работаете, то просто перейдите по адресу http://localhost/admin.

Если ваш DNS сервер находится удаленно, то с учётом того, что наш GUI слушает порт 80 на локальном порту сервера, необходимо настроить перенаправление портов с помощью SSH. Делается это для безопасного доступа к веб интерфейсу.

На клиентской машине открываем терминал и выполняем:

BASH
# прокидываем порт
ssh -4 -f -N -L 8081:localhost:8081 user@example.com
Нажмите, чтобы развернуть и увидеть больше

Где:

И проверяем:

BASH
ss -tln | grep 8081
Нажмите, чтобы развернуть и увидеть больше

В моём примере команда перенаправления порта такая:

Теперь открываем браузер и переходим по адресу: http://localhost:8081/admin

Вводим пароль, заданный в переменной FTLCONF_webserver_api_password, в моем примере это pihole_password:

И попадаем в наш веб GUI:

Интерфейс pihole, как это принято говорить, интуитивно понятен. В контексте статьи нас тут интересуют локальные записи DNS и списки блокировки рекламных доменов.

Локальные записи DNS

Записи DNS расположены в разделе Local DNS 😉. Для теста добавим несколько новых:

И сразу проверим работу разрешения имен:

BASH
dig @10.10.11.100 pihole.r4ven.me +short +answer +identify

dig @10.10.11.100 unbound.r4ven.me +short +answer +identify
Нажмите, чтобы развернуть и увидеть больше

Списки блокировки рекламных доменов

Списки доменов для блокировки задаются в разделе Adlists. Они представляют собой ссылку на текстовый файл с самим списком в формате hosts: ip_address domain_name

Блокировка достигается простым перенаправлением нежелательного доменного имени на адрес-заглушку 0.0.0.0. Всё гениальное — просто)

Как видно на скриншоте, в Pi-hole уже добавлен один публичный список. Вы можете с легкостью добавлять сюда свои:

Или в файле /opt/pihole/data/pihole/adlists.list.

Переходим к настройкам DNS на клиентах.

Настройка DNS на клиентах

В мире Linux существует множество способов настройки DNS на клиентах, что только запутывает многих пользователей. Я разберу лишь самые популярные способы.

Файл resolv.conf

Самый классический способ настройки DNS в Linux — это файл resolv.conf. Открываем его на редактирование:

BASH
vim /etc/resolv.conf
Нажмите, чтобы развернуть и увидеть больше

И добавляем запись. Важно чтобы она была выше уже существующих параметров nameserver:

BASH
nameserver 192.168.122.192
Нажмите, чтобы развернуть и увидеть больше

Сохраняем, закрываем. Настройки подхватываются на лету.

Systemd-resolved

Во многих современных дистрибутивах Linux для управления DNS используется один из модулей systemd — systemd-resolved, о котором мы уже говорили.

Для редактирования параметров DNS открываем файл конфигурации:

BASH
vim /etc/systemd/resolved.conf
Нажмите, чтобы развернуть и увидеть больше

И в блоке Resolve изменяем (или дополняем) директиву DNS=:

BASH
[Resolve]
DNS=192.168.122.192
Нажмите, чтобы развернуть и увидеть больше

Сохраняем и перезапускаем службу + очищаем кэш DNS:

BASH
systemctl restart systemd-resolved

resolvectl flush-caches
Нажмите, чтобы развернуть и увидеть больше

По опыту, в случае systemd-resolved предпочтительным будет перезапустить систему целиком)

BASH
reboot
Нажмите, чтобы развернуть и увидеть больше

Также для совместимости с приложениями, которые не используют библиотечные вызовы, а обращаются к DNS серверу напрямую, рекомендуется создать такой симлинк (с предварительным бэкапом файла resolv.conf):

BASH
mv /etc/resolv.conf{,.backup}

ln -sv /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
Нажмите, чтобы развернуть и увидеть больше

NetworkManager

А на десктопных линуксах, сетью, в т.ч. и параметрами DNS, управляет NetworkManager. Тут придется покликать мышкой.

Заходим в сетевые соединения через апплет на рабочем столе или через меню приложений:

Далее выбираем наше основное физическое соединение:

Переходим на вкладку настроек IPv4 и задаём адрес DNS сервера:

Готово)

Итоговая проверка

Для проверки корректности настроек DNS в случае resolve.conf и NetwrokManager выполняем:

BASH
nslookup r4ven.me

dig r4ven.me +short +answer +identify
Нажмите, чтобы развернуть и увидеть больше

Имя резолвится через локальный DNS, то, что нужно.

А в случае systemd-resolved команды такие:

BASH
resolvectl dns

resolvectl query r4ven.me
Нажмите, чтобы развернуть и увидеть больше

Отлично.

Теперь проверим добавленные ранее в веб интерфейсе Pi-hole DNS записи:

BASH
ping -c3 unbound.r4ven.me

ping -c3 pihole.r4ven.me
Нажмите, чтобы развернуть и увидеть больше

Слева хост где развернуты pihole + unbound, справа хост из локальной сети.

Как видим, на разных клиентах имена корректно резолвятся, даже если они недоступны😌.

Теперь перейдем к настройке DNS параметров на VPN сервере (если он у вас есть).

Настройка DNS в конфиге OpenConnect (ocserv)

Если вы настраивали ocserv по моей статье, то после добавления в схему DNS сервера, она будет выглядеть так:

Подразумевается, что openconnect и pihole + unbound развернуты на одной машине. И так, для настройки связки первым делом необходимо добавить в compose файл проекта openconnect:

BASH
vim /opt/openconnect/docker-compose.yml
Нажмите, чтобы развернуть и увидеть больше

Определение сети:

BASH
networks:
  vpn_network:
    external: true
Нажмите, чтобы развернуть и увидеть больше

А в параметры контейнер openconnect добавьте директиву networks:

BASH
...
networks:
  - vpn_network
...
Нажмите, чтобы развернуть и увидеть больше

Теперь необходимо отредактировать конфиг файл самого сервера ocserv:

BASH
vim /opt/openconnect/data/ocserv.conf
Нажмите, чтобы развернуть и увидеть больше

Добавив значение параметра dns (первого):

Параметр tunnel-all-dns должен быть true.

После необходимо перечитать конфиг или перезапустить сервис OpenConnect любым способом:

BASH
# перечитывание конфига
docker exec -it openconnect occtl reload

# перезапуск контейнера
docker restart openconnect

# или
systemctl restart openconnect
Нажмите, чтобы развернуть и увидеть больше

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

Заключение

Вот мы и настроили наш локальный DNS сервер на базе Unbound + Pi-hole. А также сконфигурировали обращение к нему клиентов. Дополнительно мы задали адрес DNS сервера в конфиге OpenConnect сервера, тем самым получив завершённое VPN решение.

Конечно мы с вами рассмотрели далеко не все возможности данного стека. Но повторюсь, такой цели и не ставилось. Pi-hole из коробки имеет множество интересных функций, например DHCP сервер, с настройкой в веб GUI и пр. Но это уже выходит за рамки данной статьи. Все источники на документацию вы найдёте ниже.

Подписывайтесь на наш канал в телеграм @r4ven_me чтобы не пропустить новые посты на сайте Вороний блог)

А если остались вопросы, добавляйтесь в наш чат там же: @r4ven_me_chat. Я стараюсь всегда отвечать в нём, в своё свободное время.

Спасибо, что читаете! Успехов вам в резолвинге имён 😉

Полезные источники

Комментарии

Вопрос из чата в телеграм: «Чем различаются dnsmasq и unbound?»

Комментарий из телеграм от Dmitry:

Комментарий из Telegram:

Авторские права

Автор: Иван Чёрный

Ссылка: https://r4ven.me/networking/podnimaem-svoj-dns-server-unbound-i-blokirovshhik-reklamy-pihole-v-docker/

Лицензия: CC BY-NC-SA 4.0

Использование материалов блога разрешается при условии: указания авторства/источника, некоммерческого использования и сохранения лицензии.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша