#!/usr/bin/perl -wT
#
# GreyDNS - Greylisting DNS-Server
# Copyright (C) 2007  Robert Schulze (rob@rob-schulze.de)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
###############################################################################

use strict;
use Socket;
use POSIX 'setsid';
use Storable;

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

my $PORT=53000;
my $LISTEN='127.0.0.1';
my $LOG='/tmp/greydns.warn';
my $DB='/tmp/greydns.tbl';

# kompletter Pfad zu diesem Skript, fuer exec bei SIGHUP
#
my $GREYDNS='/home/rob/greydns/greydns-004';

# nach $GREY_DELAY Sekunden wird die NXDOMAIN geliefert
#
my $GREY_DELAY=10;

# nach $REMOVE_DELAY Sekunden ohne weitere Abfrage wird der
# Eintrag aus der DB entfernt oder die Domain neu grau gelistet
#
my $REMOVE_DELAY=(60*60); # 1 Stunde

my $ADDR_GREYLISTED=inet_aton('127.0.0.2');
my $TXT_GREYLISTED='450 greylisted';

my $DIALUP_REGEX=qr/t-ipconnect\.de$|tisdip\.tiscali\.de$|ipt\.aol\.com$|vie\.surfer\.at$|chello|bluewin\.ch$|comcast\.net$|ameritech\.net$|pacbell\.net$|attbi\.com$|swbell\.net$|optonline\.net$|charter\.com$|hinet\.net$|\.rr\.com$|bbtec\.net|custom|optin|node|dial|dsl|cable|dyn|ppp|client|dhcp|(([0-9]{1,3}[-\.])+){3}[0-9]{1,3}|\.arpa$|\.it$|\.jp$|\.tw$|\.ru$|\.br$|\.hk$|\.siteprotect\.com$|\.procampaign\.net$|^195\.226\.164|\.domeus\.com$|npgco\.com$/;


###############################################################################
# Hier nix aendern!
# vorkompilierte Teile des DNS-Protokolls
###############################################################################

my $_DNS_BODY_A=pack
	(
	'n n N n a*',1,1,0,
	length($ADDR_GREYLISTED),$ADDR_GREYLISTED
	);
my $_DNS_BODY_TXT=pack
	(
	'n n N n a*',16,1,0,
	length(pack('Ca*',length($TXT_GREYLISTED),$TXT_GREYLISTED)),
	pack('Ca*',length($TXT_GREYLISTED),$TXT_GREYLISTED)
	);

my $_DNS_HDR_LOCALHOST=pack
	(
	'C2 n4',
	128,0,1,2,0,0
	);

my $_DNS_HDR_NXDOMAIN=pack
	(
	'C2 n4',
	128,3,1,0,0,0
	);

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

BEGIN
	{
	$ENV{'PATH'}='';
	}

if(!-e $GREYDNS)
	{
	die("$GREYDNS existiert nicht, bitte \$GREYDNS anpassen!");
	}

daemonize();

my $paddr=sockaddr_in($PORT,inet_aton($LISTEN));
socket(SOCKET,PF_INET,SOCK_DGRAM,getprotobyname('udp')) || die "socket: $!";
if(!bind(SOCKET,$paddr))
	{
	die "unable to bind to $LISTEN\:$PORT ($!)\n";
	}

my $TBL={};
if(-e $DB)
	{
	warn "found $DB, loading...\n";
	$TBL=retrieve($DB);
	warn "read ".keys(%{$TBL})." entries\n";
	}

# Signalhandler erst setzen, wenn die DB gelesen wurde
# sonst koennten komische Situationen auftreten
#
my $sigset=POSIX::SigSet->new();
POSIX::sigaction(POSIX::SIGHUP(),POSIX::SigAction->new('restart',$sigset,POSIX::SA_NODEFER()));
POSIX::sigaction(POSIX::SIGTERM(),POSIX::SigAction->new('terminate',$sigset,POSIX::SA_NODEFER()));

