#!/usr/bin/perl 
###########################################################
# txt2rbl
#
# Converts local ip blacklist to DNS RBL zone file
#
# Available from: <http://www.postconf.com/docs/txt2rbl>
# See also: <http://www.postconf.com/docs/txt2rhsbl>
###########################################################
$SRCTXT="/etc/spamips";
$DSTDB="/var/named/db.example.com.rbl";
$ZONE='rbl.example.com';
$MESSAGE="per <http://rbl.example.com/>";
$ZONENS='ns.example.com';
$ZONEADMIN='hostmaster.example.com';
$TTL=86400;
$SERIALNO=20000730;
@WHITELIST=('127','192.168','10');
###########################################################
# to do: increment serial# for slave servers,
#        test zone file with nslint,
#        check for overlapping subnets,
#        warning msg if duplicate, ...
###########################################################
$TMPDB="$DSTDB.tmp";

if ( ! -w $DSTDB ) {
	print "  ERROR: $DSTDB not found or not writeable.\n";
	exit;
} elsif ( ! -s $SRCTXT ) {
	print "  ERROR: empty source file: $SRCTXT. \n";
	exit;
}

if ( -f $TMPDB ) {
	print "  Lock ($TMPDB) found, sleeping 60 seconds.\n";
	sleep 60;
	for ( $i=1 ; $i<5 ; $i++ ) {
		if ( -f $TMPDB ) {
			print "  Lock ($TMPDB) found, sleeping another 60 seconds.\n";
			sleep 60;
		}
	}
	if ( -f $TMPDB )  {
		print "  Lock ($TMPDB) still found after 5 minutes, giving up.\n";
		exec `logger "  Lock ($TMPDB) still found after 5 minutes, giving up.\n"`;
		exit;
	}
}

sub Terminate() {
	close($SRCTXT);
	close($TMPDB);
	unlink($TMPDB);
	die("$_[0]");
}
$SIG{ INT } = \&Terminate;
$SIG{ KILL } = \&Terminate;
$SIG{ TERM } = \&Terminate;
$SIG{ QUIT } = \&Terminate;

print "Updating $ZONE from $SRCTXT to $DSTDB...\n";

open(TMPDB,">$TMPDB") || die("  Cannot create $TMPDB\n");

print TMPDB <<"TAG";
\$TTL $TTL
@ IN SOA $ZONENS. $ZONEADMIN. (
	$SERIALNO 86400 43200 604800 $TTL )

	IN NS ${ZONENS}.

2.0.0.127          A    127.0.0.2
2.0.0.127          TXT  "$MESSAGE for testing"

TAG

open(SRCTXT, $SRCTXT)|| \&Terminate("  Cannot read $SRCTXT\n");
ADDRESS: foreach (<SRCTXT>) {

	#### parse source file ####
	next ADDRESS if /(^#|^$|^\s)/;  # ignore comments, nulls, start with space/tab
	next ADDRESS if /\s(HOLD|DISCARD|WARN|LOCAL)/; # skip local tags
	@line = split(/\s/, $_);         # strip comments/tags
	$_ = $line[0];
	chomp;

	#### validate IP syntax ####
	if ( /[^0-9\.]/ || /\.\./ || /^\./ || /\.$/ ) { 
		#### illegal chars, sequential dots, leading/trailing dots
		print "  REJECT:  $_  is not a valid IP address format.\n";
		next ADDRESS
	}

	#### test octects ####
	@ip = split(/\./, $_);
	if ( $ip[4] || $ip[0] == 0 || $ip[0] == 127 || $ip[0] == 255 || $ip[3] == 255 ) {
		# must be IPv4, not broadcast, and not localhost
		print "  REJECT:  $_  is not a valid IP host or subnet.\n";
		next ADDRESS
	}
	for ( $octet = 0 ; $octet < 4 ; $octet++ ) {
		if ( $ip[$octet] < 0 || $ip[$octet] > 255 || $ip[$octet] =~ /^0[0-9]/ ) {
			print "  REJECT:  $_  contains an invalid octet.\n";
			next ADDRESS
		}
	}

	#### check whitelist ####
	$checkip = $_;
	foreach $whitelisted ( @WHITELIST ) {
		chomp $whitelisted;
		if ( $checkip eq $whitelisted ) {
			print "  REJECT:  $checkip  is whitelisted\n";
			next ADDRESS
		} # perl bug, -w may generate "panic: pp_iter"
	}

	#### convert to DNS RR ####
	if ( ! $ip[3]) {
		$_ = "*.$ip[2].$ip[1].$ip[0]";
		s/\.+/./;   # delete sequential dots
	} else {
		$_ = "$ip[3].$ip[2].$ip[1].$ip[0]";
	}

	#### cull duplicates ####
	$index{$_}++;
}
foreach (sort keys (%index) ) {
	#### A and TXT record ####
	printf TMPDB ("%-18s A   127.0.0.2\n", $_);
	printf TMPDB ("%-18s TXT \"$MESSAGE\"\n", $_);
}

close(SRCTXT);
close(TMPDB);
rename $TMPDB, $DSTDB;
exec `/usr/bin/killall -1 named`;
