We are successfully using PowerDNS as an in-line DNSSEC signer between a BIND master and a number of slave servers. When a zone on the BIND master is updated, in our case via RFC 2136 DDNS updates, the SOA serial number is incremented automatically and BIND sends out a DNS NOTIFY to the PowerDNS “slave”. PowerDNS then transfers the zone via an AXFR and stores it in its back-end database. So far nothing particularly exciting. What I wanted was for the zone transfer to be “intercepted”. I wanted to be able to change or add a resource record on the fly, and I’ll tell you why, in a moment. I spoke to Bert Hubert and proposed a solution involving a dynamically loadable library containing a C function to perform such on-the-fly modifications. Bert didn’t like the idea of a shared object messing with PowerDNS and suggested the functionality be implemented in Lua instead to which I agreed. (You may recall I wrote about the Lua functionality of PowerDNS Recursor in Appendix H of my book.) A few days passed, and since this morning, build 2065 to be precise, AXFR filtering in PowerDNS with a Lua script is implemented. A beaut!

What does this do? PowerDNS can invoke a Lua script on an incoming AXFR zone transfer. The user-defined Lua function axfrfilter within your script is invoked for each resource record read during the transfer, and the outcome of the function defines what PowerDNS does with the resulting records. More precisely, your function defines which records, with what content are stored by PowerDNS in the back-end database at the end of the zone transfer.

What you can accomplish with this Lua interception script:

  1. Ensure consistent values on SOA
  2. Change incoming SOA serial number to a YYYYMMDDnn format
  3. Ensure consistent NS RRset
  4. Timestamp the zone transfer with a TXT record
  5. Add one or more records to a zone

To enable a Lua script for a particular slave zone, determine the domain_id for the zone from the domains table, and add a row to the domainmetadata table for the domain. Supposing the domain we want has an id of 3, the following SQL statement will enable the Lua script my.lua for that domain:

    INSERT INTO domainmetadata (domain_id, kind, content) VALUES (3, "LUA-AXFR-SCRIPT", "/lua/my.lua");

The Lua script must both exist and be syntactically correct or the server logs an error. PowerDNS continues to try loading the script until it succeeds; in other words the AXFR is attempted again and again until the error-condition is cleared. Your script is reloaded for every incoming AXFR; you don’t have to restart PowerDNS after modifying your Lua script.

Your Lua functions have access to the query codes through a pre-defined Lua table called pdns. For example if you want to check for a CNAME record you can either compare qtype to the numeric constant 5 or the value pdns.CNAME – they are equivalent.

If your function decides to handle a resource record it must return a result code of 0 together with a Lua table containing one or more replacement records to be stored in the back-end database. If, on the other hand, your function decides not to modify a record, it must return -1 and an empty table indicating that PowerDNS should handle the incoming record as normal.

Consider the following simple example:

    function axfrfilter(remoteip, zone, qname, qtype, ttl, prio, content)
       -- Replace each HINFO record with this TXT
       if qtype == pdns.HINFO then
          resp = {}
          resp[1] = {    qname   = qname,
                qtype   = pdns.TXT,
                ttl   = 99,
                content   = "Hello Ahu!"
          return 0, resp
       -- Grab each _tstamp TXT record; replace rdata with timestamp
       if qtype == pdns.TXT and string.starts(qname, "_tstamp.") then
          resp = {}
          resp[1] = {
                qname   = qname,
                qtype   = qtype,
                ttl   = ttl,
                content   = os.date("Ver %Y%m%d-%H:%M")
          return 0, resp
       resp = {}
       return -1, resp
    function string.starts(s, start)
       return s.sub(s, 1, s.len(start)) == start

Upon an incoming AXFR, PowerDNS calls our axfrfilter function for each record. All HINFO records are replaced by a TXT record with a TTL of 99 seconds and the specified string. TXT Records with names starting with _tstamp. get their value (rdata) set to the current time stamp. All other records are not handled by the script which means PowerDNS simply copies them from the zone transfer to the back-end database.

Individual records can also be deleted from the transfer; a use case I may elaborate on at a later time is mechanically updating a “counter” in a zone to force an SOA serial update and transfer, without wanting that particular counter record to be seen on the slaves. Do note that messing with a zone transfer in this way is not necessarily for the faint of heart; it can break all sorts of things for you, and it may even skin your cat. I’ve warned you, and I’d be interested in ideas you have for the Lua AXFR-filter-functionality in PowerDNS.

DNS, Lua, powerdns, dnssec, and AXFR :: 16 Mar 2011 :: e-mail