PowerDNS and a CouchDB backend

May 5th, 2010 | Categories: DNS, Database, NoSQL, dnsbook | Tags: ,

I'm succumbing to comments and queries on whether the PowerDNS DNS name server could be backed by a CouchDB database. It can.

Alternative DNS ServersI'm not going to delve too deeply into PowerDNS here because I discuss it very thoroughly in chapter 6 of my book Alternative DNS Servers. So, in other words, if you're not familiar with PowerDNS, I'll recommend that as study material.

Faithful readers will recall, that I implemented a first DNS server with a CouchDB database as a proof of concept. After a complete redesign of the document model I had an implementation of a standalone authoritative DNS server with a CouchDB backend. This third iteration I discuss here takes that same model and the same data and creates a so-called pipe back-end for PowerDNS.

Now, the pipe back-end is a simple shell, Perl, or whatever program which reads queries given to it by PowerDNS from stdin and issues replies on stdout. As such, it is a very useful back-end for prototyping. Unfortunately, and as is to be expected, the result is not terribly fast; this is no limitation of PowerDNS, but rather of the shuffling of data that occurs between PowerDNS and the back-end's program.

The pipe backend is a PowerDNS native type only; it cannot be a master or a slave, nor does it have autoserial capability, where a serial number is automatically incremented. Well, the good news is that this CouchDB version has all of that:

  • The SOA serial number is incremented automatically when the JSON document in CouchDB is modified. (If you don't want this to happen, add a serial field to your zone document.)
  • CouchDB being what it is, this backend can replicate! Master, slave, whatever you like. ;-) (But admittedly, this is not DNS data AXFR-type replication.)

I'm using the following pdns.conf:

# Which backends to launch and order to query them in
launch=pipe

# Local IP addresses to which we bind
local-address=0.0.0.0

# The port on which we listen
local-port=9953

# Version of the pipe backend ABI
pipebackend-abi-version=2

# Name of co-process
pipe-command=/etc/powerdns/jpm/powercouch.pl

# Seconds to store packets in the PacketCache
cache-ttl=0

# Seconds to store packets in the PacketCache
query-cache-ttl=0

(For testing purposes, leave the two caching values at 0 for the time being.)

The powercouch.pl program is the pipe-back-end's program:

#!/usr/bin/perl
# powercouch.pl (C)2010 by Jan-Piet Mens
# demo pipe back-end for PowerDNS on CouchDB

use strict;
use IO::Socket;
use AnyEvent::CouchDB;
use Data::Dump qw(pp);

my $uri = 'http://127.0.0.1:5984/dns';
my $db = couchdb($uri);

$|=1;   # disable buffering

chomp(my $line = <>);

# Start of pipe "protocol". See
# http://doc.powerdns.com/backends-detail.html

unless($line eq "HELO\t2") {
	print "FAIL\n";
	<>;
	exit;
}
print "OK\tHere is $0. Relax.\n";

while(chomp($line = <>))
{
	my @pq;
	my $v;

	my ($type,$qname,$qclass,$qtype,$id,$client,$ip) = @pq = split(/\t/, $line);

	print "LOG\treceived: $line\n";
	if ($#pq < 6) {
		print "LOG\tPowerDNS sent unparseable line\n";
		print "FAIL\n";
		next;
	}

	# Check whether conn to CouchDB is still alive, and if not,
	# re-establish.

	eval {
		my $info = $db->info()->recv;
		# print pp($info),"\n";
	};
	if ($@) {
		$db = couchdb($uri);
	}

	my @keys = ($qname, lc $qtype);

	eval {
		$v = $db->view('dns/rrq', { key => [ @keys ] })->recv
	};
	if ($@) {
		print "LOG\tCouchDB replies: $_: $@\n";
		print "FAIL\n";
		next;
	}

	if ($#{$v->{rows}} == -1) {	# NXDOMAIN
		print "END\n";
		next;
	}

	foreach my $r (@{$v->{rows}}) {

		my $rr = $r->{value};
		my $ttl = $rr->{ttl};
		my $type = uc $rr->{type};

		# print pp($rr),"\n";

		if (($qtype == 'SOA' || $qtype == 'ANY') && $rr->{type} eq 'soa') {
			my $s = $rr->{data};
			my $reply = sprintf("%s %s %d %d %d %d %d",
				$s->{mname}, $s->{rname}, $s->{serial},
					$s->{refresh}, $s->{retry}, $s->{expire},
					$s->{minimum});
			reply($qname, $qclass, $id, $ttl, $type, $reply);

		}

		if (($qtype == 'NS' || $qtype == 'ANY') && $rr->{type} eq 'ns') {
			reply($qname, $qclass, $id, $ttl, $type, $rr->{data});
		}

		if (($qtype == 'A' || $qtype == 'ANY') && $rr->{type} eq 'a') {
			for my $ip (@{$rr->{data}}) {
				reply($qname, $qclass, $id, $ttl, $type, $ip);
			}
		}

		if (($qtype == 'CNAME' || $qtype == 'ANY') && $rr->{type} eq 'cname') {
			reply($qname, $qclass, $id, $ttl, $type, $rr->{data});
		}

		if (($qtype == 'TXT' || $qtype == 'ANY') && $rr->{type} eq 'txt') {
			for my $txt (@{$rr->{data}}) {
				reply($qname, $qclass, $id, $ttl, $type, $txt);
			}
		}
		if (($qtype == 'PTR' || $qtype == 'ANY') && $rr->{type} eq 'ptr') {
			reply($qname, $qclass, $id, $ttl, 'PTR', $rr->{data});
		}

		# AXFR not implemented
	}

	print "END\n";
}

sub reply {
	my ($qname, $qclass, $id, $ttl, $qtype, $rr) = @_;

	print "DATA\t$qname\t$qclass\t$qtype\t$ttl\t$id\t$rr\n";
	print "LOG\treturning $rr for $qname/$qtype\n";
}

Note that I'm using exactly the same CouchDB database and design that I used in the standalone version. You'll notice that the two programs are quite similar, the differences being in the way the standalone version replies to Stanford::DNSserver and the PowerDNS version simply prints its results to stdout for PowerDNS to then parse.

The result works.

$ dig example.org txt
;; ANSWER SECTION:
example.org.   1801  IN   TXT     "Relax"
example.org.   1801  IN   TXT     "powered by"
example.org.   1801  IN   TXT     "CouchDB and PowerDNS"

;; Query time: 67 msec

It isn't fast, but it works. You can, however, make it blindingly fast by tuning the cache values for PowerDNS as hinted to above. Read about it here. :-)