stocksy.co.uk
"the site for those who crave disappointment"

Sponsored Links

Chrooted BIND on OS X

5th Feb 2008, 21:09:50

By James Stocks

Updated: 13th December 2008.

It seems that more often than not, when an ISP is having an outage, it's caused by the DNS servers being unresponsive. Let's cut them out of the loop.

In this article, I hope to show you how to set up BIND both as an authoritative and caching DNS server. These instructions will work with Mac OS 10.4.x and 10.5.x.

On my home network, BIND does two jobs: Firstly, it is authoritative for the domain stocksy.co.uk. and secondly, I use it as a recursive name server to look up addresses of other computers, to do web browsing for example.

If you don't want to be authoritative for your domain, for example because someone else like your ISP or hosting provider already looks after your domain, that's fine - you can just set up the caching name server.

At your firewall, you must open TCP and UDP port 53 incoming. The DNS server will need to make outgoing connections from any source port to destination port 53.

Starting BIND

The BIND daemon (called named) is already installed on OS X, you can confirm this:

$ named -v
BIND 9.2.2

Before BIND will start, it's necessary to create a new /etc/named.conf, since the Apple-supplied one does not work well out of the box:

# cp /etc/named.conf /etc/poo.named.conf
# echo "" > /etc/named.conf
# vi /etc/named.conf
// Config file for BIND on Mac OS X, James Stocks 13/12/08

options {
        directory "/var/named";
};

//This tells BIND where to find the root servers (called ".")
zone "." {
        type hint;
        file "root.hints";
};

//Tells BIND what 'localhost' is
zone "localhost" {
        type master;
        file "db.localhost";
        allow-update { none; };
};

//This is a reverse mapping from 127.0.0.1 to 'localhost'
zone "0.0.127.in-addr.arpa" {
        type master;
        file "db.localhost.rev";
};

Now, we must go to the /var/named directory and create some more files:

# cd /var/named
# curl ftp://ftp.rs.internic.net/domain/named.root > root.hints
# rm named.ca
# mv localhost.zone db.localhost
# mv named.local db.localhost.rev

At this point, we can start BIND with:

# named -c /etc/named.conf

You can verify that it has started by tailing /var/log/system.log:

$ tail /var/log/system.log | grep named
Jun 10 13:32:00 Power-Mac named[772]: starting BIND 9.2.2 -c /etc/named.conf
Jun 10 13:32:00 Power-Mac named[772]: none:0: open: /private/etc/rndc.key: file not found
Jun 10 13:32:00 Power-Mac named[772]: couldn't add command channel 127.0.0.1#953: file not found
Jun 10 13:32:00 Power-Mac named[772]: none:0: open: /private/etc/rndc.key: file not found
Jun 10 13:32:00 Power-Mac named[772]: couldn't add command channel ::1#953: file not found
Update, 13th December 2008: Please take note that in previous versions of this article, I used the option query-source address * port 53; in my named.conf. Because of the DNS vulnerability discovered by Dan Kaminsky, this is a very bad idea, since the randomness of the UDP port is an important mitigating factor in the attack. If you are currently using this option, you should disable it as soon as possible.

Many thanks go to Rick Moen for explaining this to me.

Don't worry about named[772]: none:0: open: /private/etc/rndc.key: file not found and friends, we don't need rndc right now. Try querying the name server:

$ dig www.stocksy.co.uk @127.0.0.1

; <<>> DiG 9.2.2 <<>> www.stocksy.co.uk @127.0.0.1
;; global options:  printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 52336
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 2

;; QUESTION SECTION:
;www.stocksy.co.uk.             IN      A

;; ANSWER SECTION:
www.stocksy.co.uk.      7200    IN      A       194.106.52.245

;; AUTHORITY SECTION:
stocksy.co.uk.          7200    IN      NS      ns2.stocksy.co.uk.
stocksy.co.uk.          7200    IN      NS      ns1.stocksy.co.uk.

;; ADDITIONAL SECTION:
ns1.stocksy.co.uk.      7200    IN      A       194.106.52.245
ns2.stocksy.co.uk.      7200    IN      A       69.93.127.12

;; Query time: 3 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sat Jun 10 14:45:14 2006
;; MSG SIZE  rcvd: 119

With luck, you'll get an answer like the above.

Those of you that have been paying attention will notice that named is running as root, this is a Really Bad Idea. Lets take care of that...

Securing BIND

BIND has had a lot of security problems in the past, but its recent record is better. It's still considered prudent to jail BIND and run it as its own user so that any vulnerabilities can't affect the rest of the system.

For OS X 10.4.x use niutil:

Use nireport to check for unused uid and gid values. I have chosen to start at 200 to avoid conflicts.

# nireport . /users uid
# nireport . /groups gid

Use niutil to create a new user and group:

