#!/usr/bin/perl5 # route-leecher.pl - a component using SpamShield 2.0 beta technology # parameters on command line (space-separated) # 'n [n] [n]' - AS numbers, space-separated # 'sum' - summary of routes (robs detail of AS origin) (needs cidr-convert.c) # 'nosum' - no summary of routes (default) # 'send' - put out sendmail access file format # 'nosend' - output in human-readable form (default) # 'trans' - include all routes transiting through a particular AS (default) # 'notrans' - only include routes that originate from a particular AS # 'quick' - ASNs to be presented to router in a single regexp whenever possible # 'noquick' - ASNs to be queried one by one (default) # # mixing arguments to effect the ASNs that follow is permitted: # Example: $ route-leecher sum send 8143 nosend 8081 nosum 22934 # $ route-leecher sum send all 8143 8081 22934 # (C) 2003 Kai Schlichting, All Rights Reserved. # # V1.8 20031101 route-views.oregon-ix.net activated a new command, making the # current query string ambigious: "show ip bgp replication". # Changing our query string: "sh ip bg re" -> "sh ip bg reg", # adapting $line_length maximum to 226 # New route-server: route-views.wide.routeviews.org # Route-server removed from default: route-server.bbnplanet.net (dead?) # history: # V1.7 20030713 Sendmail syslog msgs chop off "( ... )" par'd output. Changed # access list output for "(upstreams: ..." to not use ()'s # # V1.6 20030530 timeouts from route-servers: full-line regexp's in 'quick' mode # can exceed 5 min. new $line_length limit to cut down the matches # and not see a timeout (Cisco routers returning output count that # as idle time!) # V1.5 20030529 random route-server selection will repeat 5 times with different # RS if the selected one is down or refuses connections # V1.4 20030510 added trans|notrans|noquick command line options # V1.3 20030509 cisco routers don't take more than 254 chars per input line, now # splitting AS list of the resulting regexp in 'quick' mode is too long. # now also collapsing AS paths with origin-AS prepends to find the right # upstream(s) every time. # V1.2 20030424 - added 'quick' option so we can query a large number of ASNs in # a single command to a router: querying 60+ AS's was getting awfully slow # V1.1 20030403 - can summarize routes with external "cidr-convert" now # see http://www.spamshield.org/cidr-convert.c - unknown author # V1.01 20030329 - will process AS numbers given on command line now # V1.00 first version 20030302 # Permission to redistribute and use is restricted by the # SpamShield General Use (SGU) license. # Users and victims of this software have no claims of liability # or expectation thereof against the authors/distributors/agents or third parties # in any way affiliated or connected with the forestanding. The US UCITA is specifically # disclaimed under any and all circumstances arising from the use/distribution of this # software. This is not software, but merely copies of electrons, unless we say so. # Purpose: # Suck routes originating from or transiting through a given autonomous system (AS) # from a Cisco router with a BGP4-view of the world's routes and display them either # for human evaluation or as a Sendmail-compatible access file. use Net::Telnet::Cisco; # http://nettelnetcisco.sourceforge.net/docs.html # www.CPAN.org is your friend installing this module: # 1. Run the CPAN shell from your command-line: # $ perl -MCPAN -e shell # 2. If this is your first time running CPAN, it will ask you a number of setup questions. # 3. Install the module: # cpan> install Net::Telnet::Cisco local $sm_out = 0 ; # 0 or 1 : output in sendmail access file format (the CIDR notation used # requires you to process your access file with the 'cidrexpand' # tool from $SENDMAIL_SRC/contrib/ before Sendmail can use it!) local $transit_routes = 1; # 0 if we only look at routes originating from an AS # 1 if we include routes from other AS's that use the AS as transit local $summarize_routes = 0; # 1 will collapse routes into larger blocks if possible, # at the expense of not being able to show the upstream(s) # example: # 10.10.8.0/24, 10.10.9.0/24, 10.10.10.0/23 => 10.10.8.0/22 # this requires a compiled cidr-convert.c program in your execution $PATH # see http://www.spamshield.org/cidr-convert.c - unknown author local $quick = 0; # 0 = separate queries for every AS, 1 = one big regexp for all ASNs # put out all routes of this list of AS's # don't be stupid and enter an AS with 20,000 routes here: Net::Telnet::Cisco will # likely run out of buffer space, and the router queried will choke with 100% cpu for # an extended time, probably severely impacting its performance. # my @as_list; @as_list = ('send','quick',3502,13488,19181,22653,6358,13680,19962,22428,18695,26814,8081,27332, 10721,13645,29698,29713,26956,11938,21510,27437,30035,21875,26985,22934,26797,10276, 27255,26522,29986,10314,6432,18942,23307,22625,21733,18633,26824,20401,25752,20473, 13864,22298,8082,13890,16941,24996,11509,13627,28706,22320,15083, 26857,15026,19337,26228,10316,27257,17111,11778,26483,26967, 8143,15331,12117,3726,5770,13419,22867,20290,16506,16186,27595,26346, 15188,25555,25751,25653,14091,18747,26891,4447,21869,26978,17239,8003, 29804,11784,25839, 21949,20854,29893,9656,5042,5033,7599,3408,3409,3410,3411,25761,29761, 29963,30080,23522,29809,14501,26277,30038,29994,7796,26803,16626,10621, 27349,22099,27340,30471); # comment this out to use the above list of ASNs rather than command line arguments @as_list = @ARGV; my $my_router = ""; # use your own router whenever possible to avoid killing the route-servers # most route-servers PROHIBIT scripted/automated use - you may be banned from # accessing them PERMANENTLY if the operators deem you to abuse their resources! # if in doubt, make arrangements with an operator (or owner of a non-public # route-server or owner of a BGP4-speaking router) # $my_router = 'route-server.bbnplanet.net'; # $my_router = 'route-server.ip.att.net'; # having less then 2 routers in this list will result in an endless loop! my @route_servers = ( # 'route-views.oregon-ix.net', 'route-views2.oregon-ix.net', 'route-views.wide.routeviews.org', 'route-server.cerf.net', 'route-server.exodus.net', 'route-server-eu.exodus.net', 'route-server.as5388.net', 'route-server.gblx.net', 'route-server.colt.net', # 'route-server.bbnplanet.net', # down since 10/2003? # 'route-server.ip.att.net', # sometimes doesn't return anything for 'sh ip bgp reg', but ok on the second try. weird. # 'route-server.opentransit.net', # can't use it, as they current don't allow 'term length 0' # 'zebra.swinog.ch', # unstable? swinog.ch and frnog.org ) ; my $login= ""; # login for route-server, if any my $pass = ""; # password for route-server, if any ################# sub random_rs { my $num_rs = (@route_servers); my $router = @route_servers[(rand $num_rs)]; return ($router); } ################# if ($my_router eq "") { $router = &random_rs; unless ($sm_out) { print "# Randomly selected router $router\n"; } } else { $router = $my_router; } # for summarizing routes # comment-out this line if you don't have this or don't want to use this. local $cidr_convert = "/usr/local/bin/cidr-convert"; # will use tmp file in /tmp # see http://www.spamshield.org/cidr-convert.c - unknown author my $timeout = 5*60; # seconds - some route-servers are VERY busy, especially if the # output is very long. my $line_length = 100; # Max is 226! # (do not exceed: queries will break or not work!) # most route-servers have very short timeouts: limit # the assembly of 'quick' mode regexps to this number # of characters to limit the number of ASNs matched, # and the time for the RS to return output. # The time waiting for output is counted as idle time # on Cisco routers! ################### if ($summarize_routes) { if (defined $cidr_convert) { if (! -e $cidr_convert) { print "can't find $cidr_convert\n"; exit 0; } } else { print "\$cidr_convert is not defined, but required for \$summarize_routes = 1\n"; exit 0; } } my $last_router = $router; my $tries = 5; my $session; while ($tries) { if ($session = Net::Telnet::Cisco->new( Host => $router, errmode => "return", Timeout => 10) ) { $tries = 1; last; } else { if ($my_router ne "") { # a configured router is not available? good-bye print "# Could not connect to configured router $router\n"; exit 1; } else { while ($last_router eq $router) { $router = &random_rs; } unless ($sm_out) { print "# router $last_router not responding, retrying with router $router\n"; } $last_router = $router; } } $tries -= 1; } unless ($tries) { print "# All routers tried are not responding - quitting.\n"; exit 1; } if ($my_router eq "") { unless ($sm_out) { print "# Using router $router\n"; } } # make our buffers a bit bigger my $buf_size = 10 * 1024 * 1024; $session->max_buffer_length($buf_size); unless ($sm_out) { print "# Logging into router $router\n"; } # log in unless ( $session->login($login,$pass) ) { print "error logging into $router - giving up.\n"; exit 1; } # print "# Setting NO PAGING\n"; # no paging unless ( $session->cmd(String => 'terminal length 0', Timeout => 10) ) { print "error setting term length on $router\n"; exit 1; } local $as; my $cmd; my $as_all_list = ""; AS: foreach $as (@as_list) { local @route_block; # store routes for summarizing here. if ($as =~ /^\d*$/) { # ok, it's numeric, found an AS number if ($quick) { # add to list, move on in list $as_all_list .= $as."|"; # did we grow dangerously close to 254-char line limit, including 'sh ip bgp reg' etc.? # this takes 21 chars, plus 2 for "()", plus a max. of 5 for the next AS = 227 chars max if ( (length $as_all_list) > $line_length) { # ok, do the query chop $as_all_list; # trailing | - removes one character, hence we can use > 227 &query( "(".$as_all_list.")" ); # nice regexp: (asn|asn2|asn3|...) $as_all_list = ""; # delete the list } next AS; # move on with next AS } else { # not quick: do the query now # if ($sm_out) { # print "#\n"; # } &query($as); # do the query next AS; # and next } } else { # we have a non-numeric argument (likely: command) # check if we have collected ASNs in 'quick' mode, # if yes, do the query with all currently set options # before moving on to processing of new argument. if ($as_all_list ne "") { # ok, we have something, do the query chop $as_all_list; # trailing | &query( "(".$as_all_list.")" ); # nice regexp: (asn|asn2|asn3|...) $as_all_list = ""; # delete the list # now move on to argument processing } } if ($as =~ /^quick.*$/ ) { # turn quick mode on $quick = 1; next AS; } if ($as =~ /^noquick.*$/ ) { # turn quick mode off $quick = 0; next AS; } if ($as =~ /^sum.*$/ ) { # turn summary on $summarize_routes = 1; next AS; } if ($as =~ /^nosum.*$/ ) { # turn summary off $summarize_routes = 0; next AS; } if ($as =~ /^send.*$/ ) { # turn sendmail-compatible output on $sm_out = 1; next AS; } if ($as =~ /^nosend.*$/ ) { # turn sendmail-compatible output off $sm_out = 0; next AS; } if ($as =~ /^trans.*$/ ) { # match transit through AS $transit_routes = 1; next AS; } if ($as =~ /^notrans.*$/ ) { # match transit through AS $transit_routes = 0; next AS; } print "# ignoring unknown argment $as\n"; } # foreach ($as) # we might have had an ASN as last argument (likely), and are in 'quick' mode if ($as_all_list ne "") { # ok, we have something, do the query chop $as_all_list; # trailing | &query( "(".$as_all_list.")" ); # nice regexp: (asn|asn2|asn3|...) } # $session->cmd('exit'); # this breaks us somehow. exit 0; ################################################ sub query { # use assembled $as_all_list; my $query_list = $_[0]; if ($transit_routes) { $cmd = "sh ip bg reg \^\.*\_$query_list\_\.*\$" ; print "# using command: $cmd\n"; print "# Routes transiting through or originating from AS $query_list :\n\n"; } else { $cmd = "sh ip bg reg \^\.*\_$query_list\_\$" ; print "# using command: $cmd\n"; print "# Routes originating from AS $query_list :\n\n"; } my @output = $session->cmd(String => $cmd, Timeout => $timeout); my $out_lines = @output; if ($out_lines >= 2) { my $line; local $current_route; local %ass; local $as_seen; foreach $line (@output) { if ($line =~ /^.*?(\d+\.\d+\.\d+\.\d+.*?)\s+(\d+\.\d+\.\d+\.\d+).*\s{2,}0\s(.*)\s(\d+)\s(i|e|\?)$/ ) { # we got a line with a route near the left side my $new_route = $1; my $new_as = $4; my $new_upstream = 0; # print "got line: $line"; # print "new origin AS: $new_as , AS path: $3\n"; my (@path_list) = split (' ',$3); my $next_as; while ($next_as = pop @path_list) { if ($next_as != $new_as) { $new_upstream = $next_as; # print "new origin AS: $new_as , found upstream $next_as\n"; last; } } # if $new_upstream this is 0, we accidentially caught the 'Weight' column, # meaning the upstream is in the AS of the router we are using! # (todo: map route-server to own AS, so we know which one!) # we have a new route, finished the previous one, including all additional lines # with other AS paths. Output that last route. &walz_out_route(0,$as_seen); $as_seen = $new_as; %ass = (); # delete all memory of the previous route $current_route = $new_route; $ass{$new_as}{$new_upstream} = 1; # store AS and upstream in key and keys of hash # print "\n\ngot line $line"; # print "New route line: $new_route from AS $new_as upstream: $new_upstream\n"; } elsif ($line =~ /^.*?\s{10,}.*?\s+(\d+\.\d+\.\d+\.\d+).*\s{2,}0\s(.*)\s(\d+)\s(i|e|\?)$/ ) { # a line without a route, but a different AS path for the last seen route # distinctive mark: quite a bit of white space in the first sector # print "got line $line"; # print "Possible new AS path for route: $current_route from AS $3, upstream $2\n"; my $new_as = $3; my (@path_list) = split (' ',$2); my $next_as = 0; while ($next_as = pop @path_list) { if ($next_as != $new_as) { # print "new origin AS: $new_as , found upstream $next_as\n"; last; } } # print "storing upstream $next_as for origin AS $new_as\n"; $ass{$new_as}{$next_as} = 1; # store AS in key of hash } else { # de-comment this for debugging # print "ERROR: can't parse line $line"; } # print @output; } # foreach $line &walz_out_route(1,$as_seen); # that damn loop+1 problem } # if ($out_lines >= 2) else { unless ($sm_out) { print "No routes seen from this AS. We saw:\n"; my $line; foreach $line (@output) { print $line; } } } unless ($sm_out) { print "\n\n----------end of routes for AS $query_list -----------\n\n"; } } exit 0; ############################## sub walz_out_route { my ($final,$as_seen) = @_; # 0 = ongoing block, 1 = final route in block # matters only if $summarize_routes = 1 # $as_seen is likely AS originating the route # print the route, the origin AS(s) and all upstreams seen # if we use 'sh ip bgp regexp ^.*_ASN_.*$ we will never have more than one AS # per block # inconsistent AS's (more than one) can show up with "sh ip bgp x.x.x.x/prefix long" though, # which is what this code was really originally written for my $num = keys %ass; # how many AS's if ($num) { if ($current_route =~ /^.*\/\d+$/ ) { # we have CIDR notation - do nothing } else { # print "Cisco insanity (classfull route) seen, will rewrite: $current_route\n"; # one of those insane classful routes Cisco routers put out, despite "ip classless" if ($current_route =~ /^(\d+)\..*$/ ) { my $fo = $1 ; # first octet if ( ($fo < 127) && ( $current_route =~ /^\d+\.0\.0\.0$/ ) ) { $current_route .= "/8"; } elsif ( ($fo < 192) && ( $current_route =~ /^\d+\.\d+\.0\.0$/ ) ) { $current_route .= "/16"; } elsif ( ($fo < 224) && ( $current_route =~ /^\d+\.\d+\.\d+\.0$/ ) ) { $current_route .= "/24"; } else { print "ERROR: could not determine class of route $current_route\n"; } } } if ($summarize_routes) { if ($final) { my $fname = rand(1024*1024); my $i; my @lines; open(CIDR,"|$cidr_convert >/tmp/cidr-$fname"); foreach $i (@route_block) { print CIDR $i."\n"; } close CIDR; open (RESULT,"/tmp/cidr-$fname"); @lines = ; close RESULT; unlink "/tmp/cidr-$fname"; foreach $i (@lines) { chomp $i; if (! $sm_out) { print "$i\tfrom/through AS $as_seen: (summarized route)\n"; } else { if ($transit_routes) { print "$i\t550 NO ACCESS for $i - origin AS $as_seen is banned or transits banned AS - Spammers must die.\n"; } else { print "$i\t550 NO ACCESS for $i - banned AS $as_seen originates this network - Spammers must die.\n"; } } } } else { # not final yet push @route_block , $current_route; } # if ($final) } else { # don't summarize if (! $sm_out) { print "$current_route\tfrom AS:"; } else { if ($transit_routes) { print "$current_route\t550 NO ACCESS for $current_route - origin AS $as_seen is banned or transits banned AS. Origin AS:"; } else { print "$current_route\t550 NO ACCESS for $current_route - banned AS $as_seen originates this network. Origin AS:"; } } my $a; my $b; my $print_line; foreach $a (keys %ass) { # the list of origin AS's (typically only one) if ($sm_out) { $print_line .= " $a, upstreams:"; } else { $print_line .= " $a (upstreams:"; } foreach $b (keys %{$ass{$a}} ) { # the list of upstreams for that AS - typically more than one if ($b eq "0") { # if this is 0, we accidentially caught the 'Weight' column with the # regexp: we (the router we are using) are the upstream } else { $print_line .= " $b"; } } if ($sm_out) { $print_line .= ", "; } else { $print_line .= "), "; } } if ($sm_out) { print "$print_line - Spammers must die.\n"; } else { print "$print_line\n"; } } # if ($summarize_routes) } # if ($num) } # sub walz_out_route