[nsp-sec] [OT] Providers with RTBH capability?

Steve Colam sjc at eng.gxn.net
Tue Apr 9 08:04:49 EDT 2013


On Mon, 8 Apr 2013, Rabbi Rob Thomas wrote:

> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> Dear Steve,
>
>> We modified John K's original perl to suite our purposes, I'm happy
>> to share (probably via John first) and also help with how to setup
>> in a network.
>
> Please do share, thank you!
>

Attached script... modified version of Johns original.

We use a couple of 'route-injectors' in effecting acting as v4/v6
route reflectors to announce prefixes to the rest of our network
which we want blackholing.

If user is group 'core' in /etc/group provides a few extra options.

Provides bulk add and bulk remove options.

Provides sort-by column hrefs.

We have a couple of tags for statics:

tag 9000 - blackhole within own asn

you will statics to (and a Null0 interface):
ipv4 <v4>/32 Null0
ipv6 <v6addr>/128 Null0

and a match community route policy and then set the next-hop accordingly
- watch out if its 6pe learned prefix to use the v4 next-hop...  also tag
them with your favourite community to stop them escaping.

tag 9001 - blackhole within external (upstream network) - match on your
egress policy and set upstream blackhole community/etc.

We have an external script to do snmp pull/push; I've not included that.
Johns original has code to do this in it...

mkdir /tftpboot/bhrs; chgrp <www> /tftpboot/bhrs

Only supports /32s & /128s.


Steve @ AS5413
-------------- next part --------------
#!/usr/local/bin/perl
#
# $Id: bhrs2.pl,v 1.29 2013/04/09 11:43:28 sjc Exp $
#
###################################################################
##
## The Black Hole Router Server (bhrs.pl) Project
##
## A Perl/CGI/Web interface to a black hole route server
##   John Kristoff
##  jtk at northwestern.edu
##  February, 2005
##
## 20100111 Steve Colam
##   add tag, and int(39) for v6, sql address now a varchar(39) as mysql doesn't support bfint
##
###################################################################

#mysql> show fields from bhrs;
#+------------+------------------+------+-----+---------------------+----------------+
#| Field      | Type             | Null | Key | Default             | Extra          |
#+------------+------------------+------+-----+---------------------+----------------+
#| id         | int(10) unsigned |      | PRI | NULL                | auto_increment |
#| addtime    | datetime         |      | MUL | 0000-00-00 00:00:00 |                |
#| removetime | datetime         | YES  |     | NULL                |                |
#| address    | varchar(39)      |      | MUL | 0                   |                |
#| submitter  | varchar(8)       |      |     |                     |                |
#| revoker    | varchar(8)       | YES  |     | NULL                |                |
#| casenotes  | varchar(255)     | YES  |     | NULL                |                |
#| tag        | varchar(8)       | YES  |     | NULL                |                |
#+------------+------------------+------+-----+---------------------+----------------+
#
#--
#-- Table structure for table `bhrs`
#--
#
#DROP TABLE IF EXISTS `bhrs`;
#CREATE TABLE `bhrs` (
#  `id` int(10) unsigned NOT NULL auto_increment,
#  `addtime` datetime NOT NULL default '0000-00-00 00:00:00',
#  `removetime` datetime default NULL,
#  `address` varchar(39) NOT NULL default '0',
#  `submitter` varchar(8) NOT NULL default '',
#  `revoker` varchar(8) default NULL,
#  `casenotes` varchar(255) default NULL,
#  `tag` varchar(8) default NULL,
#  PRIMARY KEY  (`id`),
#  KEY `address` (`address`),
#  KEY `addtime` (`addtime`)
#) ENGINE=MyISAM DEFAULT CHARSET=latin1;
#


use strict;
$|=1;

use CGI qw(-nosticky :standard escapeHTML);
use CGI::Carp qw(fatalsToBrowser);				# fatal errors to browser

delete @ENV{ qw(IFS CDPATH ENV BASH_ENV) };			# make %ENV safer
$ENV{PATH} = "/bin:/usr/bin:/usr/local/bin";			# limit path
 
# global, read only vars
#
my $PROGNAME	= "BhRS";
my $RCSID	= '$Id: bhrs2.pl,v 1.29 2013/04/09 11:43:28 sjc Exp $';
my $VERSION	= '$Revision: 1.29 $';
my $SCRIPT	= "bhrs2.pl";
my $TFTPBOOT	= "/tftpboot/";			# system tftpboot root dir
my $TFTPDIR	= "bhrs";			# remote tftp directory view
my $ROUTER1	= "x.x.x.x";			# 1st route server - undef if not used
my $ROUTER2	= "y.y.y.y";			# 2nd route server - undef if not used
my $groupfile	= "/etc/group";			# system group file
my $groupname	= "core";			# system group to check to enable additional privs
my $extra_priv	= 0;				# default privs, 1 to override
my $cisco_config_copy = "/opt/core-scripts/bin/cisco-config-copy.pl";	# config push tool

