Моя домашняя лаборатория подключена к интернету через маршрутизатор с прошивкой OpenWRT. Развертывая локальный ACME сервер, я понял, что, независимо от применяемого типа валидации запросов, ACME должен найти в DNS полное доменное имя сервера, для которого запрошен сертификат.
В размышлениях, где же стоит хостить свою приватную DNS зону, меня озарило: «Но у нас уже есть дома DNS-сервер в OpenWRT. Наверняка можно удаленно обновлять записи в его локальной зоне».
TL;DR: В итоге пришлось поставить BIND.
Увы, мне не удалось найти хороший способ удаленно обновлять записи в dnsmasq
(DNS-форвардер в OpenWRT по умолчанию).
Для моих целей нужно было что-то простое, авторитетное и с поддержкой TSIG. При сравнении матрицы функциональности DNS серверов и репозитория OpenWRT взгляд зацепился за такие варианты:
BIND 9.18.24-1
PowerDNS 4.7.4-1
Knot DNS 3.3.5-1
NSD 4.6.1-1
Здесь они отсортированы по принципу «чем больше зеленого в таблице, тем выше в списке».
На тот момент при выборе варианта я руководствовался отсутствием личных предпочтений и понимания, какие нюансы нужно учитывать. Поэтому отважно начал с BIND, который стоит первым в списке.
План:
Установить BIND;
Отключить DNS-forwarding в
dnsmasq
, оставив его обслуживать DHCP запросы;Настроить базовые вещи;
Включить и протестировать динамическое обновление зон;
Автоматизировать обновление в DNS записей для хостов, инициализирующихся по DHCP.
Настройки роутера:
.lan
— суффикс для имен в локальной сети,CIDR локальной сети:
192.168.1.0/24
Версии:
OpenWrt 23.05.3 arm64
BIND 9.18.24
Дисклеймер: cледуя инструкциям ниже, вы рискуете поломать разрешение имен в своей сети, уменьшить ресурс флэш-памяти роутера или лишить себя интернета. Прежде чем повторять эти шаги на реальном роутере, сначала попробуйте на эмуляторе.
Установка
Перед началом эксперимента взглянем на активные сетевые сервисы:
$ netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1637/dropbear
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 2187/uhttpd
tcp 0 0 192.168.1.1:53 0.0.0.0:* LISTEN 2510/dnsmasq
tcp 0 0 10.1.2.144:53 0.0.0.0:* LISTEN 2510/dnsmasq
tcp 0 0 127.0.0.1:53 0.0.0.0:* LISTEN 2510/dnsmasq
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 2187/uhttpd
udp 0 0 127.0.0.1:53 0.0.0.0:* 2510/dnsmasq
udp 0 0 10.1.2.144:53 0.0.0.0:* 2510/dnsmasq
udp 0 0 192.168.1.1:53 0.0.0.0:* 2510/dnsmasq
udp 0 0 0.0.0.0:67 0.0.0.0:* 2510/dnsmasq
...
Как и ожидалось, порты 53
(DNS) и 67
(DHCP) обслуживаются dnsmasq
.
Устанавливаем сервер bind
и набор утилит к нему:
$ opkg update
$ opkg install bind-server bind-tools bind-client
Затем отключаем в dnsmasq
часть, ответственную за DNS, указываем наш локальный домен и конфигурируем DHCP серевер отсылать клиентам 192.168.1.1
в качестве адреса DNS:
$ uci set dhcp.@dnsmasq[0].port=0
$ uci set dhcp.@dnsmasq[0].domain='lan'
$ uci add_list dhcp.@dnsmasq[0].dhcp_option='6,192.168.1.1'
$ uci commit dhcp
$ /etc/init.d/dnsmasq restart
$ /etc/init.d/named restart
После перезагрузки сервисов активные порты должны выглядеть примерно так:
$ netstat -tulpn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1637/dropbear
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 2187/uhttpd
tcp 0 0 127.0.0.1:953 0.0.0.0:* LISTEN 8511/named
tcp 0 0 127.0.0.1:953 0.0.0.0:* LISTEN 8511/named
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 2187/uhttpd
udp 0 0 192.168.1.1:53 0.0.0.0:* 8511/named
udp 0 0 192.168.1.1:53 0.0.0.0:* 8511/named
udp 0 0 10.1.2.144:53 0.0.0.0:* 8511/named
udp 0 0 10.1.2.144:53 0.0.0.0:* 8511/named
udp 0 0 127.0.0.1:53 0.0.0.0:* 8511/named
udp 0 0 127.0.0.1:53 0.0.0.0:* 8511/named
udp 0 0 0.0.0.0:67 0.0.0.0:* 8767/dnsmasq
...
Как видим, dnsmasq
слушает DHCP, а named
(BIND) слушает порты 53
и 953
.
Давайте проверим, что у нас есть доступ к DNS:
$ dig +dnssec +multi . DNSKEY
; <<>> DiG 9.18.24 <<>> +dnssec +multi . DNSKEY
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19567
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
; COOKIE: a398f023571fde5701000000667c7bbbd959a05085315b53 (good)
;; QUESTION SECTION:
;. IN DNSKEY
;; ANSWER SECTION:
. 171554 IN DNSKEY 256 3 8 (
AwEAAZBALoOFImwcJJg9Iu7Vy7ZyLjhtXfvO1c9k4vHj
Opf9i7U1kKtrBvhnwsOni1sb50gkUayRtMDTUQqvljMM
f4bpkyEtcE5evCzhHbFLq1coL5QOix3mfJm++FvIMaAt
52nOvAdqR/luuI11bA1AmSCIJKAUx147DcfOHYKg3as+
dznn3Iah4cWBMVzDe7PPsFS1AO6gU8EpmiRJ9VMNA09f
OyDuq9+d6sw8UUnJRMAFAuPLhUFjUAOuWOw74BC9lOtM
QpbLMz8pX0CDKdOXDHjyj61nxSSWxPdUjeoxI17lQTpS
PRtqRHFn5Fgj2e+9BVwhhWGDQN8kUVSJHZtQiI0=
) ; ZSK; alg = RSASHA256 ; key id = 5613
. 171554 IN DNSKEY 256 3 8 (
AwEAAdSiy6sslYrcZSGcuMEK4DtE8DZZY1A08kAsviAD
49tocYO5m37AvIOyzeiKBWuPuJ4m9u5HonCM/ntxklZK
YFyMftv8XoRwbiXdpSjfdpNHiMYTTV2oDUNMjdLFnF6H
YSY48xrPbevQOYbAFGHpxqcXAQT0+BaBiAx3Ls6lXBQ3
/hSVOprvDWJCQiI2OT+9+saKLddSIX6DwTVy0S5T4YY4
EGg5R3c/eKUb2/8XgKWUzlOIZsVAZZUSTKW0tX54ccAA
LO7Grvsx/NW62jc1xv6wWAXocOEVgB7+4Lzb7q9p5o30
+sYoGpOsKgFvMSy4oCZTQMQx2Sjd/NG2bMMw6nM=
) ; ZSK; alg = RSASHA256 ; key id = 20038
. 171554 IN DNSKEY 257 3 8 (
AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTO
iW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN
7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5
LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8
efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7
pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLY
A4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws
9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU=
) ; KSK; alg = RSASHA256 ; key id = 20326
. 171554 IN RRSIG DNSKEY 8 0 172800 (
20240711000000 20240620000000 20326 .
k7Tz3FFlPySd/LF69we2WyDwnqf+JTTpJ3sriFGLkq26
MGBD/fioXO4xqcCrnWVF50nKs8CaEQpdI9N0N2rW3fZh
9sVryGEvPiNnxfv8JC9MiMlt5pnVWYyOzDWpt9OAznmv
JVvqhZIi19MvmkEj+S/WQCuJwZUx+0r1Nv8mBrN0dbms
LpH3sjgs8pw8SSL4QCLFlJzmqomt1ncM5ocoWqvOU7Hb
Xgt40Gg0ZiZFqs9IebA62pbu5GAVzJEMoANUqxIo3lAg
2JIEWTpo/+hF3QpaB/SFJ0obrJMi4OULOfY2DCx1jjlq
C4qaiS7c/IaGux2bMwQV1zfRDpu4AA5eSw== )
;; Query time: 9 msec
;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP)
;; WHEN: Wed Jun 26 20:36:11 UTC 2024
;; MSG SIZE rcvd: 1169
Доступ есть, можем переходить к настройке.
Настройки по умолчанию
Сразу после установки каталог /etc/bind
выглядит так:
$ ls -lah /etc/bind
drwxr-xr-x 2 root root 3.4K Jun 26 20:15 .
drwxr-xr-x 1 root root 3.4K Jun 26 20:15 ..
-rw-r--r-- 1 root root 3.8K Feb 16 18:24 bind.keys
-rw-r--r-- 1 root root 237 Feb 16 18:24 db.0
-rw-r--r-- 1 root root 271 Feb 16 18:24 db.127
-rw-r--r-- 1 root root 237 Feb 16 18:24 db.255
-rw-r--r-- 1 root root 237 Feb 16 18:24 db.empty
-rw-r--r-- 1 root root 256 Feb 16 18:24 db.local
-rw-r--r-- 1 root root 3.1K Feb 16 18:24 db.root
-rw-r--r-- 1 root root 281 Jun 26 20:15 named-rndc.conf
-rw-r--r-- 1 root root 982 Feb 16 18:24 named.conf
-rw-r--r-- 1 root root 225 Jun 26 20:15 rndc.conf
Здесь:
bind.keys
- якоря доверия для корневой зоны DNS(.
). Если есть желание, то можно сравнить содержимое файла с тем, что нам выше вернулdig
;db.root
- информация о корневых серверах для предзаполнения нашего кэша;db.0
,db.255
,db.empty
- обратные (reverse lookup) зоны для широковещательных запросов;db.local
- прямая (forward lookup) зона для localhost;db.127
- обратная зона для loopback адресов;named-rndc.conf
- ключ и полиси, позволяющие утилитеrndc
локально управлять нашим сервером;rndc.conf
- настройки для самойrndc
;named.conf
- основной файл конфигурации BIND.
Прямо "из коробки" /etc/bind/named.conf
в OpenWRT выглядит примерно так:
// base named.conf file
// Recommended that you always maintain a change log in this file
// options clause defining the server-wide properties
options {
// all relative paths use this directory as a base
directory "/tmp";
// If your ISP provided one or more IP addresses for stable
// nameservers, you probably want to use them as forwarders.
// Uncomment the following block, and insert the addresses replacing
// the all-0's placeholder.
// forwarders {
// 0.0.0.0;
// };
auth-nxdomain no; # conform to RFC1035
// this ensures that any reverse map for private IPs
// not defined in a zone file will *not* be passed
// to the public network
empty-zones-enable yes;
};
include "/etc/bind/named-rndc.conf";
include "/tmp/bind/named.conf.local";
// prime the server with knowledge of the root servers
zone "." {
type hint;
file "/etc/bind/db.root";
};
// Provide forward mapping zone for localhost (optional)
zone "localhost" {
type primary;
file "/etc/bind/db.local";
};
// Provide reverse mapping zone for the loopback address 127.0.0.1
// zone "0.0.127.in-addr.arpa"
zone "127.in-addr.arpa" {
type primary;
file "/etc/bind/db.127";
};
zone "0.in-addr.arpa" {
type primary;
file "/etc/bind/db.0";
};
zone "255.in-addr.arpa" {
type primary;
file "/etc/bind/db.255";
};
Настройка локальных зон
Для начала добавим файл /etc/bind/db.lan
локальной зоны прямого просмотра lan.
:
; forward zone file for lan.
$ORIGIN .
$TTL 0 ; 0 seconds
lan IN SOA ns1.lan. root.lan. (
1719490275 ; serial
43200 ; refresh (12 hours)
900 ; retry (15 minutes)
1814400 ; expire (3 weeks)
7200 ; minimum (2 hours)
)
$TTL 900 ; 15 minutes
NS ns1.lan.
$ORIGIN lan.
ns1 A 192.168.1.1
openwrt A 192.168.1.1
router CNAME openwrt
acme CNAME openwrt
и файл /etc/bind/db.1.168.192
зоны обратного просмотра для нашей подсети 192.168.1.0/24
:
; reverse zone file for lan.
$ORIGIN .
$TTL 0 ; 0 seconds
1.168.192.in-addr.arpa IN SOA ns1.lan. root.lan. (
1719490269 ; serial
43200 ; refresh (12 hours)
900 ; retry (15 minutes)
1814400 ; expire (3 weeks)
7200 ; minimum (2 hours)
)
$TTL 900 ; 15 minutes
NS ns1.lan.
$ORIGIN 1.168.192.in-addr.arpa.
1 PTR ns1.lan.
PTR openwrt.lan.
Обзор синтаксиса файлов зон можно найти здесь,
но если вкратце:
Парсинг происходит сверху вниз.
;
начинает комментарий. Все, что в строке после него, игнорируется.()
скобки используются для разделения длинных строк на несколько более коротких.При парсинге к любому доменному имени, которое не заканчивается точкой "
.
", добавляется текущее значение переменной$ORIGIN
. Например, записьacme CNAME openwrt
интерпретируется какacme.lan. CNAME openwrt.lan.
, поскольку четырьмя строками выше мы задалиlan.
как текущее значение$ORIGIN
.После того, как класс
IN
определили в записиSOA
, в ресурсных записях его можно не писать.Если в записях не писать значение TTL, то оно будет принято равным текущему значению переменной
$TTL
.В записи
SOA
значениеns1.lan.
- это имя DNS сервера, который эту зону обслуживает,root.lan.
транслируется в e-mail адрес администратора зоныroot@lan
.Если в ресурсной записи самое первое поле (оно называется
OWNER
) пустое, то оно считается равным значению этого поля на предыдущей строке.Имена в обратной зоне
in-addr.arpa.
, соответствующие IP адресам, записываются в обратном порядке, то есть IP адрес10.1.2.3
превращается в3.2.1.10.in-addr.arpa.
Правила парсинга, приведенные выше, позволяют значительно сократить ресурсную запись 1.1.168.192.in-addr.arpa. 900 IN PTR openwrt.lan.
до простого PTR openwrt.lan.
Убедимся, что мы не напутали с синтаксисом:
$ named-checkzone lan /etc/bind/db.lan
zone lan/IN: loaded serial 1719490275
OK
$ named-checkzone 1.168.192.in-addr.arpa /etc/bind/db.1.168.192
zone 1.168.192.in-addr.arpa/IN: loaded serial 1719490269
OK
Теперь можем добавлять сами зоны в /etc/bind/named.conf
:
$ cat <<EOF>> /etc/bind/named.conf
zone "lan" {
type primary;
file "/etc/bind/db.lan";
};
zone "1.168.192.in-addr.arpa" {
type primary;
file "/etc/bind/db.1.168.192";
};
EOF
Проверяем синтаксис файла конфигурации:
$ named-checkconf -pzx
Перезагружаем конфигурацию bind
и смотрим, подгрузились ли зоны:
$ rndc reload
$ rndc zonestatus lan
name: lan
type: primary
files: /etc/bind/db.lan
serial: 1719490275
nodes: 5
last loaded: Thu, 27 Jun 2024 13:01:34 GMT
secure: no
dynamic: no
reconfigurable via modzone: no
$ rndc zonestatus 1.168.192.in-addr.arpa
name: 1.168.192.in-addr.arpa
type: primary
files: /etc/bind/db.1.168.192
serial: 1719490269
nodes: 2
last loaded: Thu, 27 Jun 2024 13:13:03 GMT
secure: no
dynamic: no
reconfigurable via modzone: no
И, наконец, проверяем, как работает разрешение имен:
$ nslookup acme.lan && nslookup 192.168.1.1
Server: 127.0.0.1
Address: 127.0.0.1#53
acme.lan canonical name = openwrt.lan.
Name: openwrt.lan
Address: 192.168.1.1
1.1.168.192.in-addr.arpa name = ns1.lan.
1.1.168.192.in-addr.arpa name = openwrt.lan.
Настраиваем forwarders{}
Теперь у нас есть прямое подключение к DNS, а это значит, что любой, кто видит наш траффик, будет больше нас знать о том, на какие сайты мы ходим. Попробуем исправить это с помощью перенаправления DNS запросов.
Дисклеймер: сокрытие DNS-трафика приведенным ниже способом на самом деле представляет собой выбор между Провайдер и БольшойБрат или же Доктор Зло с Иллюминатами. Конечно, если пересылаемые запросы покидают вашу сеть через порт 53 незашифрованными, интернет-провайдер все равно видит :)
Начиная с версии 9.19.10, в BIND можно настроить использование TLS для перенаправленных запросов. Увы, в репозитории пакетов OpenWRT на данный момент только версия 9.18.24. Для принуждения к DNS-over-TLS (DOT) запустим DNS прокси stubby
, который "из коробки" перенаправляет запросы на сервера CloudFlare:
$ opkg install ca-certificates
$ opkg install stubby
Проверяем, какой порт он слушает:
$ uci get stubby.global.listen_address
127.0.0.1@5453 0::1@5453
Прописываем 127.0.0.1 port 5453
в /etc/bind/named.conf
и запрещаем прямые запросы к DNS:
options {
...
forward only;
forwarders {
127.0.0.1 port 5453;
};
...
};
Проверяем и перезагружаем конфигурацию:
$ named-checkconf
$ rndc reload
$ rndc flush
Если после включения переадресации разрешение имен перестало работать или хочется не CloudFlare
вот несколько советов, которые помогли мне:
Заменить CloudFlare можно в
/etc/config/stubby
Там же можно раскомментить
option log_level '7'
, выполнить/etc/init.d/stubby reload
, что выдаст больше информации в syslog. Просмотреть его можно, запустивlogread -f
во второй сессии sshУстановка рут сертификатов
opkg install ca-certificates
помогла побороть ошибкуTLS - *Failure* - (20) "unable to get local issuer certificate"
Настраиваем динамическое обновление зон
На данный момент наш сервер DNS полностью статичен и новые записи в файлах зон сами по себе не появляются. Мы можем вручную редактировать файлы прямой и обратной зон с последующей перезагрузкой конфигурации, но есть способ лучше: использовать утилиту nsupdate
или любой другой инструмент для криптографически подписанных обновлений DNS.
Для этого нам понадобится сгенерировать ключ TSIG, подправить конфигурацю зон в /etc/bind/named.conf
и сменить владельца файлов.
Примечание: во многих примерах из интернета для генерации ключей TSIG используется утилита dnssec-keygen
. В текущей версии BIND она не годится для этой цели, поскольку функция генерации алгоритмов HMAC для использования в качестве ключей TSIG через dnssec-keygen
была удалена в BIND 9.13.0. Вместо этого мы будем использовать tsig-keygen
.
Поскольку вывод tsig-keygen
уже отформатирован для включения в файл конфигурации, нам просто нужно его запустить:
$ tsig-keygen | tee /etc/bind/keys.conf
key "tsig-key" {
algorithm hmac-sha256;
secret "HAyLN66//YxVF2lrZ6kSZK4TZEpV7WMvzYnNUQ0BvEo=";
};
Затем сослаться на сгенерированный файл в /etc/bind/named.conf
:
$ cat<<EOF>> /etc/bind/named.conf
include "/etc/bind/keys.conf";
EOF
В /etc/bind/named.conf
добавляем allow-update{key tsig-key;}
в конфигурацию зон, что разрешит нашему ключу любые изменения:
zone "lan" {
type primary;
file "/etc/bind/db.lan";
allow-update {
key tsig-key;
};
};
zone "1.168.192.in-addr.arpa" {
type primary;
file "/etc/bind/db.1.168.192";
allow-update {
key tsig-key;
};
};
Примечание: если неограниченные права в вашем случае избыточны, то можно вместо allow-update{}
задать более сложный набор правил через update-policy{}.
Проверяем и перезагружаем конфигурацию:
$ named-checkconf
$ rndc reload
Маленькая, но очень важная деталь: в данный момент каталог /etc/bind
со всеми файлами принадлежит root:root
. Нам нужно поменять владельца на bind:bind
, чтобы named
мог создавать и изменять файлы. Заодно подправим права доступа для остальных системных пользователей:
$ chown -R bind:bind /etc/bind
$ chmod 600 -R /etc/bind/*
Тестируем обновление записей через nsupdate
Давайте добавим пару записей в наши зоны. Для этого создадим файл nsupdate.cmd
со следующим содержимым:
server 127.0.0.1 53
zone lan.
update delete host2.lan.
update add host2.lan. 900 A 192.168.1.2
show
send
zone 1.168.192.in-addr.arpa.
update delete 2.1.168.192.in-addr.arpa.
update add 2.1.168.192.in-addr.arpa. 900 PTR host2.lan.
show
send
Этой последовательностью команд мы просим DNS сервер удалить все записи (если таковые существуют) для host2.lan.
в зоне lan.
, а затем добавить A
запись с TTL=900
для host2.lan.
, ссылающуюся на 192.168.1.2
. Вторая последовательность команд повторяет алгоритм для PTR
записи в обратной зоне 1.168.192.in-addr.arpa.
Запускаем nsupdate
, указывая путь к файлу с ключом и к файлу со списком команд:
$ nsupdate -k /etc/bind/keys.conf nsupdate.cmd
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 0
;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
;; ZONE SECTION:
;lan. IN SOA
;; UPDATE SECTION:
host2.lan. 0 ANY ANY
host2.lan. 900 IN A 192.168.1.2
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 0
;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
;; ZONE SECTION:
;1.168.192.in-addr.arpa. IN SOA
;; UPDATE SECTION:
2.1.168.192.in-addr.arpa. 0 ANY ANY
2.1.168.192.in-addr.arpa. 900 IN PTR host2.lan.
Проверяем результат:
$ host 192.168.1.2 && host host2.lan
2.1.168.192.in-addr.arpa domain name pointer host2.lan.
host2.lan has address 192.168.1.2
Если заглянем в каталог с конфигами, то увидим два новых файла с расширением .jnl
, в которых хранится журнал изменений записей в зонах:
$ ls /etc/bind/*jnl
/etc/bind/db.1.168.192.jnl /etc/bind/db.lan.jnl
Дисклеймер: в контексте OpenWRT это был для меня неприятный сюрприз, так как запись происходит во flash память роутера и тем самым ускоряет ее деградацию. Рекомендую ознакомиться с этим разделом документации для оценки частоты дампов в вашем случае. Я же просто принял как неизбежное зло то, что раз в 15 минут может произойти запись во flash. По крайней мере, до тех пор, пока не найдется надежный способ держать эти журналы на разделе /tmp
, который находится в оперативной памяти роутера (принимая все риски, связанные с потерей электроснабжения). Возможно, толковый совет будет в комментах.
А пока давайте просто сделаем дамп журналов в файлы зон и посмотрим на изменения:
$ rndc sync && cat /etc/bind/db.lan
$ORIGIN .
$TTL 0 ; 0 seconds
lan IN SOA ns1.lan. root.lan. (
1719490277 ; serial
43200 ; refresh (12 hours)
900 ; retry (15 minutes)
1814400 ; expire (3 weeks)
7200 ; minimum (2 hours)
)
$TTL 900 ; 15 minutes
NS ns1.lan.
$ORIGIN lan.
acme CNAME openwrt
host2 A 192.168.1.2
ns1 A 192.168.1.1
openwrt A 192.168.1.1
router CNAME openwrt
$ cat /etc/bind/db.1.168.192
$ORIGIN .
$TTL 0 ; 0 seconds
1.168.192.in-addr.arpa IN SOA ns1.lan. root.lan. (
1719490271 ; serial
43200 ; refresh (12 hours)
900 ; retry (15 minutes)
1814400 ; expire (3 weeks)
7200 ; minimum (2 hours)
)
$TTL 900 ; 15 minutes
NS ns1.lan.
$ORIGIN 1.168.192.in-addr.arpa.
1 PTR ns1.lan.
PTR openwrt.lan.
2 PTR host2.lan.
Как видим, записи для host2.lan.
появились в обеих зонах.
Запускаем nsupdate на другом хосте
Теперь проверим, как можно изменить запись с другой машины в сети. Для этого перенесем содержимое /etc/bind/keys.conf
на хост, подключенный к роутеру со стороны LAN. Также сгенерируем файл nsupdate.cmd
со списком команд, указав, что сервер находится по адресу 192.168.1.1
:
server 192.168.1.1 53
zone lan.
update delete host2.lan.
show
send
zone 1.168.192.in-addr.arpa.
update delete 2.1.168.192.in-addr.arpa.
show
send
Запускаем nsupdate
:
[rocky@test ~]$ nsupdate -k keys.conf nsupdate.cmd
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 0
;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
;; ZONE SECTION:
;lan. IN SOA
;; UPDATE SECTION:
host2.lan. 0 ANY ANY
Outgoing update query:
;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 0
;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
;; ZONE SECTION:
;1.168.192.in-addr.arpa. IN SOA
;; UPDATE SECTION:
2.1.168.192.in-addr.arpa. 0 ANY ANY
Ошибок нет - получилось!
Управляем зоной через Terraform
В принцие, не имеет значения, какой утилитой мы управляем зоной. Нам нужны лишь имя и материал ключа TSIG.
Создаем main.tf
:
# Disclaimer: Storing secrets in plain text within Terraform's configuration
# and state files is strongly discouraged due to the inevitable security risks.
# It is crucial to familiarize yourself with techniques to avoid those.
terraform {
required_providers {
dns = {
source = "hashicorp/dns"
version = "3.4.1"
}
}
}
provider "dns" {
update {
server = "192.168.1.1"
key_name = "tsig-key."
key_algorithm = "hmac-sha256"
key_secret = "HAyLN66//YxVF2lrZ6kSZK4TZEpV7WMvzYnNUQ0BvEo="
}
}
resource "dns_a_record_set" "host100" {
zone = "lan."
name = "host100"
addresses = [
"192.168.1.100"
]
ttl = 900
}
resource "dns_ptr_record" "ptr_192_168_1_100" {
zone = "1.168.192.in-addr.arpa."
name = "100"
ptr = "host100.lan."
ttl = 900
}
Обратите внимание на точку (.
) в содержимом key_name
. Провайдер hashicorp/dns
требует имена ключей оформлять как FQDN. При этом значение поля name
в конфигурации ресурса нужно задавать в укороченной форме.
Устанавливаем terraform
/tofu
и запускаем:
[rocky@test ~]$ terraform init
[rocky@test ~]$ terraform plan
[rocky@test ~]$ terraform apply
Terraform will perform the following actions:
# dns_a_record_set.host100 will be created
+ resource "dns_a_record_set" "host100" {
+ addresses = [
+ "192.168.1.100",
]
+ id = (known after apply)
+ name = "host100"
+ ttl = 900
+ zone = "lan."
}
# dns_ptr_record.ptr_192_168_1_100 will be created
+ resource "dns_ptr_record" "ptr_192_168_1_100" {
+ id = (known after apply)
+ name = "100"
+ ptr = "host100.lan."
+ ttl = 900
+ zone = "1.168.192.in-addr.arpa."
}
Plan: 2 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
dns_ptr_record.ptr_192_168_1_100: Creating...
dns_a_record_set.host100: Creating...
dns_a_record_set.host100: Creation complete after 0s [id=host100.lan.]
dns_ptr_record.ptr_192_168_1_100: Creation complete after 0s [id=100.1.168.192.in-addr.arpa.]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Вернемся в шелл OpenWRT и посмотрим на изменения в зонах:
$ rndc sync && cat /etc/bind/db.lan
$ORIGIN .
$TTL 0 ; 0 seconds
lan IN SOA ns1.lan. root.lan. (
1719490287 ; serial
43200 ; refresh (12 hours)
900 ; retry (15 minutes)
1814400 ; expire (3 weeks)
7200 ; minimum (2 hours)
)
$TTL 900 ; 15 minutes
NS ns1.lan.
$ORIGIN lan.
acme CNAME openwrt
host100 A 192.168.1.100
ns1 A 192.168.1.1
openwrt A 192.168.1.1
router CNAME openwrt
$ cat /etc/bind/db.1.168.192
$ORIGIN .
$TTL 0 ; 0 seconds
1.168.192.in-addr.arpa IN SOA ns1.lan. root.lan. (
1719490281 ; serial
43200 ; refresh (12 hours)
900 ; retry (15 minutes)
1814400 ; expire (3 weeks)
7200 ; minimum (2 hours)
)
$TTL 900 ; 15 minutes
NS ns1.lan.
$ORIGIN 1.168.192.in-addr.arpa.
1 PTR ns1.lan.
PTR openwrt.lan.
100 PTR host100.lan.
Записи для host100.lan.
появились.
Теперь удалим их:
[rocky@test ~]$ terraform destroy
Terraform will perform the following actions:
# dns_a_record_set.host100 will be destroyed
- resource "dns_a_record_set" "host100" {
- addresses = [
- "192.168.1.100",
] -> null
- id = "host100.lan." -> null
- name = "host100" -> null
- ttl = 900 -> null
- zone = "lan." -> null
}
# dns_ptr_record.ptr_192_168_1_100 will be destroyed
- resource "dns_ptr_record" "ptr_192_168_1_100" {
- id = "100.1.168.192.in-addr.arpa." -> null
- name = "100" -> null
- ptr = "host100.lan." -> null
- ttl = 900 -> null
- zone = "1.168.192.in-addr.arpa." -> null
}
Plan: 0 to add, 0 to change, 2 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
dns_ptr_record.ptr_192_168_1_100: Destroying... [id=100.1.168.192.in-addr.arpa.]
dns_a_record_set.host100: Destroying... [id=host100.lan.]
dns_a_record_set.host100: Destruction complete after 0s
dns_ptr_record.ptr_192_168_1_100: Destruction complete after 0s
Destroy complete! Resources: 2 destroyed.
Автоматическая регистрация ресурсных записей для хостов, инициализирующихся по DHCP
До сих пор изменение записей в DNS инициировалось вручную. Давайте добавим немного автоматизации для клиентов DHCP.
В OpenWRT есть механизм hotplug, который позволяет запускать пользовательские скрипты при возникновении определенных событий в различных сервисах. Нас интересуют события DHCP, поступающие от dnsmasq
, и для запуска нашего скрипта нам нужно просто поместить его в каталог /etc/hotplug.d/dhcp/
. Размещенный в этом каталоге скрипт будет вызываться из /usr/lib/dnsmasq/dhcp-script.sh
, который, в свою очередь, вызывается самим dnsmasq
.
Нашему скрипту будет передана информация о событии DHCP в переменных: $MACADDR
, $IPADDR
, $HOSTNAME
и $ACTION=("add"|"remove"|"update")
Примечание: В случае, когда клиент не передает свое имя DHCP-серверу, есть нюанс с переменной $HOSTNAME
Создадим тестовый скрипт /etc/hotplug.d/dhcp/00-hello
:
logger "======================="
logger "I've been supplied with this number of arguments: ${#}"
logger "There they are: ${@}"
logger "Here're variables available to me:"
for var_passed in $(set); do
logger "${var_passed}"
done
logger "======================="
Инициируем событие DHCP с хоста на стороне LAN, но не будем отправлять имя хоста:
[rocky@test ~]$ hostname
test
[rocky@test ~]$ sudo dhclient -r eth0 -v
Listening on LPF/eth0/bc:24:11:2a:92:e3
Sending on LPF/eth0/bc:24:11:2a:92:e3
Sending on Socket/fallback
DHCPRELEASE of 192.168.1.122 on eth0 to 192.168.1.1 port 67 (xid=0x303d9b4c)
Заглянем в вывод команды logread
на OpenWRT:
user.notice root: =======================
user.notice root: I've been supplied with this number of arguments: 0
user.notice root: There they are:
user.notice root: Here're variables available to me:
user.notice root: ACTION='remove'
...
user.notice root: HOSTNAME='OpenWrt'
...
user.notice root: IPADDR='192.168.1.122'
...
user.notice root: MACADDR='bc:24:11:2a:92:e3'
...
user.notice root: =======================
Как видим, наш скрипт вместо пустой строки видит 'OpenWrt'
в переменной $HOSTNAME
.
В то же время, когда клиент отправляет имя хоста, системная переменная $HOSTNAME
правильно переопределяется значением 'test'
.
[rocky@test ~]$ hostname
test
[rocky@test ~]$ sudo dhclient -v -H $(hostname) eth0
Listening on LPF/eth0/bc:24:11:2a:92:e3
Sending on LPF/eth0/bc:24:11:2a:92:e3
Sending on Socket/fallback
DHCPDISCOVER on eth0 to 255.255.255.255 port 67 interval 6 (xid=0x4c02d471)
DHCPOFFER of 192.168.1.122 from 192.168.1.1
DHCPREQUEST for 192.168.1.122 on eth0 to 255.255.255.255 port 67 (xid=0x4c02d471)
DHCPACK of 192.168.1.122 from 192.168.1.1 (xid=0x4c02d471)
bound to 192.168.1.122 -- renewal in 17215 seconds.
Вывод комманды logread
:
user.notice root: =======================
user.notice root: I've been supplied with this number of arguments: 0
user.notice root: There they are:
user.notice root: Here're variables available to me:
user.notice root: ACTION='add'
...
user.notice root: HOSTNAME='test'
...
user.notice root: IPADDR='192.168.1.122'
...
user.notice root: MACADDR='bc:24:11:2a:92:e3'
user.notice root: =======================
Предположу, что такое поведение задано намеренно, так как можно довольно легко отфильтровать клиентские DHCP запросы самого роутера.
Создадим файл /etc/hotplug.d/dhcp/00-hello
со следующим содержимым:
#!/bin/sh
#set -x
if [ "${HOSTNAME}" = "$(uci get system.@system[0].hostname)" ] || [ "${HOSTNAME}" = "" ] || [ "${IPADDR}" = "" ]; then
exit 0
fi
if [ "${ACTION}" != 'remove' ] && [ "${ACTION}" != 'add' ]; then
exit 0
fi
br_lan_ip=$(ip addr show dev br-lan | awk '/inet / {print $2}' | cut -d'/' -f1)
br_lan_ptr_zone=$(dig -x ${br_lan_ip} SOA | awk '/AUTHORITY SECTION:/ {getline; print $1}')
domain=$(uci get dhcp.@dnsmasq[0].domain)
REVERSE_IP=$(echo "${IPADDR}" | awk -F. '{print $4"."$3"."$2"."$1}')
TTL=900
case "${ACTION}" in
add)
logger -p daemon.info -t hotplug.dhcp "Processing \"DHCPAC ${IPADDR} ${MACADDR} ${HOSTNAME}\": Adding RRs for \"${HOSTNAME}.${domain}.\" and \"${REVERSE_IP}.in-addr.arpa.\""
nsupdate -k /etc/bind/keys.conf <<EOL | logger -p daemon.info -t hotplug.dhcp
server 127.0.0.1 53
zone ${domain}.
update delete ${HOSTNAME}.${domain}.
update add ${HOSTNAME}.${domain}. ${TTL} A ${IPADDR}
show
send
zone ${br_lan_ptr_zone}
update delete ${REVERSE_IP}.in-addr.arpa.
update add ${REVERSE_IP}.in-addr.arpa. ${TTL} PTR ${HOSTNAME}.${domain}.
show
send
EOL
# rndc sync
;;
remove)
logger -p daemon.info -t hotplug.dhcp "Processing \"DHCPRELEASE ${IPADDR} ${MACADDR}\": Removing RRs for \"${HOSTNAME}.${domain}.\" and \"${REVERSE_IP}.in-addr.arpa.\""
nsupdate -k /etc/bind/keys.conf <<EOL | logger -p daemon.info -t hotplug.dhcp
server 127.0.0.1 53
zone ${domain}.
update delete ${HOSTNAME}.${domain}.
show
send
zone ${br_lan_ptr_zone}
update delete ${REVERSE_IP}.in-addr.arpa.
show
send
EOL
# rndc sync
;;
esac
exit 0
Перезапускаем dnsmasq
:
$ /etc/init.d/dnsmasq restart
Проверяем выполнение скрипта, запуская следующие команды на хосте, подключенном к LAN интерфейсу роутера:
[rocky@test ~]$ hostname
test
[rocky@test ~]$ sudo dhclient -v -H $(hostname) eth0 -r
Listening on LPF/eth0/bc:24:11:2a:92:e3
Sending on LPF/eth0/bc:24:11:2a:92:e3
Sending on Socket/fallback
DHCPRELEASE of 192.168.1.122 on eth0 to 192.168.1.1 port 67 (xid=0x6f71f069)
[rocky@test ~]$ sudo dhclient -v -H $(hostname) eth0
Listening on LPF/eth0/bc:24:11:2a:92:e3
Sending on LPF/eth0/bc:24:11:2a:92:e3
Sending on Socket/fallback
DHCPDISCOVER on eth0 to 255.255.255.255 port 67 interval 7 (xid=0x46faa10b)
DHCPOFFER of 192.168.1.122 from 192.168.1.1
DHCPREQUEST for 192.168.1.122 on eth0 to 255.255.255.255 port 67 (xid=0x46faa10b)
DHCPACK of 192.168.1.122 from 192.168.1.1 (xid=0x46faa10b)
bound to 192.168.1.122 -- renewal in 20468 seconds.
наблюдая при этом за системными логами в OpenWRT:
$ logread -f
...
dnsmasq-dhcp[1]: DHCPRELEASE(br-lan) 192.168.1.122 bc:24:11:2a:92:e3
hotplug.dhcp: Processing "DHCPRELEASE 192.168.1.122 bc:24:11:2a:92:e3": Adding RRs for "test.lan." and "122.1.168.192.in-addr.arpa."
hotplug.dhcp: Outgoing update query:
hotplug.dhcp: ;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 0
hotplug.dhcp: ;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
hotplug.dhcp: ;; ZONE SECTION:
hotplug.dhcp: ;lan. IN SOA
hotplug.dhcp: ;; UPDATE SECTION:
hotplug.dhcp: test.lan. 0 ANY ANY
named[1681]: client @0xffff95f64280 127.0.0.1#44123/key tsig-key: signer "tsig-key" approved
named[1681]: client @0xffff95f64280 127.0.0.1#44123/key tsig-key: updating zone 'lan/IN': delete all rrsets from name 'test.lan'
hotplug.dhcp: Outgoing update query:
hotplug.dhcp: ;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 0
hotplug.dhcp: ;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
hotplug.dhcp: ;; ZONE SECTION:
hotplug.dhcp: ;1.168.192.in-addr.arpa. IN SOA
hotplug.dhcp: ;; UPDATE SECTION:
hotplug.dhcp: 122.1.168.192.in-addr.arpa. 0 ANY ANY
named[1681]: client @0xffff95de81d0 127.0.0.1#58206/key tsig-key: signer "tsig-key" approved
named[1681]: client @0xffff95de81d0 127.0.0.1#58206/key tsig-key: updating zone '1.168.192.in-addr.arpa/IN': delete all rrsets from name '122.1.168.192.in-addr.arpa'
...
dnsmasq-dhcp[1]: DHCPDISCOVER(br-lan) 192.168.1.122 bc:24:11:2a:92:e3
dnsmasq-dhcp[1]: DHCPOFFER(br-lan) 192.168.1.122 bc:24:11:2a:92:e3
dnsmasq-dhcp[1]: DHCPREQUEST(br-lan) 192.168.1.122 bc:24:11:2a:92:e3
dnsmasq-dhcp[1]: DHCPACK(br-lan) 192.168.1.122 bc:24:11:2a:92:e3 test
hotplug.dhcp: Processing "DHCPAC 192.168.1.122 bc:24:11:2a:92:e3 test": Adding RRs for "test.lan." and "122.1.168.192.in-addr.arpa."
hotplug.dhcp: Outgoing update query:
hotplug.dhcp: ;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 0
hotplug.dhcp: ;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
hotplug.dhcp: ;; ZONE SECTION:
hotplug.dhcp: ;lan. IN SOA
hotplug.dhcp: ;; UPDATE SECTION:
hotplug.dhcp: test.lan. 0 ANY ANY
hotplug.dhcp: test.lan. 900 IN A 192.168.1.122
named[1681]: client @0xffff95f64280 127.0.0.1#46151/key tsig-key: signer "tsig-key" approved
named[1681]: client @0xffff95f64280 127.0.0.1#46151/key tsig-key: updating zone 'lan/IN': delete all rrsets from name 'test.lan'
named[1681]: client @0xffff95f64280 127.0.0.1#46151/key tsig-key: updating zone 'lan/IN': adding an RR at 'test.lan' A 192.168.1.122
hotplug.dhcp: Outgoing update query:
hotplug.dhcp: ;; ->>HEADER<<- opcode: UPDATE, status: NOERROR, id: 0
hotplug.dhcp: ;; flags:; ZONE: 0, PREREQ: 0, UPDATE: 0, ADDITIONAL: 0
hotplug.dhcp: ;; ZONE SECTION:
hotplug.dhcp: ;1.168.192.in-addr.arpa. IN SOA
hotplug.dhcp: ;; UPDATE SECTION:
hotplug.dhcp: 122.1.168.192.in-addr.arpa. 0 ANY ANY
hotplug.dhcp: 122.1.168.192.in-addr.arpa. 900 IN PTR test.lan.
named[1681]: client @0xffff95f64280 127.0.0.1#39719/key tsig-key: signer "tsig-key" approved
named[1681]: client @0xffff95f64280 127.0.0.1#39719/key tsig-key: updating zone '1.168.192.in-addr.arpa/IN': delete all rrsets from name '122.1.168.192.in-addr.arpa'
named[1681]: client @0xffff95f64280 127.0.0.1#39719/key tsig-key: updating zone '1.168.192.in-addr.arpa/IN': adding an RR at '122.1.168.192.in-addr.arpa' PTR test.lan.
...
Судя по логам, в DNS должны были появиться прямая и обратная записи для хоста test.lan
с адресом 192.168.1.122
. Проверяем:
[rocky@test ~]$ nslookup test.lan
Server: 192.168.1.1
Address: 192.168.1.1#53
Name: test.lan
Address: 192.168.1.122
На этом и завершим настройку.
Вместо заключения
Окей, теперь у нас есть свой DNS сервер с поддержкой динамических зон и аутентификацией TSIG. И что дальше?
А дальше мы построим в OpenWRT свой Let's Encrypt c TLS-ALPN-01
и DNS-01
.