# niutil -create / /users/named
# niutil -createprop / /users/named uid 200
# niutil -createprop / /users/named shell /usr/bin/false
# niutil -createprop / /users/named home /var/chroot/named

# niutil -create / /groups/named
# niutil -createprop / /users/named gid 200
# niutil -createprop / /groups/named gid 200
# niutil -createprop / /groups/named users named

In 10.5.x, Apple changed things a bit, so do this instead:

Check for unused uid and gid values:

# dscl . -list /Users UniqueID
# dscl . -list /Groups PrimaryGroupID

Create a user and group:

# dscl . -create /Users/named
# dscl . -createprop /Users/named UniqueID 200
# dscl . -createprop /Users/named UserShell /usr/bin/false
# dscl . -createprop /Users/named home /var/chroot/named
# dscl . -create /Groups/named
# dscl . -createprop /Users/named PrimaryGroupID 200
# dscl . -createprop /Groups/named PrimaryGroupID 200
# dscl . -createprop /Groups/named users named

Now we can create the chroot evironment - this is the same for both OS X 10.4.x and 10.5.x

# mkdir -p /var/chroot/named
# cd /var/chroot/named
# mkdir dev
# mknod dev/null c 2 2
# mknod dev/zero c 2 12
# ln -s /dev/random dev/random
# chmod 666 dev/null dev/random dev/zero
# mkdir -p private/var/named
# ln -s private/var var
# mkdir var/run
# mkdir private/etc
# ln -s private/etc etc
# cp /etc/named.conf etc/
# cp -r /var/named* var/
# chown -R named:named ../named
# rm -r /etc/named.conf
# rm -r /var/named
# ln -s /var/chroot/named/var/named /var/named
# ln -s /var/chroot/named/etc/named.conf /etc/named.conf

Find the named process you started earlier and kill it:

# kill -9 `cat /var/run/named.pid`

Start named in its new home

# named -c /etc/named.conf -u named -t /var/chroot/named

Tail /var/log/system.log once again to check it started OK.

You might notice that uncached queries are very slow, in the order of seconds rather than milliseconds, compare the below:

Mac OS X:
;; Query time: 4522 msec
;; SERVER: 172.16.0.81#53(172.16.0.81)
;; WHEN: Sat Jun 10 12:32:33 2006
;; MSG SIZE  rcvd: 148

NetBSD:
;; Query time: 405 msec
;; SERVER: 172.16.0.82#53(172.16.0.82)
;; WHEN: Sat Jun 10 12:33:01 2006
;; MSG SIZE  rcvd: 164

I dug around Google a bit and the prevailing theory seems to be that on OS X, named attempts an IPv6 lookup, waits for this to timeout and then tries an IPv4 lookup. I tried adding listen-on-v6 { none; }; to the /etc/named.conf options, but it made little difference. At this point, I lost patience and just compiled my own copy of BIND without IPv6 support:

Retrieve, unpack and compile the newest version of BIND. At the time of writing this was 9.4.3.

# curl -O http://ftp.isc.org/isc/bind9/9.4.2/bind-9.4.3.tar.gz
# tar zxf bind-9.4.3.tar.gz && rm bind-9.4.3.tar.gz
# cd bind-9.4.3
# ./configure --disable-ipv6 && make && make install

Now, BIND is started by specifying the full path to the named binary:

# /usr/local/sbin/named -c /etc/named.conf -u named -t /var/chroot/named

To avoid confusion, I moved the old named:

# mv /usr/sbin/named /usr/sbin/9.2.2.named.poo

Being Authoritative

BIND keeps DNS records for each zone in a different file. Create one for your domain and save it as something like /var/named/db.stocksy.co.uk:

$ORIGIN .
$TTL 7200       ; 2 hours
stocksy.co.uk           IN SOA  ns1.stocksy.co.uk. stocksy.stocksy.co.uk. (
                                09060600 ; serial
                                14400      ; refresh (4 hours)
                                7200       ; retry (2 hours)
                                950400     ; expire (1 week 4 days)
                                7200       ; minimum (2 hours)
                                )
                        NS      ns1.stocksy.co.uk
                        NS      ns2.stocksy.co.uk
                        A       194.106.52.245
                        MX      0 smtp.toastputer.net.
                        MX      5 mail2.toastputer.net.
$ORIGIN stocksy.co.uk.
ns1                     A       194.106.52.245
ns2                     A       69.93.127.12
ftp                     A       194.106.52.245
www                     A       194.106.52.245

Now add the zone to /etc/named.conf:

zone "stocksy.co.uk" {
        type master;
        file "db.stocksy.co.uk";
};

You'll need to restart named for the change to take effect.

Using Views

My web server is behind a firewall which NATs my internal network to private subnet. As a result, if I try and visit my own web site to test it, the DNS server returns my external IP which does not work from inside. The way around this is to use views, allowing BIND to return different answers depending on which IP is querying it.

