[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Lightweight DNS server for Spamikaze



Hi,

This is a first stab at a real time DNSBL for Spamikaze. Not tested to well yet, but seems to work OK. Feel free to include in Spamikaze if deemed worthy.

Regards,
M4


#!/usr/bin/perl
#
# Copyright (C) 2007 Martijn Lievaart <m@xxxxxxx>
# Released under the GNU GPL
#
# NO WARRANTY, see the file COPYING for details.
#
# This file someday may be part of the spamikaze project:
#        http://spamikaze.nl.linux.org/
#
# DNSBL daemon to query the spamikaze database in real-time
#
# Add something like this to your named.conf
#
#	zone "skbl.example.com" IN {
#		type forward;
#		forward only;
#		forwarders {
#			192.168.1.200 port 1234;
#		};
#	};
#
#
# Tested on Linux. Probably works on most Un*xes.
# Probably does not work on Windows.
#

use 5.007_003; # need safe signals

use Net::DNS;
use Net::DNS::Nameserver;
use Socket;

use strict;
use warnings;

use FindBin;
use lib "$FindBin::Bin";
use lib '/opt/spamikaze/scripts'; # for development 
use Spamikaze;

use Getopt::Long;
use File::Basename;
use File::Spec::Functions;
use POSIX 'setsid';
use Fcntl qw(LOCK_EX LOCK_NB);


my $programname = basename($0);
my $SELF = catfile $FindBin::Bin, $programname;
my $pidfile = '/var/run/skbld/skbld.pid';

my $help = 0;
my $foreground = 0;
my $verbose = 0;
my $debug = 0;
my $ttl;
# these should go into the main spamikaze config
my $port = 1234;
my $user;
my $group;

my @org_argv = @ARGV;

my $result =
    GetOptions ("help|h" => \$help,
		"verbose|v"  => \$verbose,
		"foreground|f" => \$foreground,
		"debug|d" => \$debug,
		"port|p=i" => \$port,
		"ttl|t=i" => \$ttl,
		"group|g=s" => \$group,
		"user|u=s" => \$user,
		);

usage($result) if (not $result or $help);

$verbose |= $debug;
$foreground |= $debug;

unless (defined($ttl)) {
    $ttl = $debug ? 0 : $Spamikaze::dnsbl_ttl;
}

# Create listening socket first, so we can listen on port 53 before
# dropping privs.
my $ns =
    Net::DNS::Nameserver->new(LocalPort    => $port,
			      ReplyHandler => \&reply_handler,
			      Verbose      => $debug, # verbose is to verbose :-)
			      )
    or die "couldn't create nameserver object\n";

# Use of global filehandle intentional!
open(SELFLOCK, "<$0") or die("Couldn't open $0: $!\n");
flock(SELFLOCK, LOCK_EX | LOCK_NB) or die("Aborting: another $programname is already running\n");

if ($group) {
    my $gid = getgrnam($user);
    if (defined($gid)) {
	$) = $gid;
	warn "Cannot switch to group $group: $!\n" if $!;
    } else {
	warn "User $group does not exist.\n";
    }
}


if ($user) {
    my $uid = getpwnam($user);
    if (defined($uid)) {
	$> = $uid;
	warn "Cannot switch to user $user: $!\n" if $!;
    } else {
	warn "User $user does not exist.\n";
    }
}

# See if we can connect to the DB. If not, we can give an error before
# we daemonize.
my $dbh = Spamikaze::DBConnect();

unless ($foreground) {
    print "Deamonizing...\n" if $verbose;
    if (my $pid = daemonize()) {
	# parent
	print "Daemonized\n" if $debug;
	$dbh->{InactiveDestroy} = 1;
	exit;
    }
    writepid($$);
}

print "$programname ready to answer queries on port $port.\n";
$0 = "$programname on port $port";

$SIG{'INT'} = \&siginthandler;
#$SIG{'HUP'} = \&sighuphandler; # perldoc perlipc recommends the following:
my $sigset = POSIX::SigSet->new();
my $action = POSIX::SigAction->new('sighuphandler',
				   $sigset,
				   &POSIX::SA_NODEFER);
