dynamically updating private DNS zone records in OpenWRT
![](https://habrastorage.org/getpro/habr/upload_files/676/311/4af/6763114af949f84e4b836bdff0e9d92d.png)
My home lab is connected to the Internet via a router running OpenWRT firmware. When deploying a local ACME server, I realized that regardless of the type of request validation used, ACME must find the fully qualified domain name of the server for which the certificate is requested in DNS.
While thinking about where to host my private DNS zone, it dawned on me: “But we already have a DNS server in OpenWRT at home. Surely we can remotely update records in its local zone.”
TL;DR: In the end I had to install BIND.
Unfortunately, I couldn't find a good way to update records remotely in dnsmasq
(Default DNS forwarder in OpenWRT).
For my purposes I needed something simple, authoritative and TSIG supported. When comparing DNS server functionality matrices and the OpenWRT repository caught my eye on the following options:
BIND 9.18.24-1
PowerDNS 4.7.4-1
Knot DNS 3.3.5-1
NSD 4.6.1-1
Here they are sorted according to the principle “the more green in the table, the higher in the list”.
At that time, when choosing an option, I was guided by the lack of personal preferences and understanding of what nuances need to be taken into account. Therefore, I bravely started with BIND, which is first on the list.
Plan:
Install BIND;
Disable DNS forwarding in
dnsmasq
leaving it to service DHCP requests;Set up basic things;
Enable and test dynamic zone updates;
Automate updating DNS records for hosts initialized via DHCP.
Router settings:
.lan
— suffix for names in the local network,CIDR of local network:
192.168.1.0/24
Versions:
OpenWrt 23.05.3 arm64
BIND 9.18.24
Disclaimer: By following the instructions below, you risk breaking name resolution on your network, depleting your router's flash memory, or robbing yourself of internet. Before repeating these steps on a real router, try them on an emulator first.
Installation
Before starting the experiment, let's take a look at the active network services:
$ 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
...
As expected, the ports 53
(DNS) and 67
(DHCP) are served dnsmasq
.
Installing the server bind
and a set of utilities for it:
$ opkg update
$ opkg install bind-server bind-tools bind-client
Then we turn it off dnsmasq
the part responsible for DNS, we specify our local domain and configure the DHCP server to send to clients 192.168.1.1
as DNS address:
$ 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
After restarting the services, the active ports should look something like this:
$ 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
...
As we can see, dnsmasq
listens to DHCP, and named
(BIND) listens on ports 53
And 953
.
Let's check that we have access to 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
We have access, we can move on to setup.
Default settings
Immediately after installation the catalog /etc/bind
looks like that:
$ 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
Here:
bind.keys
– trust anchors for the DNS root zone(.
). If you wish, you can compare the contents of the file with what was returned to us above.dig
;db.root
– information about root servers to pre-populate our cache;db.0
,db.255
,db.empty
– reverse lookup zones for broadcast requests;db.local
– forward lookup zone for localhost;db.127
– reverse zone for loopback addresses;named-rndc.conf
– the key and policies that allow the utilityrndc
manage our server locally;rndc.conf
– settings for the mostrndc
;named.conf
– the main BIND configuration file.
Straight out of the box /etc/bind/named.conf
in OpenWRT it looks something like this:
// 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";
};
Setting up local zones
First, let's add a file /etc/bind/db.lan
local forward lookup zone 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
and file /etc/bind/db.1.168.192
reverse lookup zones for our subnet 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.
An overview of zone file syntax can be found Here,
but in short:
Parsing occurs from top to bottom.
;
begins a comment. Everything on the line after it is ignored.()
Brackets are used to split long lines into several shorter ones.When parsing to any domain name that does not end with a dot “
.
“, the current value of the variable is added$ORIGIN
. For example, the entryacme CNAME openwrt
is interpreted asacme.lan. CNAME openwrt.lan.
since four lines above we setlan.
as current value$ORIGIN
.After the class
IN
defined in the recordSOA
it is not necessary to write it in resource records.If you do not write the TTL value in the records, it will be taken as equal to the current value of the variable.
$TTL
.In recording
SOA
meaningns1.lan.
– this is the name of the DNS server that serves this zone,root.lan.
is transmitted to the zone administrator's e-mail addressroot@lan
.If the very first field in a resource record (it is called
OWNER
) is empty, then it is considered equal to the value of this field on the previous line.Names in the reverse zone
in-addr.arpa.
corresponding to IP addresses, are written in reverse order, that is, the IP address10.1.2.3
turns into3.2.1.10.in-addr.arpa.
The parsing rules given above allow to significantly reduce resource recording 1.1.168.192.in-addr.arpa. 900 IN PTR openwrt.lan.
to simple PTR openwrt.lan.
Let's make sure we haven't messed up the syntax:
$ 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
Now we can add the zones themselves /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
Let's check the syntax of the configuration file:
$ named-checkconf -pzx
Reloading the configuration bind
and see if the zones have loaded:
$ 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
And finally, let's check how name resolution works:
$ 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.
Setting up forwarders{}
Now we have a direct connection to DNS, which means that anyone who sees our traffic will know more about what sites we visit than we do. Let's try to fix this by redirecting DNS requests.
Disclaimer: Hiding DNS traffic in the way described below is really a choice between ISP and Big Brother or Dr. Evil and the Illuminati. Of course, if the forwarded requests leave your network via port 53 unencrypted, the ISP still sees it 🙂
Starting with version 9.19.10, BIND can be configured to use TLS for forwarded requests. Unfortunately, the OpenWRT package repository currently only has version 9.18.24. To force DNS-over-TLS (DOT), let's run a DNS proxy stubby
which “out of the box” redirects requests to CloudFlare servers:
$ opkg install ca-certificates
$ opkg install stubby
Let's check which port it is listening on:
$ uci get stubby.global.listen_address
127.0.0.1@5453 0::1@5453
We prescribe 127.0.0.1 port 5453
V /etc/bind/named.conf
and we prohibit direct requests to DNS:
options {
...
forward only;
forwarders {
127.0.0.1 port 5453;
};
...
};
We check and reload the configuration:
$ named-checkconf
$ rndc reload
$ rndc flush
If after enabling redirection name resolution stopped working or you don't want CloudFlare
Here are some tips that helped me:
You can replace CloudFlare in
/etc/config/stubby
You can also comment there.
option log_level '7'
to execute/etc/init.d/stubby reload
which will give more information to syslog. You can view it by runninglogread -f
in the second ssh sessionInstalling root certificates
opkg install ca-certificates
helped to overcome the errorTLS - *Failure* - (20) "unable to get local issuer certificate"
Setting up dynamic zone updates
At the moment, our DNS server is completely static and new records in zone files do not appear by themselves. We can manually edit the forward and reverse zone files and then reload the configuration, but there is a better way: use the utility nsupdate
or any other tool for cryptographically signed DNS updates.
To do this, we will need to generate a TSIG key, adjust the zone configuration in /etc/bind/named.conf
and change the owner of the files.
Note: Many examples from the Internet use the utility to generate TSIG keys dnssec-keygen
. In the current version of BIND it is not suitable for this purpose, since the function of generating HMAC algorithms for use as TSIG keys via dnssec-keygen
was removed in BIND 9.13.0. We will use tsig-keygen
.
Since the conclusion tsig-keygen
is already formatted for inclusion in the config file, we just need to run it:
$ tsig-keygen | tee /etc/bind/keys.conf
key "tsig-key" {
algorithm hmac-sha256;
secret "HAyLN66//YxVF2lrZ6kSZK4TZEpV7WMvzYnNUQ0BvEo=";
};
Then reference the generated file in /etc/bind/named.conf
:
$ cat<<EOF>> /etc/bind/named.conf
include "/etc/bind/keys.conf";
EOF
IN /etc/bind/named.conf
we add allow-update{key tsig-key;}
into the zone configuration, which will allow our key to make any changes:
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;
};
};
Note: if unlimited rights are excessive in your case, you can instead allow-update{}
set a more complex set of rules through update-policy{}.
We check and reload the configuration:
$ named-checkconf
$ rndc reload
A small but very important detail: at the moment the catalog /etc/bind
with all files belongs root:root
We need to change the owner to bind:bind
to named
could create and modify files. At the same time, we will correct the access rights for other system users:
$ chown -R bind:bind /etc/bind
$ chmod 600 -R /etc/bind/*
Testing updating records via nsupdate
Let's add a couple of entries to our zones. To do this, we'll create a file nsupdate.cmd
with the following contents:
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
With this sequence of commands we ask the DNS server to delete all records (if any) for host2.lan.
in the zone lan.
and then add A
recording with TTL=900
For host2.lan.
referring to 192.168.1.2
. The second sequence of commands repeats the algorithm for PTR
reverse zone entries 1.168.192.in-addr.arpa.
Let's launch nsupdate
specifying the path to the file with the key and to the file with the list of commands:
$ 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.
Let's check the result:
$ 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
If we look into the directory with configs, we will see two new files with the extension .jnl
which store the log of changes to records in zones:
$ ls /etc/bind/*jnl
/etc/bind/db.1.168.192.jnl /etc/bind/db.lan.jnl
Disclaimer: in the context of OpenWRT, this was an unpleasant surprise for me, since the recording occurs in the router's flash memory and thus accelerates its degradation. I recommend that you familiarize yourself with this section documentation to estimate the dump frequency in your case. I just accepted as a necessary evil that once every 15 minutes a write to flash might occur. At least until a reliable way to keep these logs on a partition is found /tmp
which is located in the router's RAM (accepting all the risks associated with loss of power supply). Perhaps some sensible advice will be in the comments.
For now, let's just dump the logs into zone files and look at the changes:
$ 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.
As we can see, the records for host2.lan.
appeared in both zones.
Run nsupdate on another host
Now let's check how we can change the record from another machine on the network. To do this, we'll transfer the contents /etc/bind/keys.conf
to the host connected to the router from the LAN side. We will also generate a file nsupdate.cmd
with a list of commands, indicating that the server is located at 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
Let's launch 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
There are no mistakes – it worked!
Managing a Zone via Terraform
In principle, it does not matter which utility we manage the zone with. We only need the name and the TSIG key material.
We create 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
}
Please note the dot (.
) in the content key_name
. Provider hashicorp/dns
requires key names to be formatted as FQDN. In this case, the value of the field name
in the resource configuration it must be specified in a shortened form.
We are installing terraform
/tofu
and run:
[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.
Let's go back to the OpenWRT shell and look at the changes in the zones:
$ 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.
Entries for host100.lan.
appeared.
Now let's remove them:
[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.
Automatic registration of resource records for hosts initialized via DHCP
Up until now, changing DNS records has been manually initiated. Let's add some automation for DHCP clients.
OpenWRT has a mechanism hotplugwhich allows you to run custom scripts when certain events occur in various services. We are interested in DHCP events coming from dnsmasq
and to run our script we just need to place it in the directory /etc/hotplug.d/dhcp/
. The script placed in this directory will be called from /usr/lib/dnsmasq/dhcp-script.sh
which in turn is called by itself dnsmasq
.
Our script will be passed information about the DHCP event in variables: $MACADDR
, $IPADDR
, $HOSTNAME
And $ACTION=("add"|"remove"|"update")
Note: In the case where the client does not transmit its name to the DHCP server, there is a nuance with the $HOSTNAME variable.
Let's create a test script /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 "======================="
We initiate a DHCP event from the host on the LAN side, but we won't send the hostname:
[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)
Let's take a look at the command output logread
on 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: =======================
As we can see, our script sees instead of an empty line 'OpenWrt'
in variable $HOSTNAME
.
At the same time, when the client sends the host name, the system variable $HOSTNAME
is correctly overridden by the value '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.
Command output 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: =======================
I assume that this behavior is intentional, since it is quite easy to filter out client DHCP requests from the router itself.
Let's create a file /etc/hotplug.d/dhcp/00-hello
with the following contents:
#!/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
Restarting dnsmasq
:
$ /etc/init.d/dnsmasq restart
We check the execution of the script by running the following commands on the host connected to the LAN interface of the router:
[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.
while monitoring the system logs in 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.
...
Judging by the logs, direct and reverse records for the host should have appeared in DNS test.lan
with address 192.168.1.122
. We check:
[rocky@test ~]$ nslookup test.lan
Server: 192.168.1.1
Address: 192.168.1.1#53
Name: test.lan
Address: 192.168.1.122
This concludes the setup.
Instead of a conclusion
Okay, now we have our own DNS server with support for dynamic zones and TSIG authentication. What's next?
And then we will build our Let's Encrypt in OpenWRT TLS-ALPN-01
And DNS-01
.