# protect against DoS attacks
$CGI::DISABLE_UPLOADS	= 1;			# no uploads
$CGI::POST_MAX		= 1024 * 16;		# max 16K posts

# modules we'll be using
use Socket;				# for inet_aton
use DBI;				# for database connectivity
use Data::Validate::IP;			# ip validation - functions need to be moved to Net::IP;
use Net::IP;				# ip address mgmt

# database connectivity, site specific
my $dsn		= "DBI:mysql:bhrs:localhost:3306";
my $user	= "bhrs";
my $pass	= "<password>";
my ($dbh, $sth)	= (0, 0);			# db and statement handles

###

if (!$ENV{REMOTE_USER}) {			# must have remote user set
	dienice(__LINE__, "Authentication error.");
}

&enable_extra_priv;

if (param) {
	resolve("yes") if param('resolve') eq 'yes';				# manage ip/hostname resolution
	resolve("no") if param('resolve') eq 'no';				# manage ip/hostname resolution
}

print header,"\n";
print_start_html();

my $order_by = "address+0";
if (param) {
	$order_by = "addtime"	if param('order_by') eq "addtime";		# report ordering options
	$order_by = "submitter"	if param('order_by') eq "submitter";
	$order_by = "tag"	if param('order_by') eq "tag";
	$order_by = "casenotes"	if param('order_by') eq "casenotes";

	bulksub_add()		if param('bulksub') eq 'add';			# process bulk additions
	bulkremove()		if param('bulkremove') eq 'yes';		# process bulk removals
}


	
print h2("Black Hole Route Server Mgmt"), "\n";


$dbh = DBI->connect($dsn, $user, $pass) ||					# connect to db
	dienice(__LINE__, "Database connection error: " . DBI->errstr);

