#!/usr/bin/env perl
#
#  blackhole6: A tool find IPv6 blackholes
#
#  Syntax: blackhole6 DESTINATION [HEADERSIZE [PROTOCOL [PORT]]]
#
#  Copyright (C) 2011-2024 Fernando Gont <fgont@si6networks.com>
#
#  Programmed by Fernando Gont for SI6 Networks <https://www.si6networks.com>
#
#  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 3 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, see <http://www.gnu.org/licenses/>.
#
#  Please send any bug reports to Fernando Gont <fgont@si6networks.com>

use Socket();
use constant EXIT_SUCCESS => 0;
use constant EXIT_FAILURE => 1;

$SI6TOOLKIT="SI6 Networks IPv6 Toolkit 2.2";

$total=0;
$response=0;
$timeout=0;

sub usage{
	print "usage: blackhole6 DESTINATION [EHTYPE[EHSIZE]] [PROTOCOL [PORT]]]\n";
}


# Function GetASN()
#
# Obtains the autonomous system number (ASN) for a given IPv6 address
#
sub GetASN{
	@revname=`addr6 -a $_[0] -r`;

	if( ($? >> 8) != 0){
		return(-1);
	}
	else{
		chomp($revname[0]);
		$queryname= $revname[0] . ".origin6.asn.cymru.com.";
		@reverse=`host -t TXT $queryname`;

		if($reverse[0] =~ m/\"\s*((\d+)\s*)\s+|"/){
			return($2);
		}
		else{
			return(-1);
		}
	}
}

# Function GetORG()
#
# Obtains the organization corresponding to an autonomous system number (ASN)
#
sub GetORG{
	$querystring="host -t TXT AS".$_[0].".asn.cymru.com";
	@asinfo= `$querystring`;

	if($asinfo[0] =~ m/\"*.\|.*\|.*\|.*\|\s*(.*)\"/){
		return($1);
	}
	else{
		return(-1);
	}
}

# Remove all items from the array of IP addresses
undef @ipsfree;

print "$SI6TOOLKIT\n";
print "blackhole6: A tool to find IPv6 blackholes\n";

if($> != 0){
	print "Error: blackhole6 requires superuser privileges\n";
	exit(EXIT_FAILURE);
}

if($#ARGV < 0){
	print "Error: Must specify an IPv6 address\n";
	usage();
	exit(EXIT_FAILURE);
}


# Obtain the IPv6 addres corresponding to the specified domain name
# (This implicitly obtains the canonic address if an IPv6 address was specified)
my ( $err, @addrs ) = Socket::getaddrinfo( $ARGV[0], 0, { 'protocol' => Socket::IPPROTO_TCP, 'family' => Socket::AF_INET6 } );

if($err){
	die $err;
}

my ( $err, $dstaddr ) = Socket::getnameinfo( $addrs[0]->{addr}, Socket::NI_NUMERICHOST );
if ($err){
	die $err;
}

#$dstaddr= Socket::inet_ntop(Socket::AF_INET6, $addrs[0]->{addr});

$ehtype= "do";
$ehsize= "8";
$prototype="icmp";

if($#ARGV > 0){
	if($ARGV[1] =~ m/([a-zA-Z]+):?(\d+)/){
		$ehtype= lc($1);
		$ehsize= $2;
	}
	else{
		$ehtype= $ARGV[1];
	}

	if($#ARGV > 1){
		$prototype=	$ARGV[2];

		if($#ARGV > 2){
			$port=	$ARGV[3];
		}
	}
}

$protoopt="";
	
if($prototype eq "tcp"){
	$payloadhdr= 20;

	if($port eq ""){
		$port= 80;
	}
	
	$protoopt= "--tcp-flags S -a $port";
}
elsif($prototype eq "udp"){
	$payloadhdr=8;
	
	if($port eq ""){
		$port= 53;
	}
	
	$protoopt= "-a $port";
}
elsif($prototype eq "icmp" || $prototype eq "icmp6" || $prototype eq "icmpv6"){
	$prototype="icmp";
	$payloadhdr=8;
}
else{
	print "Error: Unknown payload type \"$prototype\".\n";
	exit(EXIT_FAILURE);
}

$ehprotoopt= $protoopt;

if($ehtype eq "fh" || $ehtype eq "frag"){
	if($ehsize < 8){
		print "Error: Fragment size should be larger than or equal to 8 bytes.\n";
		exit(EXIT_FAILURE);
	}
	elsif($ehsize > 1280){
		print "Error: Fragment size should be smaller than or equal to 1280 bytes.\n";
		exit(EXIT_FAILURE);
	}

	$payload= ($ehsize * 2) - $payloadhdr;
	
	if($payload > 0){
		$ehprotoopt= $ehprotoopt." -P $payload";
		$protoopt=$protoopt." -P ".($payload/2);		
	}
}

if($ehtype eq "fh" || $ehtype eq "frag"){
	$eh= "-y $ehsize";
}
elsif($ehtype eq "hbh"){
	$eh= "-H $ehsize";
}
elsif($ehtype eq "do"){
	$eh= "-u $ehsize";
}
elsif($ehtype eq "esp"){
	$eh= "-p esp";
}
elsif($ehtype eq "ah"){
	$eh= "-p ah";
}
else{
	print "Error: Unknown EH type";
	exit(EXIT_FAILURE);
}

print "Tracing $ARGV[0] ($dstaddr)...\n";

$maxhopsfree=0;
$maxhopsfreeip="";
@tcp=`path6 -d $dstaddr -p $prototype $protoopt --rate-limit 40pps`;

if(($? >> 8) != 0){
	print "blackhole6: Found issues when running path6 to the specified target";
	exit(EXIT_FAILURE);
}

