Disclaimer: This post was originally posted in 2008 as an article on the now-defunct website zazzybob.com. While the software version and actual commands used may vary, the concepts are still similar and give a general idea of how to approach a given problem.
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
|
1 2 3 4 |
# 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
|
1 2 |
# /usr/local/named/sbin/named -v BIND 9.4.1-P1 |
The latest BIND source can always be downloaded from ISC.
MySQL Configuration
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
|
1 2 |
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
|
1 2 |
both# rpm -qlc `rpm -q mysql` /etc/my.cnf |
Edit with your favourite editor and make the following additions
|
1 2 3 |
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
|
1 2 3 4 |
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.
|
1 |
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
|
1 |
node2# chown mysql:mysql /var/named/mysql |
Once this is done, edit your slaves MySQL configuration file, and update the socket location
|
1 2 |
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
|
1 2 3 |
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
|
1 |
both# service mysqld start |
Once MySQL is up and running, we can start to build our database.
Connect to your master MySQL instance
|
1 |
node1# mysql -u root -p |
First, create the “dns” database, and a “dns” user whom will have administrative rights over it
|
1 2 |
node1_mysql> CREATE DATABASE dns; node1_mysql> GRANT ALL ON dns.* TO 'dns'@'localhost' IDENTIFIED BY 'PasswordHere'; |
Next, a “rep_dns” user for replication
|
1 |
node1_mysql> GRANT REPLICATION SLAVE ON *.* TO 'rep_dns'@'slave.example.com' IDENTIFIED BY 'PasswordHere'; |
Configure, then start, slave
|
1 2 3 4 5 6 |
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
|
1 2 3 |
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
|
1 2 3 4 5 6 7 8 9 |
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
|
1 |
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
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
|
1 2 3 |
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
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)
|
1 2 3 4 5 |
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.
|
1 2 3 4 |
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.
|
1 |
node2# ./configure --with-dlz-mysql=/usr/local/mysql --prefix=/usr/local/named |
Once the configure has completed successfully, make, then make install
|
1 |
node2# make && make install |
BIND-9.4.1-P1 is now installed.
|
1 2 3 |
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
|
1 2 |
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
|
1 |
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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
|
1 |
named2# dig ns . > /var/named/var/named/root.hints |
Copy a few remaining files to the chroot tree
|
1 2 |
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.
|
1 2 3 |
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
|
1 2 3 4 5 6 7 8 |
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:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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.
Testing
We’re now ready to test our nameserver.
On the master MySQL server, execute the following SQL (or similar)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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.
Conclusion
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.