my $request='';
while(1)
	{
	my $hispaddr=recv(SOCKET,$request,512,0);
	if(!$hispaddr)
		{
		warn "recv failed: $!\n";
		next;
		}

	my $res='';
	my $now=time();
	my $rq=unpack_request($request);

	#if(get_ptr($rq->{'DOM'})!~m/$DIALUP_REGEX/)
	#	{
	#	$res=pack_response_nxdomain($rq);
	#	}

	if(defined($TBL->{$rq->{'DOM'}}))
		{
		if( ($TBL->{$rq->{'DOM'}}) > $now)		
			{
			# weiterer Request innerhalb GREY_DELAY
			#
			$res=pack_response_localhost($rq);
			}
		elsif( ($TBL->{$rq->{'DOM'}}+$REMOVE_DELAY) < $now)
			{
			# letzte Abfrage ausserhalb des Rahmens		
			# Greylisting neu starten
			#
			$TBL->{$rq->{'DOM'}}=$now+$GREY_DELAY;
			$res=pack_response_localhost($rq);
			}
		else
			{
			# letze Abfrage ist im Rahmen
			# Greylisting nur erneuern
			#
			$TBL->{$rq->{'DOM'}}=$now;
			$res=pack_response_nxdomain($rq);
			}
		}
	else
		{
		# erster Request ueberhaupt
		#
		$TBL->{$rq->{'DOM'}}=$now+$GREY_DELAY;			
		$res=pack_response_localhost($rq);
		}
	

	if(length($res)>512)
		{
		warn "generated udp-msg exceeds 512 bytes\n";
		# TC-Bit setzen?
		}

	if(!send(SOCKET,$res,0,$hispaddr))
		{
		warn "send failed: $!\n";
		}
		
	$request='';
	}

exit(0);

sub daemonize 
{
chdir('/') || die "Can't chdir to /: $!";
open(STDIN,'/dev/null') || die "Can't read /dev/null: $!";
open(STDOUT,'>/dev/null') || die "Can't write to /dev/null: $!";
defined(my $pid = fork) || die "Can't fork: $!";
exit if $pid;
setsid() || die "Can't start a new session: $!";
open(STDERR,">>$LOG") || die "Can't dup stdout: $!";
warn "Started with pid $$\n";
return 1;
}

sub unpack_request
{
my %r=();

# Header
#
@r{'ID','F1','F2','QN','AN','AA','ARR'}=unpack('n C2 n4',$_[0]);


# betreffende Domain
#
my $question=substr($_[0],12);
$r{'QNAME'}=substr($question,0,-length(pack("n2",1,1)));
$r{'QUESTION'}=$question;

my $domain='';
my $finished=0;
while(!$finished)
	{
	my($len,$part)=unpack('Ca*',$question);
	$part=substr($part,0,$len);

	if($part ne '')
		{
		$domain.="$part\.";
		}
	else
		{
		$finished=1;
		}
	$question=substr($question,length(pack('Ca*',$len,$part)));
	}


$r{'DOM'}=substr($domain,0,-1);

# Type und Class
#
@r{'TYPE','CLASS'}=unpack("n2",$question);

return \%r;
}

sub pack_response_nxdomain
{
my $r=shift;
my $res=pack('n',$r->{'ID'});
$res.=$_DNS_HDR_NXDOMAIN;
$res.=$r->{'QUESTION'};
return $res;
}

sub pack_response_localhost
{
my $r=shift;
my $res=pack('n',$r->{'ID'});
$res.=$_DNS_HDR_LOCALHOST;
$res.=$r->{'QUESTION'};

# A
#
$res.=$r->{'QNAME'}.$_DNS_BODY_A;

# TXT
#
$res.=$r->{'QNAME'}.$_DNS_BODY_TXT;

return ($res);
}

###############################################################################
# cleanup
# raeumt auf und sichert die uebrig gebliebenen Datensaetze in einer Datei
###############################################################################

sub cleanup
{
my $limit=time()-$REMOVE_DELAY;
my $exp=0;

while(my($ip,$stamp)=each(%{$TBL}))
	{
	if($stamp < $limit)
		{
		delete($TBL->{$ip});
		$exp++;
		}
	}
store($TBL,$DB);
warn "cleanup: stats (time/expired/db-size) ".join("/",time()-($limit+$REMOVE_DELAY),$exp,(-s $DB))."\n";
}

###############################################################################
# get_ptr
# liefert den PTR Record einer IP-Adresse zurueck
# add-on by Sirko Zidlewitz
###############################################################################

sub get_ptr
{
my $ip=shift;
my @hostnames=gethostbyaddr(pack('C4',split('\.',$ip)), AF_INET);
if(!defined($hostnames[0]))
	{
	$hostnames[0]=$ip;
	}
return $hostnames[0];
}


###############################################################################
# restart
# raeumt auf und startet das Programm via exec() neu, da sonst Unmengen an 
# Speicher angesammelt werden
###############################################################################
 
sub restart
{
local $SIG{'HUP'}='IGNORE';
cleanup();
warn "SIGHUP... restarting\n";
exec($GREYDNS);
}

###############################################################################
# terminate
# raeumt auf und stirbt
###############################################################################

sub terminate
{
local $SIG{'HUP'}='IGNORE';
cleanup();
warn "SIGTERM... exiting\n";
exit(0);
}


