#!/usr/bin/perl
#
# tor-tables.pl - get list of tor servers, build an iptables table based on
# them
#
# IANAP (i am not a programmer)  this script is quick and dirty.  i wanted the
# ability to block inbound traffic from tor so this was the motivation.
# 
# in a nutshell, we get the cached-directory file, build a list of the routers
# and their ips, try to determine which ones are exit nodes and build iptables
# rules based on that.  exit node determination is simply "does a router have
# "accept: \*:" in it's config".  so yes this is a pretty broad definition but 
# it's the best i could come up with.  if nothing else i wanted the behavior
# not to block transit-only nodes (but this can be done with -a)
#
# *** NOTE ***
#
# this script doesn't really block anything.  it builds a user defined table
# called 'tor'.  to actually block the traffic, you need something like
#
# iptables -A INPUT -j tor
# iptables -A FORWARD -j tor
#
# if you add these as your first rules, you will traverse the tor table
# for every packet first, then jump back to your normal table(s)
#
##############################################################################

$IPTABLES_CMD     = "iptables -A tor -s";
$IPTABLES_LOG     = "-m state --state INVALID,NEW -m limit --limit 20/min \-j LOG --log-prefix \"tor/bad_incoming_packet \""; 
$IPTABLES_DROP    = "-m state --state INVALID,NEW -j DROP";
$IPTABLES_FLUSH   = "iptables -F tor 2>/dev/null";
$IPTABLES_CREATE  = "iptables -N tor 2>/dev/null";
$IPTABLES_DESTROY = "iptables -X tor 2>/dev/null";

# main tor directory servers
############################
@tordir = ('86.59.5.130', '18.244.0.114', '18.244.0.188:9031');

# no user config needed below here
##################################

use Getopt::Std;
use LWP::UserAgent;
$Getopt::Std::STANDARD_HELP_VERSION = 1;
$main::VERSION = "0.0";

%routers = ();

getopts(av);

# randomize what tor dir server we hit
######################################
srand;
@new = ();
while (@tordir)
{
	push(@new, splice(@tordir, rand @tordir, 1));
}
@tordir = @new;

# retrieve the 'cached-directory' file
######################################
foreach $tor (@tordir)
{
	if ($opt_v) { print STDERR "Trying tor directory server: $tor\n"; }
	$ua = LWP::UserAgent->new( timeout => 45);
	$URL = 'http://'.$tor;
	$ua->agent('Mozilla/5.0 (_; _)');
	$req = new HTTP::Request GET => $URL;
	$res = $ua->request($req);
	if (!$res->is_success)
	{
		if ($opt_v) { print STDERR "Retrieval from $tor failed.\n"; } 
		next;
	}	

	@output = (split /\n/, $res->content);
	last;
}

if (!@output) { print STDERR "Couldn't get Tor server directory !\n"; exit 1; }


# kind of messy/braindead here, but iptables likes things to happen in
# a certain order
######################################################################
`$IPTABLES_FLUSH`;
`$IPTABLES_CREATE`;

if ($opt_v) { print STDERR "Got cached-directory, processing..."; }

# find the routers names, ip addresses, fill up the hash
########################################################
foreach $line (@output)
{
	chomp $line;

	if ($line =~ /^$/) { next ; }
	
	if ($line =~ /^router (\S+){1} ([\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}) /)
	{
		$router_name = $1, $router_ip = $2;
		$routers{ $router_name } = $router_ip;
		$start = 1;
 	}

	if ($line =~ /^-----END SIGNATURE-----$/)
	{
		$start = 0;
	}

	if ($start eq "1")
	{
		push @{$router_name}, $line;
	}	 

}

if ($opt_v)
{
	print STDERR "Done.\n";
	print STDERR "Adding iptables rules.\n";
}

# firewall ALL tor servers
##########################
if ($opt_a)
{
	$count = 0;
	foreach $router_name (keys %routers)
	{
		$count++;		
		`$IPTABLES_CMD $routers{ $router_name } $IPTABLES_LOG`;
		`$IPTABLES_CMD $routers{ $router_name } $IPTABLES_DROP`;
		
		if ($opt_v) { print STDERR "\r$count"; }
		
	}
	
	if ($opt_v) { print STDERR " rules added.\n"; }
	exit 0;
}

# only exit nodes (default)
###########################
$count = 0;
foreach $router_name (keys %routers)
{
	foreach $line (@{$router_name})
	{
		if ($line =~ /^accept \*:/)
		{
			$count++; 			
			`$IPTABLES_CMD $routers{ $router_name } $IPTABLES_LOG`;
			`$IPTABLES_CMD $routers{ $router_name } $IPTABLES_DROP`;
			if ($opt_v) { print STDERR "\r$count"; }
			last;
		}
	}
}
if ($opt_v) { print STDERR " rules added.\n"; }
exit 0;

# Getopt:: help/version stuff
#############################
sub main::HELP_MESSAGE
{

    print << 'EndHelp';

$ tor-tables.pl [-av]

  -a	block ALL tor servers (exit + transit)
  -v	verbose

EndHelp
exit 0;
}
