Настройка основного и резервного DNS-серверов с автоинкрементом серийного номера зоны на базе PowerDNS

by Anton Chernousov aka GITA-DEV


Опубликовано: 07 Авг 2018 (последние правки 1 неделя, 2 дня)


Настройка основного и резервного DNS-серверов с автоинкрементом серийного номера зоны на базе PowerDNS

Как вы наверное знаете, вам совсем не обязательно использовать DNS-сервера провайдера для управления вашим доменом и вы можете осуществлять хостинг DNS-записей на своих собственных DNS-серверах. Такой подход дает большую гибкость в управлении DNS-зоной, но и настройка DNS-серверов работающих в режиме MASTER-SLAVE это не самая тривиальная задача. Если вы все же решили изучить этот вопрос, то вы наверное обратили внимание, что 90% статей сводятся к настройке двух DNS-серверов Bind в режиме ведущий-ведомый и может показаться, что bind это единственный Opensource DNS-сервер.

Около трех лет назад, я решал довольно интересную задачу по реализации отказоустойчивого кластера web-серверов и одним из компонентов этой системы был DNS-сервер на базе PowerDNS с хранением данных в базе СУБД Postgresql. Этот элемент отвечал за оперативное переключение DNS-записей Front-узлов Nginx на случай DDos-атаки и выдачу гео-зависимых адресов Front-узлов исходя из IP-адреса запрашивающего.

Сейчас назревает похожий проект и я подумал, а почему бы мне не использовать старые наработки и не освежить в памяти как это вообще было. Тестировать можно на собственных серверах, я все равно хотел настроить Wildcard SSL для Let's Encrypt, а для этого мне в любом случае понадобится подконтрольный мне DNS-сервер, а не сервер провайдера.

В любом случае, вне зависимости от того собираетесь ли вы в ближайшее время настраивать MASTER-SLAVE DNS-сервера, а рекомендую для общего понимания вопроса пробежаться по этой небольшой статье.

Установка и базовая настройка PowerDNS

Установка необходимых пакетов:

# aptitude install pdns-backend-pgsql pdns-server

Разворачиваем схему данных в базу:

# cat /usr/share/doc/pdns-backend-pgsql/schema.pgsql.sql | psql -U web_portal -h 127.0.0.1 web_portal_db

Настраиваем конфигурационный файл /etc/powerdns/pdns.d/pdns.local.gpgsql.conf, он содержит параметры подключения к базе данных (настройте его на подключение к базе куда мы загрузили схему данных):

# PostgreSQL Configuration 
# 
# Launch gpgsql backend 
launch+=gpgsql 
 
# gpgsql parameters 
gpgsql-host=localhost 
gpgsql-port=5432 
gpgsql-dbname=pdns_db 
gpgsql-user=pdns_user 
gpgsql-password=xxxPasswordxxx 
gpgsql-dnssec=yes

Так как наш PowerDNS сервер будет выступать в качестве мастер-сервера в центральную конфигурацию (/etc/powerdns/pdns.conf) обязательно добавьте параметр:

master=yes

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

  • PowerAdmin - web-интерфейс написанный на PHP предназначенный для администрирования PowerDNS при помощи WEB-интерфейса, официальная страница на GITHUB - https://github.com/poweradmin/poweradmin
  • PowerDNS-Admin - еще один web-интерфейс теперь уже написанный на Python и упакованный в Docker-контейнер. Официальная страница на том же GITHUB - https://github.com/ngoduykhanh/PowerDNS-Admin
  • DjangoPowerDNS - как вы наверное поняли из названия, еще одна web-мордочка, но теперь уже написанная на Django, но она использует Python 2.7 и это уже легаси и должно быть переписано на Python 3

Проще всего конечно запустить PowerDNS-Admin так как он изначально задумывался как Docker-контейнер (хотя конечно можно и без контейнера его запустить), но я предпочитаю не использовать Docker там где можно обойтись.

Запуск PowerDNS-Admin без использования Docker-контейнера

