I’m in an environment in which a number of outward-facing authoritative DNS servers are fed by hidden primary servers; nothing special. What is perhaps a bit special is that all these servers are hand-crafted: zone definitions and BIND configurations are lovingly hand-edited, and as such, there is little control over which zones are actually valid, which of the servers are correctly configured, etc.

What I need on the “outside” DNS servers is a list of zones which have been configured on the hidden primaries, so that I can automate verification: are all zones configured, are any missing, etc. But there’s a problem here: I’m not the copy-and-paste type of person: I want delivery of this zone list automated.

What makes this a bit tricky is that the only communications channel available to me for transporting data between the outward-facing servers and the internal hidden primaries or vice versa, is the DNS ports: port 53 UDP and TCP, so I thought I’d just zone-transfer a list of zones out in its own zone. Crazy but easy. :-) I’m going to show you how I’m solving the problem. First of all, let me show you what I’m talking about.

On the left of the following diagram is a hidden primary BIND name server. On the right, is a set of firewalls (the thick red line) and then come the outward-facing name servers. (Their brand is irrelevant; it suffices that they are capable of transferring zones from the left-hand-side name server via the DNS zone transfer (AXFR) protocol.)

What I need is a list of zones configured on the hidden primary servers (seen on the left in the diagram above). I could use the usual Unix toolbox to parse named.conf and its included files, but I thought I’d rather query BIND itself, through its statistics server.

You may know that BIND 9.5 introduced a built-in statistics server you enable when building the server. The statistics server provides a large variety of data in XML format you can retrieve directly via HTTP. The host and port on which the statistics server listens and access controls to it are configured in named.conf. For example, the statements

acl "trusted" {;         // local;
statistics-channels { 
   inet *  port 8053 allow { trusted; }; 

tell BIND to accept HTTP connections to its statistics server at any of the system interfaces on port 8053 for the addresses in the “trusted” ACL.

In fact if I point my Web browser at the host:port combination configured for BIND (which for me would be I’ll see a nicely (oh, my eyes) formatted display of the data. This is a tiny excerpt showing some of my configured zones:

(The formatted output you see above in the browser is created by an XSL style sheet which is also provided by the BIND name server – look closely at the second line of what is output in the example below.) The plain XML looks different of course; the first few lines of what I get when I retrieve the XML with an invocation of curl -s look like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <?xml-stylesheet type="text/xsl" href="/bind9.xsl"?>
    <isc version="1.0">
        <statistics version="2.2">

What I need from the XML is a list of zones, which is easier said than done. In BIND zones may be contained in views, and I may have the same zone in two (or more) different views. So, what I really want is a list which contains the zone name, the view name, the class and the zone’s current SOA serial number.

I first tried parsing the XML produced by the BIND statistics server with Perl, but that was much too slow for me, so I created bzl, a small and very fast libxml2-enabled C program that grabs the XML statistics directly via HTTP from BIND, uses Xpath to extract the zones and prints those out one per line. An example output is:

    42401 temp.aa IN internal
    0 0.IN-ADDR.ARPA IN internal
    0 127.IN-ADDR.ARPA IN internal
    0 254.169.IN-ADDR.ARPA IN internal
    0 2.0.192.IN-ADDR.ARPA IN internal
    0 100.51.198.IN-ADDR.ARPA IN internal
    0 113.0.203.IN-ADDR.ARPA IN internal
    0 IN internal
    0 IN internal
    0 IN internal
    0 8.B.D. IN internal
    0 D.F.IP6.ARPA IN internal
    0 8.E.F.IP6.ARPA IN internal
    0 9.E.F.IP6.ARPA IN internal
    0 A.E.F.IP6.ARPA IN internal
    0 B.E.F.IP6.ARPA IN internal
    - foo.bar IN internal
    1287679338 bzl IN internal
    17 example.net IN external
    2001013101 bind CH extern-chaos
    0 authors.bind CH
    0 hostname.bind CH
    0 version.bind CH
    0 id.server CH

Note how BIND’s internal automatically configured zones usually have an SOA serial number of 0. Also note, the zone foo.bar has a dash for the serial number – the zone can’t be loaded because it’s an SDB zone and it’s back-end isn’t with me in the hotel (although it should be).

The rest is trivial: a Perl program called makezonefile.pl periodically takes that output, filters out the zones I’m not interested in, and creates a normal RFC 1035 zone file.

    $TTL 1H 
    ;  generated by ./makezonefile.pl (bzl) on Thu Oct 21 20:26:30 2010
    @  IN SOA  localhost. jpmens.no.where. 1287681990 1H 1H 1W 60   
    @  IN NS   localhost.
    temp.aa                                  IN TXT "42401"
    foo.bar                                  IN TXT "-"
    bzl                                      IN TXT "1287681967"
    example.net                              IN TXT "17"
    bzl-nzones                               IN TXT "4"

Each zone name retrieved by bzl becomes an unqualified domain name. The value of the TXT resource record is the zone’s SOA serial retrieved from the XML out of the statistics server; it is simply for informational purposes, but I could even use it for monitoring. An additional resource record called bzl-nzones is added to the zone file: it contains the number of zones listed in this zone file.

And now? I said: the rest is trivial. I configure my hidden primary server to serve this zone to particular hosts only, of course:

    zone "bzl" in {
      type master;
      file "master/bzl-zonefile.db";
      allow-transfer {; };
      allow-query { ... };
      also-notify {; };

And I configure the zone as a slave zone on the outward-facing name servers, taking particular care to protect the zone’s content. (Make sure you restrict in a view or via an ACL which clients may query the zone!) Let me show you how the zone will be transferred onto a slave:

    $ dig @ bzl axfr
    bzl.                    3600    IN      SOA     localhost. jpmens.no.where. 1287682762 3600 3600 604800 60
    bzl.                    3600    IN      NS      localhost.
    temp.aa.bzl.            3600    IN      TXT     "42401"
    foo.bar.bzl.            3600    IN      TXT     "-"
    bzl.bzl.                3600    IN      TXT     "1287681990"
    bzl-nzones.bzl.         3600    IN      TXT     "4"
    example.net.bzl.        3600    IN      TXT     "17"
    bzl.                    3600    IN      SOA     localhost. jpmens.no.where. 1287682762 3600 3600 604800 60

A cron entry on the hidden primary creates the zone file and reloads the name server. If the zone has changed, BIND will notify its slave(s) which then re-transfer the zone.

    0 * * * *  /usr/local/sbin/makezonefile.pl && /usr/sbin/rndc reload bzl

Thanks to the BIND statistics server I have an easy way of listing zones configured in a BIND name server, and wrapping a few bits around that data, solves a problem I would have had difficulty solving otherwise.

Further reading:

DNS, CLI, and hack :: 21 Oct 2010 :: e-mail