Essentially, just create a new directory, /var/named/internal and copy all of your zone files into it. All zones have to be in all views, regardless of whether you actually want to offer different views for the zone:

// Config file for BIND on Mac OS X, James Stocks 13/12/08

options {
        directory "/var/named";
};


// Define the LAN 
acl "sprucewaylan" {
		// Replace this with the network address of your LAN (in CIDR notation)
        172.16.0.0/24;
};


// Begin LOCAL
view "sprucewaylan" {

		//This view matches 172.16.0.0/24
        match-clients { sprucewaylan; };
        
        //We want to allow recursion (look up any domain) from the LAN
        recursion yes;

		//This tells BIND where to find the root servers (called ".")
		zone "." {
				type hint;
				file "internal/root.hints";
		};
		
		//Tells BIND what 'localhost' is
		zone "localhost" {
				type master;
				file "internal/db.localhost";
				allow-update { none; };
		};
		
		//This is a reverse mapping from 127.0.0.1 to 'localhost'
		zone "0.0.127.in-addr.arpa" {
				type master;
				file "internal/db.localhost.rev";
		};
		
		//This is your zone
		zone "stocksy.co.uk" {
				type master;
				file "internal/db.stocksy.co.uk";
		};

};
// End LOCAL



// Begin INTERNET
view "inet" {
		
		//This view matches anything not matched by the sprucewaylan ACL
        match-clients { any; };
        
        //We don't want to allow recursion from the internet
        recursion no;

		//This tells BIND where to find the root servers (called ".")
		zone "." {
				type hint;
				file "root.hints";
		};
		
		//Tells BIND what 'localhost' is
		zone "localhost" {
				type master;
				file "db.localhost";
				allow-update { none; };
		};
		
		//This is a reverse mapping from 127.0.0.1 to 'localhost'
		zone "0.0.127.in-addr.arpa" {
				type master;
				file "db.localhost.rev";
		};
		
		//This is your zone
		zone "stocksy.co.uk" {
				type master;
				file "db.stocksy.co.uk";
		};
            
};
// End INTERNET
Update, 13th December 2008: Please take note that in previous versions of this article, I used the option query-source address * port 53; in my named.conf. Because of the DNS vulnerability discovered by Dan Kaminsky, this is a very bad idea, since the randomness of the UDP port is an important mitigating factor in the attack. If you are currently using this option, you should disable it as soon as possible.

Many thanks go to Rick Moen for explaining this to me.

Now you're free to replace the external IPs in the internal zone files with your internal IP addresses.

launchd

Now you've got BIND working so well, you'll probably want it to start on boot. This is fairly easy to accomplish using launchd. First, unload the named launchdaemon:

# cd /System/Library/LaunchDaemons
# launchctl unload ./org.isc.named.plist

launchctl may respond with 'No such process' - that's fine. Move the named launchdaemon out of the way and create a new one:

# cp org.isc.named.plist dist.org.isc.named.plist
# echo "" > org.isc.named.plist
# vi org.isc.named.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Disabled</key>
        <false/>
        <key>Label</key>
        <string>org.isc.named</string>
        <key>OnDemand</key>
        <false/>
        <key>ProgramArguments</key>
        <array>
                <string>/usr/local/sbin/named</string>
                <string>-c</string> 
                <string>/etc/named.conf</string>
                <string>-u</string> 
                <string>named</string>
                <string>-t</string>
                <string>/var/chroot/named</string> 
                <string>-f</string>
        </array>
        <key>ServiceIPC</key>
        <false/>
</dict>
</plist>

This is just the /usr/local/sbin/named -c /etc/named.conf -u named -t /var/chroot/named but XMLised. The -f option in the program arguments is needed to keep named in the foreground (i.e. don't daemonise), otherwise launchd loses control of it.

Kill any existing named daemon and use launchd to kick off a new one:

# kill -9 `cat /var/run/named.pid`
# launchctl load org.isc.named.plist

As usual, check /var/log/system.log for any errors. If you feel like it, you could reboot to double check that it really starts on boot.

1 Archived Comment

20th Jul 2010, 10:53:53 by huzia
This article's works to in SL (Snow Leopard) within my MBP (Macbook Pro), I've just tried it, though with a slight differences in killing named process. In the first named process killing, it should be replaced with:
kill -9 `cat /var/run/named/named.pid`

And in the second named process killing, it should be replaced with:
kill -9 `cat /var/chroot/named/var/run/named/named.pid`

After using launchd, if you want to kill named process, you should run this to minimize typing:
rm -r /var/run/named
rm -r /var/chroot/named/var/run/named
ln -s /var/chroot/named/var/run/named.pid /var/run/named.pid

Then if you want to kill/restart named, rather than going through the full path, you can go through the symbolic link instead:
kill -9 `cat /var/run/named.pid`

Oups, just don't forget to use sudo, or you can enable root if you want to.
Last word, Great post! :D

New Comments

Some Rights Reserved