Официальную инструкцию вы можете найти по адресу: https://github.com/ngoduykhanh/PowerDNS-Admin/wiki/Running-PowerDNS-Admin-on-Ubuntu-16.04---Ubuntu-18.04, я в свою очередь опишу последовательность установки которую я применял когда запускал этот проект.

Устанавливаем необходимые пакеты:

# apt-get install python3-dev libsasl2-dev libldap2-dev libssl-dev libxml2-dev \
  libxslt1-dev libxmlsec1-dev libffi-dev pkg-config python3-virtualenv

Устанавливаем Yarn:

# curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
# echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
# apt-get update -y
# apt-get install -y yarn

Клонируем проект из репозитария и создаем виртуальное окружение:

# git clone https://github.com/ngoduykhanh/PowerDNS-Admin.git /opt/web/powerdns-admin
# cd /opt/web/powerdns-admin
# virtualenv -p python3 flask
# source ./flask/bin/activate
# pip install -r requirements.txt
# cp ./config_template.py ./config.py

Отредактируйте файл config.py (подключение к базе и т.п.), после чего инициализируйте базу данных:

# export FLASK_APP=app/__init__.py
# flask db upgrade
# flask db migrate -m "Init DB"
# yarn install --pure-lockfile
# flask assets build
# ./run.py

Большой минус, это то что web-интерфейс использует базу данных исключительно mysql. После тестового запуска web-интерфейс будет доступен по адресу http://localhost:9191.

Заполнение базы данных вручную и тестирование работы PDNS

Базу данных можно заполнить и в ручную без использования WEB-интерфейсов или в том случае когда вам требуется разработать свой интерефейс управления. В простейшем случае, для того чтобы получить работоспособный DNS-сервер обслуживающий MASTER-зону, вам необходимо внести следующие записи:

INSERT INTO domains (name, type) values ('gita-dev.ru', 'MASTER');
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'gita-dev.ru','ns1.gita-dev.ru. anton.gita-dev.ru. 100080 10380 3600 604800 3600','SOA',86400,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'gita-dev.ru','ns1.gita-dev.ru','NS',86400,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'gita-dev.ru','ns2.gita-dev.ru','NS',86400,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'ns1.gita-dev.ru','80.211.102.101','A',3600,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'ns2.gita-dev.ru','94.177.204.179','A',3600,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'gita-dev.ru','80.211.102.101','A',3600,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'www.gita-dev.ru','80.211.102.101','A',3600,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'mail.gita-dev.ru','94.177.204.179','A',3600,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'gita-dev.ru','mail.gita-dev.ru','MX',3600,10);

Проверяем, что pdns-сервер работает корректно при помощи двух команд dig и nslookup. Классический DNS-запрос A-записи у конкретного DNS-сервера выглядит следующим образом:

# nslookup www.gita-dev.ru 127.0.0.1

В современных дистрибутивах по умолчанию уже нет nslookup, а используется более мощная команда dig и запрос MX-записей у локального DNS-сервера будет выполнен вот такой командой:

# dig MX gita-dev.ru @127.0.0.1

В результате выполнения команды вы получите более развернутый ответ от DNS-сервера:

; <<>> DiG 9.10.3-P4-Ubuntu <<>> MX gita-dev.ru @127.0.0.1 
;; global options: +cmd 
;; Got answer: 
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12244 
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 2 
;; WARNING: recursion requested but not available 
 
;; OPT PSEUDOSECTION: 
; EDNS: version: 0, flags:; udp: 1680 
;; QUESTION SECTION: 
;gita-dev.ru.                  IN     MX 
 
;; ANSWER SECTION: 
gita-dev.ru.           3600   IN     MX     10 mail.gita-dev.ru. 
 
;; ADDITIONAL SECTION: 
mail.gita-dev.ru.      3600   IN     A      94.177.204.179 
 
;; Query time: 4 msec 
;; SERVER: 127.0.0.1#53(127.0.0.1) 
;; WHEN: Sun Aug 05 05:31:56 CEST 2018 
;; MSG SIZE rcvd: 77

