Setting up a BIND DLZ Nameserver with MySQL Replication

Introduction

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.

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

    
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

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.

Testing

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.

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.

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.

Valid CSS!

Valid HTML 4.01!