$sth = $dbh->prepare("select id,address,addtime,submitter,
	tag,casenotes from bhrs where removetime is null order by ".$order_by) ||
	dienice(__LINE__, "Database preparation error: " . DBI->errstr);

$sth->execute || dienice(__LINE__, "Database execution error: " . DBI->errstr);

my $cookiequery = new CGI;						# cookie for manage dns resolution
	my $resolvecookie = $cookiequery->cookie('RESOLVE');		# get the cookie value
	$resolvecookie = "no" if !$resolvecookie;
	print start_form(-method=>'GET', -action=>$SCRIPT);
	if ($resolvecookie eq "yes") {
		print hidden(-name=>'resolve', value=>'no'), "\n",
		submit(-value=>'disable DNS resolution');
	} else {
		print hidden(-name=>'resolve', value=>'yes'), "\n",
			submit(-value=>'enable DNS resolution');
	}
	print "\n", end_form, p, "\n";

print start_form(-method=>'GET', -action=>$SCRIPT),
	"Enter IP addresses in dotted decimal notation: ", br, "\n",
	hidden(-name=>'bulksub', value=>'add'), "\n",
	textarea(-name=>'addresses', -rows=>'5', cols=>'20'), br, "\n",

	"Enter case notes. Max 255 chars.", br, "\n",
	textfield(-name=>'casenotes', -size=>'50', -maxlength=>'255'), br, "\n",
	" &nbsp\n";

if ($extra_priv == 1) {
	print br,checkbox_group(-name=>'tag', -values=>['tag9001'])," (enable tag 9001)",p;
}

print submit(-value=>'Bulk Add Routes to the Black Hole!'), "\n",
	end_form, p, "\n";

print start_form(-method=>'GET', -action=>$SCRIPT),			# check box for bulk remove
	hidden(-name=>'bulkremove', value=>'yes'), "\n";


print "<table border=1>\n",
	"<caption>Current Black Hole Route List</caption>\n",
	"<tr>",
	"<th>",a({-href=>"$SCRIPT?order_by=address+0"},"IP address");
	
print "<th>Host name" if $resolvecookie eq "yes";

print "<th>",a({-href=>"$SCRIPT?order_by=addtime"},"Date added"),
	"<th>ID<th>",a({-href=>"$SCRIPT?order_by=submitter"},"Submitter"),
	"<th>",a({-href=>"$SCRIPT?order_by=tag"},"Tag"),
	"<th>",a({-href=>"$SCRIPT?order_by=casenotes"},"Notes"),
	"<th>",submit(-value=>'Bulk Remove'),"</tr>\n";

	# Loop through black hole routes, resolve addresses if possible
	my $name = "[undefined]";
	while(my($id,$address,$addtime,$submitter,$tag,$casenotes) = $sth->fetchrow_array) {
		dienice(__LINE__, "Database fetch error: " . DBI->errstr) if $DBI::err;

		if ($resolvecookie eq "yes") {
			$name = gethostbyaddr(inet_aton($address), AF_INET) || "[lookup failed]";
		} else {
			$name = "[skipped]";
		}

		print "<tr valign='center'>";
		#print a({-href=>"$SCRIPT?submit=display&address=$address"},$address);
		$address = bigint_to_ip($address);
		print "<td>$address</td>";
		print "<td>$name</td>" if $resolvecookie eq "yes";
		print "<td>$addtime</td><td>$id</td><td>$submitter</td><td>$tag</td><td>", escapeHTML($casenotes),"</td>";
		print "<td><input type=\"checkbox\" name=\"bulkdel\" value=\"$address\"></td>";
		print "</tr>\n";
	}
    
	# perform one last fetchrow_array error check
	dienice(__LINE__, "Database fetch error:" . DBI->errstr) if $DBI::err;

	# clean up
	print "</table>\n",
		end_form, "\n",
		p,"\n$RCSID\n",
		end_html, "\n";

	$sth->finish || dienice(__LINE__, "Database unable to finish: " . DBI->errstr);
	$dbh->disconnect || dienice(__LINE__, "Database disconnect error: " . DBI->errstr);



print "here\n<p>";
exit 0;




sub enable_extra_priv {				# enable extra privs if remove_user is in the right system group
	my $tmp_group;
	open(GROUP,$groupfile);
	while(<GROUP>) {
		if (/^$groupname/) {
			$tmp_group = $_;
			last;
		}
	} 
	close(GROUP);
    
	my $user = $ENV{REMOTE_USER};			# am I allowed access to remove tool
	if ($tmp_group =~ /(\:$user$|\:$user,|,$user,|,$user$)/) {
		$extra_priv = 1; 
	} else {
		$extra_priv = 0;
	}
}       

sub dienice {								# dienice() - something bad happened, exit gracefully
	my $linenum = shift;
	my $message = shift;
                
	print br, "\n",
		p("WARNING(", $linenum, "): ", $message, "\n"),
		p("Operation aborted.\n"),
		br, "\n",
		p(a({-href=>$SCRIPT},"Return to BHRS homepage"), "\n"),
		"<P>\n$RCSID\n",
		end_html, "\n";
	exit 1;
}   


sub resolve {								# manage dns resolution preference via cookie
	my $status = shift;
	my $query = new CGI;

	my $resolvecookie = $query->cookie(-name=>'RESOLVE',
		-value=>$status,
		-expires=>'+10y');
        
	print $query->header(-cookie=>$resolvecookie);

	print_start_html();

	print h1("Cookie set"),
		p(a({-href=>$SCRIPT},"Return to BHRS homepage"), "\n"),
		end_html, "\n";

	exit;
}               


sub bulksub_add {
	if (param('casenotes') eq "") {
		print h1("casenotes field not completed - use the back-button"), br, "\n",
			end_html, "\n";
		exit;
	}
	print h1("adding blackholes");
	my (@address) = split(/\s/, param('addresses'));

	my @ios_config_update;

	my $tag = "9000";					# default tag
	$tag = 9001 if param('tag') eq "tag9001";		# override to 9001

	foreach(@address) {
		my $ipaddr = $_;
		if ($ipaddr) {
			if(is_ipv4($ipaddr) || is_ipv6($ipaddr)) {					# is valid v4 or v6 addr

				# is addr already in db
				$dbh = DBI->connect($dsn, $user, $pass) || dienice(__LINE__, "Database connection error: " . DBI->errstr);

				$sth = $dbh->prepare("select distinct addtime,submitter,casenotes
					from bhrs where address = ? and removetime is null") ||
					dienice(__LINE__, "Database preparation error: " . DBI->errstr);

				$sth->execute(ip_to_bigint($ipaddr)) || dienice(__LINE__, "Database execution error: " . DBI->errstr);

				my($addtime,$submitter,$casenotes) = $sth->fetchrow_array;
				dienice(__LINE__, "Database fetch error: " . DBI->errstr) if $DBI::err;

				$sth->finish || dienice(__LINE__, "Database unable to finish: " . DBI->errstr);
				$dbh->disconnect || dienice(__LINE__, "Database disconnect error: " . DBI->errstr);

				if ($addtime) {							# addr is already present in the database
					print "error: $ipaddr is already being black holed", br, "\n";
					next;
				}

				# Retrieve the current system time, which becomes db addtime
				my ($sys_sec, $sys_min, $sys_hour, $sys_day,  $sys_mon, $sys_year, undef, undef, undef) = localtime(time);
				my $currenttime = sprintf("%04d\-%02d\-%02d %02d:%02d:%02d",
					$sys_year + 1900,
					$sys_mon + 1,
					$sys_day,
					$sys_hour,
					$sys_min,
					$sys_sec);

				# insert address into the blackhole list
				$dbh = DBI->connect($dsn, $user, $pass) || dienice(__LINE__, "Database connection error: " . DBI->errstr);

				$sth = $dbh->prepare("insert into bhrs (addtime, address, submitter, tag, casenotes) values (?,?,?,?,?)") ||
					dienice(__LINE__, "Database preparation error: " . DBI->errstr);

				$sth->execute($currenttime,
					ip_to_bigint($ipaddr),
					$ENV{REMOTE_USER},
					$tag,
					param('casenotes')) || dienice(__LINE__, "Database execution error: " . DBI->errstr);

				$sth->finish || dienice(__LINE__, "Database unable to finish: " . DBI->errstr);

				# now pull the ID that was created back out
				$sth = $dbh->prepare("select id from bhrs where address=? and addtime=?") ||
					dienice(__LINE__, "Database preparation error: " . DBI->errstr);

				$sth->execute(ip_to_bigint($ipaddr), $currenttime) || dienice(__LINE__, "Database execution error: " . DBI->errstr);

				my ($id) = $sth->fetchrow_array;
				$sth->finish || dienice(__LINE__, "Database unable to finish: " . DBI->errstr);

				$dbh->disconnect || dienice(__LINE__, "Database disconnect error: " . DBI->errstr);

				# ADD CONFIGDBPUSH CODE
				my $name = "BHRS-".$id."-".$ENV{REMOTE_USER};
				if(is_ipv4($ipaddr)) {
					push(@ios_config_update, "ip route $ipaddr 255.255.255.255 null0 tag $tag name $name");
				} else {
					if(is_ipv6($ipaddr)) {
						push(@ios_config_update, "ipv6 route $ipaddr/128 null0 tag $tag");	# v6 statics don't support 'name'
					}
				}
				next;

			} else {
				print "error: skipped ($ipaddr) - not a valid v4 or v6 address<br>\n";	# no valid
				next;
			}
		} else {
			next;
		}
	}
	# process config add to router
	my $return = router_config_update(@ios_config_update);
	if ($return == 0) {
		print "config push complete ok",br,"\n";
	} else {
		print "error: config push returned $return - contact core\n" if $return != 0;					# config push failed if not 0 returned
	}

	print p(a({-href=>$SCRIPT},"Return to BHRS homepage"), "\n"), end_html, "\n";
	exit;

}

sub bulkremove {
        print h1("removing blackholes");
	my @bulkdel = param('bulkdel');

	my @ios_config_update;

	foreach(@bulkdel) {
		my $ipaddr = $_;
		print "processing: ($ipaddr)", br, "\n";

		# get the address from the database
		$dbh = DBI->connect($dsn, $user, $pass) || dienice(__LINE__, "Database connection error: " . DBI->errstr);

		$sth = $dbh->prepare("select distinct addtime,submitter,casenotes
			from bhrs where address = ? and removetime is null") ||
			dienice(__LINE__, "Database preparation error: " . DBI->errstr);

		$sth->execute(ip_to_bigint($ipaddr)) || dienice(__LINE__, "Database execution error: " . DBI->errstr);

		# expect to only get one row of data, but loop just in case
		my $count = 0;
		my($id,$addtime,$submitter,$casenotes) = (undef, undef, undef, undef);
		while(my @array = $sth->fetchrow_array) {
			dienice(__LINE__, "Database fetch error: " . DBI->errstr) if $DBI::err;
			$addtime	= $array[0];
			$submitter	= $array[1];
			$casenotes	= $array[2];
			$count++;
		}

		dienice(__LINE__, "Database fetch error: " . DBI->errstr) if $DBI::err;		# perform one last fetchrow_array error check

		# disconnect from db
		$sth->finish || dienice(__LINE__, "Database unable to finish: " . DBI->errstr);
		$dbh->disconnect || dienice(__LINE__, "Database disconnect error: " . DBI->errstr);

		dienice(__LINE__, "Duplicate records detected.  Contact DB admin.") if $count > 1;		# dup records
		dienice(__LINE__, "No records found.") if $count < 1;						# error 0 records

		my ($sys_sec, $sys_min, $sys_hour, $sys_day, $sys_mon, $sys_year, undef, undef, undef) = localtime(time);
		my $currenttime = sprintf("%04d\-%02d\-%02d %02d:%02d:%02d",
			$sys_year + 1900,
			$sys_mon + 1,
			$sys_day,
			$sys_hour,
			$sys_min,
			$sys_sec );

		# remove address from the black hole list
		$dbh = DBI->connect($dsn, $user, $pass) || dienice(__LINE__, "Database connection error: " . DBI->errstr);

		$sth = $dbh->prepare("update bhrs set revoker = ?, removetime = ?
			where address = ? and removetime is null") ||
			dienice(__LINE__, "Database preparation error: " . DBI->errstr);

		$sth->execute($ENV{REMOTE_USER}, $currenttime, ip_to_bigint($ipaddr)) ||
			dienice(__LINE__, "Database execution error: " . DBI->errstr);

		$sth->finish || dienice(__LINE__, "Database finish error: " . DBI->errstr);
		$dbh->disconnect || dienice(__LINE__, "Database disconnect error: " . DBI->errstr);

		# ADD CONFGDBPUSH CODE
		if (is_ipv4($ipaddr)) {
			push(@ios_config_update, "no ip route $ipaddr 255.255.255.255 null0");
		} else {
			if (is_ipv6($ipaddr)) {
			push(@ios_config_update, "no ipv6 route $ipaddr/128 null0");
			}
		}
		#print "remove ($ipaddr) ($currenttime)<br>";
	}
	# process config remove from router
	my $return = router_config_update(@ios_config_update);
	if ($return == 0) {
		print "config push complete ok",br,"\n";
        } else {
                print "error: config push returned $return - contact core\n" if $return != 0;                                   # config push failed if not 0 returned
        }


	print "error: config push returned $return - contact core\n" if $return != 0;				# config push failed if not 0 returned

	print p(a({-href=>$SCRIPT},"Return to BHRS homepage"), "\n"),
		end_html, "\n";
	exit;
}

sub print_start_html {
	print	start_html( -title  => "$SCRIPT $PROGNAME $VERSION" ), "\n",
		'<link rel="shortcut icon" href="/companyicons/favicon.ico">', "\n",
		img{src => "/companyicons/logo.png"}, "\n";
	return;
}

sub ip_to_bigint {						# convert ip addr to int(39)
	my $addr_to_convert = shift @_;
	my $ip = new Net::IP($addr_to_convert) || dienice(__LINE__, "Net::IP, cannot process address: ".$addr_to_convert."\n");
	return $ip->intip();
}

sub bigint_to_ip {
	my $bigint_to_convert = shift @_;
	if ($bigint_to_convert > 4294967295) {			# assume this is a v6 addr
		my $bit_string = Net::IP::ip_inttobin($bigint_to_convert,6);
		my $net_ip_handle = new Net::IP(Net::IP::ip_bintoip ($bit_string,6));
		return $net_ip_handle->short();
	} else {
		my $bit_string = Net::IP::ip_inttobin($bigint_to_convert,4);
		return Net::IP::ip_bintoip ($bit_string,4);
	}
}

sub router_config_update {
	my @router_config_update_ios_config_update = @_;
	if ($#router_config_update_ios_config_update == -1) {
		# nothing to do
		return(0);
	}

	my $router_config_update_tftpfile = $TFTPDIR."/".$PROGNAME.".".$$.".".$ENV{REMOTE_USER};
	my $router_config_update_tmpfile = $TFTPBOOT.$router_config_update_tftpfile;

	print "building router config update file ($router_config_update_tmpfile)<br>\n";

	print p,"pushing config (this will take a few seconds...) :",br,"\n";
	open(FILEHANDLE, ">$router_config_update_tmpfile");
		foreach(@router_config_update_ios_config_update) {
			print $_,br,"\n";
			print FILEHANDLE $_."\n";
		}
		print FILEHANDLE "do write\n!\nend\n";
	close(FILEHANDLE);
	my $return_router1 = system("$cisco_config_copy -q -h $ROUTER1 -u $router_config_update_tftpfile");
	my $return_router2 = system("$cisco_config_copy -q -h $ROUTER2 -u $router_config_update_tftpfile");
	unlink $router_config_update_tmpfile;
	if ($return_router1 == 0 && $return_router2 == 0) {
		return(0);
	} else {
		return(1);
	}
}




More information about the nsp-security mailing list