The Swede in the middle of Silicon Valley

Wednesday, April 29, 2009

DNS query parser

The last days I've been working on our internal DNS servers looking at the new BIND statistics I enabled 2 weeks back. I sure found some interesting behaviors of some clients that shouldn't be there thanks to it.

Thinking a bit more at statistics I started looking at the query-log, if you have managed a DNS server you know that the query-log tends to build up quickly and it can become pretty unmanageable to troubleshoot unless you've some decent programming skills. Due to this I figured, hell, lets dump the data in a MySQL database so we can run some nice queries to see trending etc.

The code below is what I ended up with. What it does is creating a named pipe (FIFO) that allows BIND to write all query-log data directly a database after being parsed by the script.

Please notice that I'm still working on the code and will post the updates to this post, if you have any proposals or issues with it please email me.

Disclaimer: This code is very much untested and it's expensive to run from a CPU perspective, use caution!

...
logging {
channel query_log {
file "/var/log/querypipe";
time-stamp yes;
}
...


Once started all query-log data is sent to the script and gets parsed, once the writecache has reached it's top the data is being written to the database. The reason for maintaining a writecache is because MySQL with MyISAM has a lot better performance inserting several rows at once rather than one at the time, the backside to this is that you're data wont be real-time which may not be ideal depending on your reporting needs.

CREATE TABLE `queries` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`date` varchar(25) DEFAULT NULL,
`time` varchar(25) DEFAULT NULL,
`id` int(11) DEFAULT NULL,
`port` int(11) DEFAULT NULL,
`query` varchar(255) DEFAULT NULL,
`rr` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM


To run the code edit the variables to match your system settings, to demonize it run it with -D, once started, run rndc reload or restart your name-server to release locking of the query-log.


#!/usr/bin/perl

use Getopt::Long;
use DBI;
use POSIX qw(setsid);

my $daemon = 0;

Getopt::Long::GetOptions('daemon|D' => \$daemon);


if ($daemon) {
defined(my $pid = fork) or die "Can't fork: $!";
exit if $pid;
setsid or die "Can't start a new session: $!";
}

$SIG{INT} = \&terminate;
$SIG{HUP} = \&terminate;

sub terminate {
unlink $FIFO;
exit 0;
}

while (1) {

my $LOG_PATH = '/var/named/chroot/var/log/';
my $FIFO = $LOG_PATH.'querypipe';
my $RNDC = '/usr/sbin/rndc';
my $user = 'named';

my($login,$pass,$uid,$gid) = getpwnam($user) or die "$user not in passwd file";

my $db = DBI->connect("dbi:mysql:database=dns;host=localhost:3306;user=dns;password=dns") or die "Failed to connecto DB\n";

while (1) {
unless (-p $FIFO) {
unlink $FIFO;
system('mknod', $FIFO, 'p')
&& die "can't mknod $FIFO: $!";
}
chown $uid, $gid, $FIFO;
system("$RNDC reload");
open (FIFO, "< $FIFO") || die "can't read $FIFO: $!";
while () {
my($date, $time, $source, $port, $query, $rr) = /([0-9]{1,2}-[A-Za-z]{3}-[0-9]{4}) ([0-9]{2}:[0-9]{2}:[0-9]{2}).[0-9]{1,5} client (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})#([0-9]{1,5}): query: (.*) IN (.*) +/;

my $statement = $db->prepare("INSERT INTO `queries` values (NULL, '$date', '$time', '$source', '$port', '$query', '$rr')");
$statement->execute() or die "Failed to write to DB!";
}
close FIFO;
}
}


Planned additions:
- Help-messages
- Function to log
- Auto adjust write frequency based on load
- Clean-up
- Fix MySQL table to use appropriate fields

Sunday, April 5, 2009

Enabling your recursive BIND server to support DNSSEC

So I've been doing some BIND hacking this weekend looking into enabling my recursive BIND instance to do DNSSEC lookups if available.

Since the root-servers today doesn't have native support for DNSSEC DLV (Look-aside Validation) has to be used. ISC provides one of these giving somewhat of a proxying DNSSEC validation.

To enable DNSSEC lookups on your servers the easiest approach is to use dnssec-configure which reconfigures your named.conf with the appropiate variables. To enable, run:
dnssec-configure --dnssec=on --dlv=on -u -b
This will add the following to your config:
options {
...
dnssec-enable yes;
dnssec-validation yes;
dnssec-lookaside . trust-anchor dlv.isc.org.;
dnssec-accept-expired yes;
...
}

...
include "/etc/named.dnssec.keys";
Open up /etc/named.dnssec and add the following to enable DLV:
include "/etc/pki/dnssec-keys/dlv/dlv.isc.org.conf";
This includes the DNSKEY of dlv.isc.org which is required to validate lookups towards this server. Unless you include the key lookups will not work. Notice that you may have to shuffle the files around a bit to adopt to your setup.

After you restart named your BIND server will query the DLV server for secure lookups, if you have logging enabled you will be able to see DNSSEC debug records.

As you probably know, when using signed entities keys are refreshed with a set constant of time. The recommended time for a DNSKEY is 1 month. Due to this the key you have present in your dlv.isc.org.conf will become deprecated after 1 month. I decided to solve this by making a perl-script re-creating the dlv-configuration every night to ensure that my server doesn't loose the capability of doing lookups. If DLV is unavailable no recursion will work!

Here's the script:

#!/usr/bin/perl

use IO::File;
use strict;
use warnings;
use Net::DNS;
use Data::Dumper;

my $dlv_file = "/var/named/chroot/etc/pki/dnssec-keys/dlv/dlv.isc.org.conf";

my $res = Net::DNS::Resolver->new(
nameservers => [qw(127.0.0.1)],
recurse => 1,
debug => 0
);

my $dnssec_key = $res->query("dlv.isc.org", 'DNSKEY');

my $dlvconf = new IO::File('<'.$dlv_file);

sub nl2br {
my $t = shift or return;
$t =~ s/([\r\n])/$1/g;
return $t;
}

open FILE, ">".$dlv_file;
print FILE "trusted-keys {\n\n";
print FILE for map {

"dlv.isc.org. ".$_->flags.' '.$_->protocol.' '.$_->algorithm.' "'.nl2br($_->key)."\";\n"

} $dnssec_key->answer;

print FILE "};\n";
close (FILE);

chown 25, 25, $dlv_file;

system("rndc reload");

exit 0;

Add the script to crontab and it will automatically update the DLV key as needed.