This article sets out to describe the process used to set up a BIND nameserver with Dynamically Loadable Zones (DLZ) running with a MySQL backend (with replication).
DLZ allows us to maintain a dynamic database of zone information, located either locally or across the network (and with replication, highly-available). Thus we eliminate the need for zone transfers. Further information on BIND DLZ can be found at the BIND DLZ SourceForge site.
My test environment will use a single BIND instance (running with a replicated slave MySQL backend), with all updates performed on a remote master MySQL server. It would be easy to configure multiple slave MySQL replicas, running BIND, for a highly available solution.
The Operating System used in the enviroment was Red Hat Enterprise Linux AS release 4 (Nahant Update 2) which comes with a included build of MySQL 4.1.12
# rpm -qa | grep mysql
mysql-4.1.12-3.RHEL4.1
mysql-server-4.1.12-3.RHEL4.1
mysql-devel-4.1.12-3.RHEL4.1
You could also opt for a source MySQL build. For a test environment, the included RPMs are fine.
BIND, however, will be built from the latest available source. We will ensure a secure configuration, as well as the inclusion of BIND DLZ functionality. The current release at the time of publishing this article is 9.4.1-P1
# /usr/local/named/sbin/named -v
BIND 9.4.1-P1
The latest BIND source can always be downloaded from ISC.
The first thing to do is make sure that the MySQL daemon (mysqld) is down on both your master node (hereafter referred to as node1) and the slave (node2). Use the service command to check the status on both nodes
both# service mysqld status
mysqld is stopped
On the master node, we'll add a couple of lines to the MySQL configuration file, /etc/my.cnf. You can verify the location of the configuration file with the following command
both# rpm -qlc `rpm -q mysql`
/etc/my.cnf
Edit with your favourite editor (vi ;-)) and make the following additions
node1# vi /etc/my.cnf
log-bin=binlog
server-id=1
As this is our master server, we use a server-id of 1. We also set a binlog filename prefix.
On the slave node, add the appropriate lines to the configuration file to allow for replication
node2# vi /etc/my.cnf
log-bin=binlog
server-id=2
replicate-do-db=dns
Here we set a binlog filename prefix, set a server id of 2 (you could use any number greater than 1), and instruct MySQL that we want to allow replication of the "dns" database.
Next we must start thinking ahead. We'll want to run BIND in a chroot environment for security, so we must allow named to access the MySQL socket file (otherwise the DLZ MySQL driver will be unable to read the socket file outside of the chroot - the default location is /var/lib/mysql/mysql.sock).
So now is a good time to construct our BIND chroot tree. Placing the chroot on a seperate filesystem is the ideal security measure, so we'll place it on /var. An even better filesystem would be a seperate /var/named filesystem, which for a Production system is recommended.
Remember, we're running BIND on the MySQL slave - so only perform this step on node2.
node2# mkdir -p /var/named/{dev,etc,lib,mysql,var} /var/named/var/{log,named,run}
We'll set ownership and permissions over this tree later. For now, just check that mysql can write to the /var/named/mysql directory created above
node2# chown mysql:mysql /var/named/mysql
Once this is done, edit your slaves MySQL configuration file, and update the socket location
node2# grep '^socket' /etc/my.cnf
socket=/var/named/tmp/mysql.sock
Before you can start your MySQL servers, there is one final step to perform. Update the mysqld init script to cater for the change in socket location
node2# cp -p /etc/init.d/mysqld{,.$( date +%Y%m%d )}
node2# grep -- -S /etc/init.d/mysqld
RESPONSE=`/usr/bin/mysqladmin -uUNKNOWN_MYSQL_USER -S /var/named/tmp/mysql.sock ping 2>&1` && break
Next, start your MySQL instances
both# service mysqld start
Once MySQL is up and running, we can start to build our database.
Connect to your master MySQL instance
node1# mysql -u root -p
First, create the "dns" database, and a "dns" user whom will have administrative rights over it
node1_mysql> CREATE DATABASE dns;
node1_mysql> GRANT ALL ON dns.* TO 'dns'@'localhost' IDENTIFIED BY 'PasswordHere';
Next, a "rep_dns" user for replication
node1_mysql> GRANT REPLICATION SLAVE ON *.* TO 'rep_dns'@'slave.example.com' IDENTIFIED BY 'PasswordHere';
Configure, then start, slave
node2# mysql -u root -p -S /var/named/mysql/mysql.sock
node2_mysql> CHANGE MASTER TO
-> master_host='master.example.com',
-> master_user='rep_dns',
-> master_password='PasswordHere';
node2_mysql> START SLAVE;
If you check the MySQL processlist, you'll see the Binlog Dump running
node2# mysqladmin -u root -p -S /var/named/tmp/mysql.sock processlist | grep binlog
| 4 | rep_dns | master.example.com:32818 | | Binlog Dump | 172 | Has sent all binlog to slave;
waiting for binlog to be updated |
Check that the replication has worked
node2_mysql> SHOW DATABASES;
+----------+
| Database |
+----------+
| dns |
| mysql |
| test |
+----------+
3 rows in set (0.00 sec)
Check that you can connect as the "dns" user on the slave
node2# mysql -u dns -p -S /var/named/tmp/mysql.sock
If everything checks out, you're ready to continue.
First, on the master, execute the following SQL to create a table, and indexes
CREATE TABLE dns.resource_records (
id int(11) unsigned NOT NULL auto_increment,
zone varchar(255) NOT NULL,
ttl int(11) NOT NULL default '86400',
type varchar(255) NOT NULL,
host varchar(255) NOT NULL default '@',
mx_priority int(11) default NULL,
data text,
primary_ns varchar(255) default NULL,
resp_contact varchar(255) default NULL,
serial bigint(20) default NULL,
refresh int(11) default NULL,
retry int(11) default NULL,
expire int(11) default NULL,
minimum int(11) default NULL,
PRIMARY KEY ( id )
);
USE dns;
CREATE INDEX host_index ON resource_records( host );
CREATE INDEX type_index ON resource_records( type );
CREATE INDEX zone_index ON resource_records( zone );
We will not be using zone transfers (as recommended by the BIND DLZ project), so no need to configure anything further.
This should all show up on the slave
node2_mysql> USE dns;
node2_mysql> DESCRIBE resource_records;
node2_mysql> SHOW INDEX FROM resource_records;
We can now move on to our BIND compilation
First, we need to get the MySQL develepment RPM installed (as this contains includes required for the DLZ build), including the required dependencies (as appropriate)
node2# rpm -Uvh /path/to/openssl-devel-0.9.7a-43.2.i386.rpm \
node2> /path/to/krb5-devel-1.3.4-17.i386.rpm \
node2> /path/to/zlib-devel-1.2.1.2-1.2.i386.rpm \
node2> /path/to/e2fsprogs-devel-1.35-12.2.EL4.i386.rpm \
node2> /path/to/mysql-devel-4.1.12-3.RHEL4.1.i386.rpm
Once this is done, you can compile BIND.
node2# cd /path/to/source
node2# tar xzf bind-9.4.1-P1.tar.gz
node2# cd bind-9.4.1-P1
node2# ./configure --with-dlz-mysql=yes --prefix=/usr/local/named
If you don't have the standard MySQL RPMs installed, you may need to adjust the --with-dlz-mysql option to reference to include a path to your MySQL install, e.g.
node2# ./configure --with-dlz-mysql=/usr/local/mysql --prefix=/usr/local/named
Once the configure has completed successfully, make, then make install
node2# make && make install
BIND-9.4.1-P1 is now installed.
node2# export PATH="/usr/local/named/sbin:/usr/local/named/bin:${PATH}"
node2# named -v
BIND 9.4.1-P1
We now need to finish off the build of our chroot tree. Add a "named" user and a "named" group, if they don't already exist
node2# groupadd -u 25 named
node2# useradd -u 25 -g named -c "BIND" -s /sbin/nologin -d /var/named named
Populate a base rndc.conf and named.conf
node2# rndc-confgen | tee /var/named/etc/named.conf > /var/named/etc/rndc.conf
rndc.conf can be left alone. We'll need to modify named.conf heavily. A sample named.conf follows
1 key "rndckey" {
2 algorithm hmac-md5;
3 secret "17wShq5o+q1RoN2hhaWsGQ==";
4 };
5
6 controls {
7 inet 127.0.0.1 port 953
8 allow { 127.0.0.1; } keys { "rndckey"; };
9 };
10
11 options {
12 directory "/var/named";
13 listen-on-v6 { none; };
14 listen-on { 127.0.0.1; 192.168.0.201; };
15 pid-file "/var/run/named.pid";
16 };
17
18 zone "." IN {
19 type hint;
20 file "root.hints";
21 };
22
23 zone "localhost" IN {
24 type master;
25 file "localhost.zone";
26 allow-update { none; };
27 notify no;
28 };
29
30 zone "127.in-addr.arpa" IN {
31 type master;
32 file "127.zone";
33 allow-update { none; };
34 notify no;
35 };
36
37 dlz "Mysql zone" {
38 database "mysql
39 {host=localhost dbname=dns user=dns pass=PasswordHere ssl=false port=3306 socket=/mysql/mysql.sock}
40 {SELECT zone FROM resource_records WHERE zone = '%zone%'}
41 {SELECT ttl, type, mx_priority, IF(type = 'TXT', CONCAT('\"',data,'\"'), data) AS data
42 FROM resource_records
43 WHERE zone = '%zone%' AND host = '%record%' AND type <> 'SOA' AND type <> 'NS'}
44 {SELECT ttl, type, data, primary_ns, resp_contact, serial, refresh, retry, expire, minimum
45 FROM resource_records
46 WHERE zone = '%zone%' AND (type = 'SOA' OR type='NS')}
47 {SELECT ttl, type, host, mx_priority, IF(type = 'TXT', CONCAT('\"',data,'\"'), data) AS data, resp_contact, serial, refresh, retry, expire, minimum
48 FROM resource_records
49 WHERE zone = '%zone%' AND type <> 'SOA' AND type <> 'NS'}";
50 };
Lines 1-4 contain the rndc secret that we'll use (via rndc.conf) to use rndc to connect to named. Make sure that the secret here, and the secret in rndc.conf match.
Next, in lines 6-9, the controls block limits rndc access to localhost only, using the rndckey defined above.
A standard options block follows in lines 11-16, where listen addresses, and various paths are defined (remembering to work relative to the chroot tree).
Next, I've defined a root hints zone, plus localhost forward and reverse zones in lines 18-28. This configuration would be different if you were using views.
Things get interesting between lines 37 and 51, where our MySQL DLZ zone is defined.
The driver is declared in line 38, with the connection built in line 39. Your MySQL socket is, again, relative to the chroot.
The appropriate SQL is contained within lines 40 through 49. See the BIND DLZ SourceForge site and this Gentoo Wiki article for more information. The first SELECT query is used to determine if your DNS server is authoritative for the domain being queried, and is used by findzone() within the MySQL driver. The second SELECT is used by lookup() within the MySQL driver. This will be called during name resolution DNS queries. The third SELECT is used to fetch SOA and NS records, and is used by the authority() function in the MySQL driver. The final retrieves all other records, and is used by allnodes(). You can have an additional SELECT to deal with zone transfers, but this is not recommended, so we omit it, and use MySQL replication to transfer our zones instead. In the SELECTs, an IF statement is used to add leading and trailing double quotes around the data fields for TXT records.
As this is in a test environment, I have not implemented ACLs and other security controls, and these would be beyond the scope of this article. You should at least set allow-query-cache to none, recursion to no, as well as setting an appropriate acl on allow-query.
Continue setting up the chroot environment. Create your localhost forward and reverse zone files using your favourite editor.
node2# cat /var/named/var/named/127.zone
$ORIGIN 127.in-addr.arpa.
$TTL 1W
@ 1D IN SOA localhost. root.localhost. (
2002081601 ; serial
3H ; refresh
15M ; retry
1W ; expiry
1D ) ; minimum
1D IN NS localhost.
* 1D IN PTR localhost.
node2# cat /var/named/var/named/localhost.zone
$TTL 1W
@ IN SOA ns.localhost. root.localhost. (
2002081601 ; Serial
28800 ; Refresh
14400 ; Retry
604800 ; Expire - 1 week
86400 ) ; Minimum
IN NS ns
localhost. IN A 127.0.0.1
Populate your root hints file
named2# dig ns . > /var/named/var/named/root.hints
Copy a few remaining files to the chroot tree
named2# cp /lib/libresolv.so.2 /lib/tls/libc.so.6 /lib/ld-linux.so.2 /usr/lib/libnss_dns.so /var/named/lib
named2# cp /etc/localtime /var/named/etc
Create a /dev/random device node in your chroot tree. For this, the filesystem on which your chroot resides must not have the nodev mount option set.
node2# ls -l /dev/random
crw-rw-rw- 1 root root 1, 8 Nov 26 2007 /dev/random
node2# mknod /var/named/dev/random c 1 8
Fix permissions and ownership across the chroot
node2# service mysqld stop
node2# find /var/named \( -type f -o -type d \) -exec chown named:named {} \;
node2# find /var/named -type d -exec chmod 750 {} \;
node2# find /var/named -type f -exec chmod 640 {} \;
node2# usermod -G named mysql
node2# usermod -G mysql named
node2# chown mysql /var/named/mysql
node2# service mysqld start
Start named in the foreground, and check for errors:
node2# named -u named -t /var/named -c /etc/named.conf -g
25-Nov-2007 22:41:07.513 starting BIND 9.4.1-P1 -u named -t /var/named -c /etc/named.conf -g
25-Nov-2007 22:41:07.517 loading configuration from '/etc/named.conf'
25-Nov-2007 22:41:07.518 listening on IPv4 interface lo, 127.0.0.1#53
25-Nov-2007 22:41:07.520 listening on IPv4 interface eth0, 192.168.0.201#53
25-Nov-2007 22:41:07.522 Loading 'Mysql zone' using driver mysql
25-Nov-2007 22:41:07.526 automatic empty zone: 254.169.IN-ADDR.ARPA
25-Nov-2007 22:41:07.526 automatic empty zone: 2.0.192.IN-ADDR.ARPA
25-Nov-2007 22:41:07.527 automatic empty zone: 255.255.255.255.IN-ADDR.ARPA
25-Nov-2007 22:41:07.527 automatic empty zone: 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.IP6.ARPA
25-Nov-2007 22:41:07.527 automatic empty zone: 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.IP6.ARPA
25-Nov-2007 22:41:07.527 automatic empty zone: D.F.IP6.ARPA
25-Nov-2007 22:41:07.527 automatic empty zone: 8.E.F.IP6.ARPA
25-Nov-2007 22:41:07.527 automatic empty zone: 9.E.F.IP6.ARPA
25-Nov-2007 22:41:07.527 automatic empty zone: A.E.F.IP6.ARPA
25-Nov-2007 22:41:07.527 automatic empty zone: B.E.F.IP6.ARPA
25-Nov-2007 22:41:07.531 command channel listening on 127.0.0.1#953
25-Nov-2007 22:41:07.531 ignoring config file logging statement due to -g option
25-Nov-2007 22:41:07.533 zone 127.in-addr.arpa/IN: loaded serial 2002081601
25-Nov-2007 22:41:07.534 zone localhost/IN: NS 'ns.localhost' has no address records (A or AAAA)
25-Nov-2007 22:41:07.534 zone localhost/IN: loaded serial 2002081601
25-Nov-2007 22:41:07.535 running
The most important thing above is that the 'Mysql zone' has been loaded using the DLZ MySQL driver.
We're now ready to test our nameserver.
On the master MySQL server, execute the following SQL (or similar)
INSERT INTO dns.resource_records ( zone,
ttl,
type,
host,
primary_ns,
resp_contact,
serial,
refresh,
retry,
expire,
minimum ) VALUES ( 'example.com',
86400,
'SOA',
'@',
'ns1.example.com.',
'hostmaster.example.com.',
2007112201,
10800,
7200,
604800,
86400 );
On the BIND nameserver, try a query
node2# dig soa example.com @localhost
; <<>> DiG 9.4.1-P1 <<>> soa example.com @localhost
; (1 server found)
;; global options: printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35186
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;example.com. IN SOA
;; ANSWER SECTION:
example.com. 86400 IN SOA ns1.example.com. hostmaster.example.com. 2007112201 10800 7200 604800 86400
;; Query time: 23 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sun Nov 25 22:43:06 2007
;; MSG SIZE rcvd: 80
You can now add additional records, and continue testing.
This concludes the build of our BIND DLZ solution with MySQL replication. There are many alternatives to using MySQL as the backend database, for example PostgreSQL and even BerkeleyDB are supported. I hope that this has demonstrated what a versatile, effecient and exciting technology BIND DLZ is. It would be easy to write a script (Shell/Perl) to administer records within the DLZ database, or even script up a simple (or not so simple) web-based provisioning tool.
Cheers
Kevin Waldron
kevin@zazzybob.com
Disclaimer! - This article is provided for guidance only, and does not replace the relevant official documentation and manuals. I will not be held liable for any hosed systems and/or data.