Настройка MASTER-SLAVE связки DNS серверов PDNS и Bind

В качестве резервного DNS-сервера я буду использовать уже упомянутый в начале статьи Bind, хотя вы можете пойти более интересным путем и настроить потоковую или логическую репликацию хранилища данных Postgresql и использовать его как источник данных для аналогичного PowerDNS-сервера (про потоковую репликацию я уже писал в статье: https://gita-dev.ru/blog/nastrojka-potokovoj-replikatsii-postgresql-servera-wal-replikatsija).

Итак, мы выбрали классический путь и начнем с настройки SLAVE-сервера на базе Bind, для чего естественно его надо установить:

# aptitude install bind9

Не забудьте открыть в Firewall 53-ий UDP-порт (и TCP-порт если используется).

Итак, что нам сейчас требуется сделать:

  • Описать DNS-зону
  • Разрешить передачу зоны с мастер-сервера
  • На мастере настроить оповещения о том, что зона обновлена

Для того чтобы описать SLAVE-зону вам потребуется создать следующую запись в файле /etc/bind/named.conf:

zone "gita-dev.ru" { 
   type slave; 
   file "gita-dev.ru.db"; 
   masters { 93.170.131.222; }; 
};

Если вы сейчас выполните перезапуск DNS сервера BIND, то в логах вы увидите ошибку авторизации на MASTER-сервере, что совершенно логично, так как совершенно незачем передавать кому попало информацию о DNS-записях:

Aug 6 09:38:13 mail named[6329]: zone gita-dev.ru/IN: Transfer started. 
Aug 6 09:38:13 mail named[6329]: transfer of 'gita-dev.ru/IN' from 80.211.102.101#53: connected using 94.177.204.179#43437 
Aug 6 09:38:13 mail named[6329]: transfer of 'gita-dev.ru/IN' from 80.211.102.101#53: failed while receiving responses: NOTAUTH 
Aug 6 09:38:13 mail named[6329]: transfer of 'gita-dev.ru/IN' from 80.211.102.101#53: Transfer status: NOTAUTH 
Aug 6 09:38:13 mail named[6329]: transfer of 'gita-dev.ru/IN' from 80.211.102.101#53: Transfer completed: 0 messages, 0 records, 0 bytes, 0.006 secs (0 bytes/sec)

Как вы понимаете, зону мы описали и теперь надо настроить трансфер зоны с MASTER на SLAVE и для этого вернемся на MASTER-сервер и изменим один параметр конфигурационного файла /etc/powerdns/pdns.conf:

allow-axfr-ips=127.0.0.0/8,::1,94.177.204.179/32

Потребуется перезапуск PDNS-сервера:

# /etc/init.d/pdns restart

Перезапускаем Bind на SLAVE-сервере и проверяем лог-файл:

Aug 6 09:45:15 mail named[6373]: zone gita-dev.ru/IN: Transfer started. 
Aug 6 09:45:15 mail named[6373]: transfer of 'gita-dev.ru/IN' from 80.211.102.101#53: connected using 94.177.204.179#34511 
Aug 6 09:45:15 mail named[6373]: zone gita-dev.ru/IN: transferred serial 100080 
Aug 6 09:45:15 mail named[6373]: transfer of 'gita-dev.ru/IN' from 80.211.102.101#53: Transfer status: success 
Aug 6 09:45:15 mail named[6373]: transfer of 'gita-dev.ru/IN' from 80.211.102.101#53: Transfer completed: 3 messages, 10 records, 320 bytes, 0.083 secs (3855 bytes/sec)

Вот теперь зона передана успешно и мы можем попробовать запросить сведения о DNS-записях уже у SLAVE-сервера:

$ dig www.gita-dev.ru @94.177.204.179 
 
; <<>> DiG 9.10.3-P4-Ubuntu <<>> www.gita-dev.ru @94.177.204.179 
;; global options: +cmd 
;; Got answer: 
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7613 
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 3 
;; WARNING: recursion requested but not available 
 
;; OPT PSEUDOSECTION: 
; EDNS: version: 0, flags:; udp: 4096 
;; QUESTION SECTION: 
;www.gita-dev.ru.              IN     A 
 
;; ANSWER SECTION: 
www.gita-dev.ru.       3600   IN     A      80.211.102.101 
 
;; AUTHORITY SECTION: 
gita-dev.ru.           86400  IN     NS     ns2.gita-dev.ru. 
gita-dev.ru.           86400  IN     NS     ns1.gita-dev.ru. 
 
;; ADDITIONAL SECTION: 
ns1.gita-dev.ru.       3600   IN     A      80.211.102.101 
ns2.gita-dev.ru.       3600   IN     A      94.177.204.179 
 
;; Query time: 100 msec 
;; SERVER: 94.177.204.179#53(94.177.204.179) 
;; WHEN: Mon Aug 06 09:48:25 +07 2018 
;; MSG SIZE rcvd: 128

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

Кстати, на SLAVE-сервере мы не указывали полный путь к файлу где будет храниться база данных зоны у bind и по умолчанию в Ubuntu и Debian она размещается в файле:

/var/cache/bind/gita-dev.ru.db

После первой синхронизации DNS-серверов, нам необходимо указать наши сервера в качестве NS-серверов обслуживающих DNS-зону у хостинг провайдера, где мы приобрели наш домен:

После первичной синхронизации в таблице domains (базы данных Postgresql мастер-сервера) появится запись notified_serial с серийным номером зоны. Следующим этапом добавим DNS-запись в базу данных и инкрементируем серийный номер DNS-зоны:

INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (1,'chat.gita-dev.ru','94.177.216.44','A',3600,NULL);
UPDATE public.records SET content='ns1.gita-dev.ru. anton.gita-dev.ru. 100181 10380 3600 604800 3600' WHERE id = 1;

Проверяем логи на SLAVE-сервере и видим, что синхронизация до номера серийного номера зоны 181 прошла в автоматическом режиме:

Aug 7 09:29:50 mail named[8342]: client 80.211.102.101#15741: received notify for zone 'gita-dev.ru' 
Aug 7 09:29:50 mail named[8342]: zone gita-dev.ru/IN: notify from 80.211.102.101#15741: no serial 
Aug 7 09:29:50 mail named[8342]: zone gita-dev.ru/IN: Transfer started. 
Aug 7 09:29:50 mail named[8342]: transfer of 'gita-dev.ru/IN' from 80.211.102.101#53: connected using 94.177.204.179#43199 
Aug 7 09:29:50 mail named[8342]: zone gita-dev.ru/IN: transferred serial 100181 
Aug 7 09:29:50 mail named[8342]: transfer of 'gita-dev.ru/IN' from 80.211.102.101#53: Transfer status: success 
Aug 7 09:29:50 mail named[8342]: transfer of 'gita-dev.ru/IN' from 80.211.102.101#53: Transfer completed: 3 messages, 12 records, 361 
bytes, 0.116 secs (3112 bytes/sec) 
Aug 7 09:29:50 mail named[8342]: zone gita-dev.ru/IN: sending notifies (serial 100181)

Именно это мне и требовалось сделать, и хотя мы еще не перешли к самому интересному еще немного про SOA-зону и обновления. Во первых, вы может инициировать обновление DNS-зоны в ручном режиме с мастер-сервера командой:

# pdns_control notify gita-dev.ru

Во вторых, рекомендуется использовать для номера зоны специальный формат записи (хотя можно и не обращать на это особого внимания):

example.com.  3600  SOA  dns.example.com. hostmaster.example.com. (
                         1999022301   ; serial YYYYMMDDnn
                         86400        ; refresh (  24 hours)
                         7200         ; retry   (   2 hours)
                         3600000      ; expire  (1000 hours)
                         172800 )     ; minimum (   2 days)

Автоинкремент серийного номера зоны при добавлении, удалении или изменении DNS-записей

Вот мы и пришли к самому интересному и для тестирования автоикремента нам понадобится добавить несколько фэйковых доменов которые мы в дальнейшем удалим. Добавляем соответствующие записи в базу данных:

INSERT INTO domains (name, type) values ('test.local', 'MASTER');
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (2,'test.local','ns1.test.local. anton.test.local. 100080 10380 3600 604800 3600','SOA',86400,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (2,'test.local','ns1.test.local','NS',86400,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (2,'test.local','ns2.test.local','NS',86400,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (2,'ns1.test.local','80.211.102.101','A',3600,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (2,'ns2.test.local','94.177.204.179','A',3600,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (2,'test.local','80.211.102.101','A',3600,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (2,'www.test.local','80.211.102.101','A',3600,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (2,'mail.test.local','94.177.204.179','A',3600,NULL);
INSERT INTO records (domain_id, name, content, type,ttl,prio) VALUES (2,'test.local','mail.test.local','MX',3600,10);

Так мы добавили домен test.local и естественно, что по аналогии мы можем добавить test2.local и test3.local

Автоинкремент серийного номера SOA-записи мы будем выполнять при помощи функции и нескольких триггеров, начнем с функции которая будет инкрементировать серийный номер SOA-записи по переданному ID-домена:

-- FUNCTION: public."IncDomainSerial"(integer)

-- DROP FUNCTION public."IncDomainSerial"(integer);

CREATE OR REPLACE FUNCTION public.IncDomainSerial(
	domid integer)
    RETURNS character varying
    LANGUAGE 'plpgsql'

    COST 100
    VOLATILE 
AS $BODY$
DECLARE
  soacursor CURSOR FOR SELECT id,content FROM records WHERE domain_id=$1 AND type='SOA' LIMIT 1;
  soarecid int;
  res_soaserial varchar;
  soacontent varchar;
  res_soacontent varchar;
BEGIN
   OPEN soacursor;
   FETCH FIRST FROM soacursor INTO soarecid,soacontent;
   CLOSE soacursor;
   res_soacontent=split_part(soacontent,' ', 1) || ' ' ||split_part(soacontent,' ', 2);
   res_soaserial=int4(split_part(soacontent,' ', 3))+1;
   UPDATE records SET content=res_soacontent || ' ' || res_soaserial WHERE id=soarecid;
   RETURN res_soaserial;
END;

$BODY$;

ALTER FUNCTION public."IncDomainSerial"(integer)
    OWNER TO web_portal;

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

SELECT public.IncDomainSerial(2);

Дополнительно функция возвращает новый серийный номер SOA-зоны. Следующим этапом мы создаем уже триггерную функцию которая будет вызывать созданную выше функцию на операциях добавления данных в базу, удаления и обновления.

-- FUNCTION: public.serialupdateondatachange()

-- DROP FUNCTION public.serialupdateondatachange();

CREATE FUNCTION public.serialupdateondatachange()
  RETURNS trigger
  LANGUAGE 'plpgsql'
  COST 100
  VOLATILE NOT LEAKPROOF 
AS $BODY$
BEGIN

IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE') THEN
  IF NEW.type != 'SOA' THEN
    PERFORM "IncDomainSerial"(NEW.domain_id);
    END IF;
    RETURN NEW;
END IF;

IF (TG_OP = 'DELETE') THEN
    PERFORM "IncDomainSerial"(OLD.domain_id);
    RETURN OLD;
END IF;

END;
$BODY$;

ALTER FUNCTION public.serialupdateondatachange()
  OWNER TO web_portal;

Финальным аккордом мы назначаем триггерную функцию таблице:

CREATE TRIGGER "UpdateSerial" BEFORE INSERT OR DELETE OR UPDATE ON records FOR EACH ROW EXECUTE PROCEDURE serialupdateondatachange();

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


Есть вопросы?
Спрашивайте и я обязательно вам отвечу!

* Поля обязательные для заполнения .

Блог это некоммерческий проект! Если вам понравился мой блог и то что я пишу помогло вам на практике, то можете сказать спасибо материально.