POSIX::sigaction(&POSIX::SIGHUP, $action);

eval {
    $ns->main_loop();
};

if ($@ =~ "^OutOfHere at ") {
    print "$programname terminating.\n";
} else {
    print "$programname terminating due to unknown reason.\n";
    print "'$@'\n" if $@;
}

unless ($foreground) {
    unlink $pidfile or warn "Cannot unlink $pidfile";
}

exit;

#######################################################################

sub siginthandler {
    die "OutOfHere";
}

sub sighuphandler {
    print "got SIGHUP\n";
    exec($SELF, @org_argv) or die "Couldn't restart: $!\n";
}

sub writepid {
    my $pid = shift;
    if (open my $fh, ">$pidfile") {
	print $fh "$pid\n";
    } else {
	warn "can't open $pidfile: $!\n";
    }
}

sub daemonize {

    chdir '/'                 or die "Can't chdir to /: $!";

    # Get ready to daemonize by redirecting our output to syslog, requesting that logger prefix the lines with our program name:
    open(STDOUT, "|-", "logger -t $programname") or die("Couldn't open logger output stream: $!\n");
    open(STDERR, ">&STDOUT") or die("Couldn't redirect STDERR to STDOUT: $!\n");
    $| = 1; # Make output line-buffered so it will be flushed to syslog faster
    open STDIN, '/dev/null'   or die "Can't read /dev/null: $!";

    defined(my $pid = fork)   or die "Can't fork: $!";
    return $pid if $pid;

    setsid                    or die "Can't start a new session: $!";
    umask 0;
    return $pid;
}



sub reply_handler {
    my ($qname, $qclass, $qtype, $peerhost) = @_;
    my ($rcode, @ans, @auth, @add);

    $verbose and print "request for $qname/$qtype\n";
    if ($qtype eq "A" or $qtype eq "TXT") {
	if ($qname =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)\./) {
	    my ($octd, $octc, $octb, $octa) = ($1, $2, $3, $4);
	    my $ip = "$4.$3.$2.$1";

	    my $sql = "SELECT 1 FROM ipnumbers WHERE visible = 1
                         AND octa=$octa AND octb=$octb AND octc=$octc AND octd=$octd";
	    my $sth = $dbh->prepare($sql);
	    $sth->execute();

	    if ($sth->fetch) {
	      SWITCH: for ($qtype) {
		  /^A$/ and do {
		      push @ans, Net::DNS::RR->new("$qname $ttl $qclass $qtype 127.0.0.1");
		  };
		  /^TXT$/ and do {
		      my $dnsbl_url_base = $Spamikaze::dnsbl_url_base;
		      push @ans, Net::DNS::RR->new("$qname $ttl $qclass $qtype \"$dnsbl_url_base$ip\"");
		  };
	      }
		$debug and print "Spam IP: $ip\n";
		$rcode = "NOERROR";
	    } elsif ($sth->err) {
		warn $sth->errstr();
		$rcode = "SERVFAIL";
	    } else {
		$debug and print "Ham IP: $ip\n";
		$rcode = "NXDOMAIN";
	    }

	} else {
	    $rcode = "FORMERR";
	}
    } else {
	$rcode = "NOTIMP";
    }

    # mark the answer as authoritive (by setting the aa flag
    return ($rcode, \@ans, \@auth, \@add, { aa => 1 });
}



sub usage {
    my $rc = shift;
    print <<EOD;

Usage: $programname [options]

SpamiKaze BlackList Daemon, a light weight DNS server that
serves a spamikaze zone.

Options:
 help|h          Print this help.
 verbose|v       Be more verbose.
 debug|d         Gobs of debugging info (implies -v and -f).
 foreground|f    Don\'t daemonize.
 port|p          Port to listen on (default 1234)
 ttl|t           TTL to use for answers. Default is 0 if -d,
                 taken from spamikaze inifile otherwise.
 user|u          Switch to this user after binding to socket.
 group|g         Switch to this group after binding to socket.

EOD
    exit($rc);
}