Моя домашняя лаборатория подключена к интернету через маршрутизатор с прошивкой 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, который стоит первым в списке.

План:

  1. Установить BIND;

  2. Отключить DNS-forwarding в dnsmasq, оставив его обслуживать DHCP запросы;

  3. Настроить базовые вещи;

  4. Включить и протестировать динамическое обновление зон;

  5. Автоматизировать обновление в 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.

Комментарии (0)