[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",
"  \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