foreach $line (@tcp){
	# Discard lines that do not contain a "probe" line
	if($line =~ m/^(?:\s+)?(\d+)\s+\((\S+)\)/){
		if($1 > $maxhopsfree){
			$maxhopsfree=$1;
			$maxhopsfreeip=$2;

			# We store the IPv6 addresses of all hops
			push(@ipsfree, $2);
		}
	}
}

$maxhopstrouble=0;
$maxhopstroubleip="";

# XXX: This is an ugly hack. should be removed. ESP and AH should possibly be handled as EHs, rather than probe types
if($ehtype eq "esp" || $ehtype eq "ah"){
	@tcp=`path6 -d $dstaddr $eh --rate-limit 40pps`;
}else{
	@tcp=`path6 -d $dstaddr -p $prototype $ehprotoopt $eh --rate-limit 40pps`;
}


if(($? >> 8) != 0){
	print "blackhole6: Found issues when running path6 to the specified target\n";
	exit(EXIT_FAILURE);
}

foreach $line (@tcp){
	# Discard lines that do not contain a "probe" line
	if($line =~ m/^(?:\s+)?(\d+)\s+\((\S+)\)/){
		if($1 > $maxhopstrouble){
			$maxhopstrouble=$1;
			$maxhopstroubleip= $2;
		}
	}
}

$dropip="";
$dropip2="";

for($i=0; $i< $#ipsfree; $i++){
	if($ipsfree[$i] eq $maxhopstroubleip){
		$dropip= $ipsfree[$i+1];
		$dropip2= $ipsfree[$i+2];
		last;
	}
}

$dstaddrasn= GetASN($dstaddr);

# $dstaddrasn holds the destination system
if($dstaddrasn == -1){
	$dstaddrasn= " Unknown";
}

elsif($dstaddrasn eq ""){
	$dstaddrasn= " Unknown";
}


$maxhopsfreeasn= GetASN($maxhopsfreeip);

if($maxhopsfreeasn == -1){
	$maxhopsfreeasn= " Unknown";
}

elsif($maxhopsfreeasn eq ""){
	$maxhopsfreeasn= " Unknown";
}

$maxhopstroubleasn= GetASN($maxhopstroubleip);

if($maxhopstroubleasn == -1){
	$maxhopstroubleasn= " Unknown";
}
elsif($maxhopstroubleasn eq ""){
	$maxhopstroubleasn= " Unknown";
}

$ehtype_uc= uc($ehtype);

if($dstaddrasn ne  " Unknown"){
	$dstaddrorg= GetORG($dstaddrasn);

	if($dstaddrorg == -1){
		$dstaddrorg= "Unknown organization";
	}
}
else{
	$dstaddrorg= "Unknown organization";
}

if($maxhopsfreeasn ne  " Unknown"){
	$maxhopsfreeorg= GetORG($maxhopsfreeasn);

	if($maxhopsfreeorg == -1){
		$maxhopsfreeorg= "Unknown organization";
	}
}
else{
	$maxhopsfreeorg= "Unknown organization";
}

if($maxhopstroubleasn ne  " Unknown"){
	$maxhopstroubleorg= GetORG($maxhopstroubleasn);

	if($maxhopstroubleorg == -1){
		$maxhopstroubleorg=  "Unknown organization";
	}
}
else{
	$maxhopstroubleorg=  "Unknown organization";
}

print "\nDst. IPv6 address: $dstaddr (AS$dstaddrasn - $dstaddrorg)\n";
print "Last node (no EHs): $maxhopsfreeip (AS$maxhopsfreeasn - $maxhopsfreeorg) ($maxhopsfree hop(s))\n";
print "Last node ($ehtype_uc $ehsize): $maxhopstroubleip (AS$maxhopstroubleasn - $maxhopstroubleorg) ($maxhopstrouble hop(s))\n";


if($maxhopsfreeip eq $dstaddr){
	if($maxhopstroubleip eq $dstaddr){
			print "Dropping node: No packet drops\n";
	}
	else{
		$dropasn= GetASN($dropip);
		if($dropasn == -1){
			$dropasn="";
		}

		$droporg= "";

		if($dropasn ne ""){
			$droporg= GetORG($dropasn);
			if($droporg == -1){
				$droporg= "";
			}
		}
		
		if($dropasn eq ""){
			$dropasn= " Unknown";
		}

		if($droporg eq ""){
			$droporg= "Unknown organization";
		}

		if($dropip2 ne ""){
			$dropasn2= GetASN($dropip2);
			if($dropasn2 == -1){
				$dropasn2="";
			}

			$droporg2= "";

			if($dropasn2 ne ""){
				$droporg2= GetORG($dropasn2);
				if($droporg2 == -1){
					$droporg2="";
				}
			}
		
			if($dropasn2 eq ""){
				$dropasn2= " Unknown";
			}

			if($droporg2 eq ""){
				$droporg2= "Unknown organization";
			}
		}

		if($dropip ne ""){
				if($dropasn2 eq "" || $dropasn2 eq "Unknown" || $dropasn eq $dropasn2){
					print "Dropping node: $dropip (AS$dropasn - $droporg)\n";
				}
				else{
					if($dropasn eq "" || $dropasn eq "Unknown"){
						print "Dropping node: $dropip (AS$dropasn2 - $droporg2)\n";					
					}
					else{
						print "Dropping node: $dropip (AS$dropasn - $droporg || AS$dropasn2 - $droporg2)\n";
					}
				}
		}
		else{
			print "Dropping node could not be determined (you may want to try again)\n";
		}
	}
}
else{
	print "Dropping nodes: Packets being dropped for both the no-EH and the EH case\n";
}

