| use strict; | use strict; | ||||
| use POSIX ":sys_wait_h"; | use POSIX ":sys_wait_h"; | ||||
| use List::Util qw(shuffle); | |||||
| use Data::Dumper; | use Data::Dumper; | ||||
| use warnings; | use warnings; | ||||
| $|=1; | |||||
| my %conf = ( | my %conf = ( | ||||
| avail => 2, | |||||
| avail => 10, | |||||
| spare => 2, | spare => 2, | ||||
| ipt_chain => "PREROUTING", | ipt_chain => "PREROUTING", | ||||
| ipt_rule => "-p tcp -m tcp --dport 777 -m statistic --mode random --probability %s -j REDIRECT --to-ports %s", | |||||
| ipt_rule => "-s %s -p tcp -m tcp --dport 777 -m statistic --mode random --probability %s -j REDIRECT --to-ports %s", | |||||
| ipt_table => 'nat', | ipt_table => 'nat', | ||||
| ipt_source4 => '193.110.95.0/24', | |||||
| ipt_source6 => '2001:1a8f:ffff::/48', | |||||
| socks_offset => 9000, | socks_offset => 9000, | ||||
| path => '/opt/tor', | path => '/opt/tor', | ||||
| dns_check => [ | dns_check => [ | ||||
| 'www.microsoft.com', | |||||
| 'www.google.com', | 'www.google.com', | ||||
| 'www.youtube.com', | |||||
| 'www.facebook.com', | |||||
| 'www.baidu.com', | |||||
| 'www.wikipedia.org', | |||||
| 'www.qq.com', | |||||
| 'www.taobao.com', | |||||
| 'www.tmall.com', | |||||
| 'www.yahoo.com', | |||||
| 'www.amazon.com', | |||||
| 'www.twitter.com', | |||||
| 'www.suho.com', | |||||
| ], | ], | ||||
| getip => 'http://www.spale.com/cgi-bin/ip.cgi', | |||||
| proto => { | proto => { | ||||
| ipv4 => 1, | ipv4 => 1, | ||||
| ipv6 => 1, | ipv6 => 1, | ||||
| }, | }, | ||||
| loopinterval => 60, | |||||
| loopinterval => 10, | |||||
| statefile => 'var/status.txt', | |||||
| ); | ); | ||||
| # states: startup, ready, problem, dying, killed | # states: startup, ready, problem, dying, killed | ||||
| sub run { | sub run { | ||||
| # check state / start new spares / etc.. | |||||
| check_processes(); | check_processes(); | ||||
| # update conntrack session usage per client | |||||
| ipt_conn(); | |||||
| # display state of all clients | |||||
| show_state(); | show_state(); | ||||
| # commit log | |||||
| log_commit(); | |||||
| # wait for next loop | |||||
| sleep($conf{loopinterval}); | sleep($conf{loopinterval}); | ||||
| } | } | ||||
| sub show_state { | sub show_state { | ||||
| my $s = ipt_conn(); | |||||
| printf("======================================\n"); | |||||
| printf("%3s %-7s %-5s %4s %4s\n", "id", "state", "mode", "ipv4", "ipv6"); | |||||
| log_add("=======================================================================================\n"); | |||||
| log_add("%3s %-7s %-5s %8s %8s %15s %35s\n", "id", "state", "mode", "ipt_ipv4", "ipt_ipv6", "ipv4", "ipv6"); | |||||
| log_add("---------------------------------------------------------------------------------------\n"); | |||||
| for(my $id=0; defined $var[$id]; $id++) { | for(my $id=0; defined $var[$id]; $id++) { | ||||
| printf("%3s %-7s %-5s %4s %4s\n", | |||||
| log_add("%3s %-7s %-5s %8s %8s %15s %35s\n", | |||||
| $id, | $id, | ||||
| $var[$id]{state}, | $var[$id]{state}, | ||||
| $var[$id]{mode}, | $var[$id]{mode}, | ||||
| exists $s->{$id}{ipv4} ? $s->{$id}{ipv4} : 0, | |||||
| exists $s->{$id}{ipv6} ? $s->{$id}{ipv6} : 0, | |||||
| $var[$id]{ipt}{ipv4}, | |||||
| $var[$id]{ipt}{ipv6}, | |||||
| $var[$id]{ipv4}, | |||||
| $var[$id]{ipv6}, | |||||
| ); | ); | ||||
| } | } | ||||
| printf("======================================\n"); | |||||
| log_add("=======================================================================================\n"); | |||||
| } | } | ||||
| sub check_processes { | sub check_processes { | ||||
| my $inuse=0; | my $inuse=0; | ||||
| my @spares; | my @spares; | ||||
| # check for dead tor clients (killed or crashed) | |||||
| for(my $id=0; defined $var[$id]; $id++) { | for(my $id=0; defined $var[$id]; $id++) { | ||||
| if ( $var[$id]{pid} eq waitpid($var[$id]{pid}, WNOHANG) ) { | |||||
| print "child $var[$id]{pid} died. resetting instance $id\n"; | |||||
| ipt_del($id); | |||||
| $var[$id]{pid}=undef; | |||||
| $var[$id]{state}='killed'; | |||||
| $var[$id]{mode}='none'; | |||||
| if ( defined $var[$id]{pid} && $var[$id]{pid} eq waitpid($var[$id]{pid}, WNOHANG) ) { | |||||
| log_add("child $var[$id]{pid} died. resetting instance $id\n"); | |||||
| tor_dead($id); | |||||
| } | } | ||||
| } | } | ||||
| # check for conntrack on dying processes | |||||
| for(my $id=0; defined $var[$id]; $id++) { | for(my $id=0; defined $var[$id]; $id++) { | ||||
| if ( $var[$id]{state} =~ /^(startup|ready|problem)$/ ) { | |||||
| check_dns($id); | |||||
| if ( $var[$id]{state} eq 'dying' && | |||||
| $var[$id]{ipt}{ipv4} eq 0 && | |||||
| $var[$id]{ipt}{ipv6} eq 0 ) { | |||||
| log_add("client $id was dying and has no conntrack anymore, killing\n"); | |||||
| tor_kill($id); | |||||
| } | } | ||||
| } | } | ||||
| # check dns query for startup, read and problem clients | |||||
| check_dns(); | |||||
| # count/list available live and available spare clients | |||||
| for(my $id=0; defined $var[$id]; $id++) { | for(my $id=0; defined $var[$id]; $id++) { | ||||
| if ( $var[$id]{state} eq "ready" && $var[$id]{mode} eq 'live' ) { | |||||
| if ( $var[$id]{state} =~ /^(ready|problem)$/ && $var[$id]{mode} eq 'live' ) { | |||||
| $inuse++; | $inuse++; | ||||
| } | } | ||||
| } | } | ||||
| print "found " . scalar(@spares) . " spare clients and $inuse live clients\n"; | |||||
| log_add("found " . scalar(@spares) . " spare clients and $inuse live clients\n"); | |||||
| # check if available live processes are sufficient | |||||
| # and get from spare if missing | |||||
| if ( $inuse < $conf{avail} ) { | if ( $inuse < $conf{avail} ) { | ||||
| print "missing live processes. looking for spares.\n"; | |||||
| log_add("missing live processes. looking for spares.\n"); | |||||
| for(my $x=0; $x<($conf{avail}-$inuse); $x++) { | for(my $x=0; $x<($conf{avail}-$inuse); $x++) { | ||||
| my $new = pop @spares; | |||||
| if ( defined $new ) { | |||||
| if ( $var[$new]{state} eq 'ready' ) { | |||||
| print "moving spare to prod.\n"; | |||||
| tor_prod($new); | |||||
| } | |||||
| else { | |||||
| push @spares, $new; | |||||
| if ( defined $spares[0] ) { | |||||
| if ( $var[$spares[0]]{state} eq 'ready' ) { | |||||
| log_add("moving spare to prod.\n"); | |||||
| tor_prod(shift @spares); | |||||
| $inuse++; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| # if spares arent enough, start some more | |||||
| # especially useful for quick availability after initial startup | |||||
| for(my $x=0; $x<($conf{avail}-$inuse)/2; $x++) { tor_new(); } | |||||
| # check if there are enough spares | |||||
| if ( scalar(@spares) < $conf{spare} ) { | if ( scalar(@spares) < $conf{spare} ) { | ||||
| print "missing spare processes, starting new ones.\n"; | |||||
| log_add("missing spare processes, starting new ones.\n"); | |||||
| for(my $x=0; $x < ($conf{spare}-scalar(@spares)); $x++) { | for(my $x=0; $x < ($conf{spare}-scalar(@spares)); $x++) { | ||||
| tor_new(); | tor_new(); | ||||
| } | } | ||||
| } | } | ||||
| # update public ipv4/ipv6 for tunnels | |||||
| for(my $id=0; defined $var[$id]; $id++) { | |||||
| if ( $var[$id]{state} =~ /^(ready|problem)$/ ) { | |||||
| getip($id,4) if $var[$id]{ipv4} eq '-'; | |||||
| getip($id,6) if $var[$id]{ipv6} eq '-'; | |||||
| } | |||||
| } | |||||
| } | |||||
| sub getip { | |||||
| my($id,$proto) = @_; | |||||
| open(CURL,"timeout 10 curl -f -s -$proto --socks5 localhost:$var[$id]{port} $conf{getip} |"); | |||||
| while(<CURL>) { | |||||
| if ( /^([0-9a-f\:\.]+)$/ ) { | |||||
| $var[$id]{"ipv".$proto} = $1; | |||||
| } | |||||
| } | |||||
| close(CURL); | |||||
| } | } | ||||
| sub check_dns { | sub check_dns { | ||||
| my($id) = @_; | |||||
| my @pids; | |||||
| my $ok = 0; | |||||
| foreach my $hostname ( @{$conf{dns_check}} ) { | |||||
| print "Checking DNS ($hostname) for $id "; | |||||
| system("timeout 5 tor-resolve $hostname 127.0.0.1:$var[$id]{port} >/dev/null 2>&1"); | |||||
| if ( $? ) { | |||||
| print "ERROR\n"; | |||||
| log_add("DNS Check Start "); | |||||
| for(my $id=0; defined $var[$id]; $id++) { | |||||
| # ignore clients not in bad state | |||||
| if ( $var[$id]{state} !~ /^(startup|ready|problem)$/ ) { | |||||
| next; | |||||
| } | |||||
| my $pid = fork(); | |||||
| if ( $pid ) { | |||||
| push @pids, { id => $id, pid => $pid }; | |||||
| log_add("."); | |||||
| } | } | ||||
| else { | else { | ||||
| print "OK\n"; | |||||
| $ok++; | |||||
| my $ok = 0; | |||||
| foreach my $hostname ( shuffle @{$conf{dns_check}} ) { | |||||
| system("timeout 5 tor-resolve $hostname 127.0.0.1:$var[$id]{port} >/dev/null 2>&1"); | |||||
| if ( ! $? ) { $ok=1; last; } | |||||
| } | |||||
| if ( $ok ) { exit 0; } | |||||
| else { exit 1; } | |||||
| } | } | ||||
| } | } | ||||
| if ( $var[$id]{state} eq 'startup' ) { | |||||
| if ( $ok ) { | |||||
| $var[$id]{state} = 'ready'; | |||||
| log_add("\n"); | |||||
| log_add("DNS Check results: "); | |||||
| foreach my $x ( @pids ) { | |||||
| my $id = $x->{id}; | |||||
| my $pid = $x->{pid}; | |||||
| waitpid($pid, 0); | |||||
| my $ok = $? ? 0 : 1; | |||||
| log_add($ok ? "+" : "-"); | |||||
| if ( $var[$id]{state} eq 'startup' ) { | |||||
| if ( $ok ) { | |||||
| $var[$id]{state} = 'ready'; | |||||
| } | |||||
| else { | |||||
| $var[$id]{state} = 'problem'; | |||||
| } | |||||
| } | } | ||||
| else { | |||||
| elsif ( $var[$id]{state} eq 'ready' && !$ok ) { | |||||
| $var[$id]{state} = 'problem'; | $var[$id]{state} = 'problem'; | ||||
| } | } | ||||
| } | |||||
| elsif ( $var[$id]{state} eq 'ready' && !$ok ) { | |||||
| $var[$id]{state} = 'problem'; | |||||
| } | |||||
| elsif ( $var[$id]{state} eq 'problem' ) { | |||||
| if ( $ok ) { | |||||
| $var[$id]{state} = 'ready'; | |||||
| } | |||||
| else { | |||||
| $var[$id]{state} = 'dying'; | |||||
| ipt_del($id); | |||||
| elsif ( $var[$id]{state} eq 'problem' ) { | |||||
| if ( $ok ) { | |||||
| $var[$id]{state} = 'ready'; | |||||
| } | |||||
| else { | |||||
| tor_dying($id); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| log_add("\n"); | |||||
| } | } | ||||
| sub tor_new { | sub tor_new { | ||||
| my $next; | my $next; | ||||
| print "initiating a new tor client\n"; | |||||
| # try to recycle a position | # try to recycle a position | ||||
| for(my $id=0; defined $var[$id]; $id++) { | for(my $id=0; defined $var[$id]; $id++) { | ||||
| if ( $var[$id]{state} eq 'killed' ) { | if ( $var[$id]{state} eq 'killed' ) { | ||||
| print "recovering id $id\n"; | |||||
| $next = $id; | $next = $id; | ||||
| last; | last; | ||||
| } | } | ||||
| # alternatively, create a new position | # alternatively, create a new position | ||||
| if ( !defined $next ) { | if ( !defined $next ) { | ||||
| $next = @var; | $next = @var; | ||||
| print "new id $next\n"; | |||||
| } | } | ||||
| $var[$next] = { | $var[$next] = { | ||||
| mode => "spare", | mode => "spare", | ||||
| port => $conf{socks_offset} + $next, | port => $conf{socks_offset} + $next, | ||||
| pid => undef, | pid => undef, | ||||
| ipt => { ipv4 => 0, ipv6 => 0 }, | |||||
| ipv4 => '-', | |||||
| ipv6 => '-', | |||||
| }; | }; | ||||
| print "creating configuration file and dependencies\n"; | |||||
| # create a config file and the required files and directories | # create a config file and the required files and directories | ||||
| my $datadir = "$conf{path}/var/lib/$next"; | my $datadir = "$conf{path}/var/lib/$next"; | ||||
| my %files = ( | my %files = ( | ||||
| EOF | EOF | ||||
| close(C); | close(C); | ||||
| print "forking...\n"; | |||||
| my $pid = fork(); | my $pid = fork(); | ||||
| if ( !defined $pid ) { | if ( !defined $pid ) { | ||||
| die "Failed to fork, something has gone badly wrong!\n"; | die "Failed to fork, something has gone badly wrong!\n"; | ||||
| } | } | ||||
| elsif ( $pid ) { | elsif ( $pid ) { | ||||
| print "tor client pid $pid\n"; | |||||
| log_add("new tor client pid $pid\n"); | |||||
| $var[$next]{pid} = $pid; | $var[$next]{pid} = $pid; | ||||
| } | } | ||||
| else { | else { | ||||
| # move process from spare to prod | # move process from spare to prod | ||||
| $var[$id]{mode} = 'live'; | $var[$id]{mode} = 'live'; | ||||
| ipt_add($var[$id]{port}); | ipt_add($var[$id]{port}); | ||||
| } | } | ||||
| sub tor_dying { | |||||
| my($id) = @_; | |||||
| $var[$id]{state} = 'dying'; | |||||
| $var[$id]{mode} = 'none'; | |||||
| ipt_del($id); | |||||
| } | |||||
| sub tor_dead { | |||||
| my($id) = @_; | |||||
| ipt_del($var[$id]{port}); | |||||
| $var[$id]{pid}=undef; | |||||
| $var[$id]{state}='killed'; | |||||
| $var[$id]{mode}='none'; | |||||
| } | |||||
| sub tor_kill { | |||||
| my($id) = @_; | |||||
| kill 'KILL', $var[$id]{pid}; | |||||
| } | |||||
| sub ipt_add { | sub ipt_add { | ||||
| my($port) = @_; | my($port) = @_; | ||||
| system("iptables -t $conf{ipt_table} -A $conf{ipt_chain} " . sprintf($conf{ipt_rule}, "1.0", $port)) if $conf{proto}{ipv4}; | |||||
| system("ip6tables -t $conf{ipt_table} -A $conf{ipt_chain} " . sprintf($conf{ipt_rule}, "1.0", $port)) if $conf{proto}{ipv6}; | |||||
| system("iptables -t $conf{ipt_table} -A $conf{ipt_chain} " . sprintf($conf{ipt_rule}, $conf{ipt_source4}, "1.0", $port)) if $conf{proto}{ipv4}; | |||||
| system("ip6tables -t $conf{ipt_table} -A $conf{ipt_chain} " . sprintf($conf{ipt_rule}, $conf{ipt_source6}, "1.0", $port)) if $conf{proto}{ipv6}; | |||||
| ipt_recalc(); | ipt_recalc(); | ||||
| } | } | ||||
| my $prob = 1 / ($nums - $num); | my $prob = 1 / ($nums - $num); | ||||
| system("iptables -t $conf{ipt_table} -R $conf{ipt_chain} $rulenum " . sprintf($conf{ipt_rule}, $prob, $port)); | |||||
| system("iptables -t $conf{ipt_table} -R $conf{ipt_chain} $rulenum " . sprintf($conf{ipt_rule}, $conf{ipt_source4}, $prob, $port)); | |||||
| } | } | ||||
| } | } | ||||
| my $prob = 1 / ($nums - $num); | my $prob = 1 / ($nums - $num); | ||||
| system("ip6tables -t $conf{ipt_table} -R $conf{ipt_chain} $rulenum " . sprintf($conf{ipt_rule}, $prob, $port)); | |||||
| system("ip6tables -t $conf{ipt_table} -R $conf{ipt_chain} $rulenum " . sprintf($conf{ipt_rule}, $conf{ipt_source6}, $prob, $port)); | |||||
| } | } | ||||
| } | } | ||||
| close(IPT); | close(IPT); | ||||
| } | } | ||||
| return \%s; | |||||
| for(my $id=0; defined $var[$id]; $id++) { | |||||
| $var[$id]{ipt}{ipv4} = exists $s{$id}{ipv4} ? $s{$id}{ipv4} : 0; | |||||
| $var[$id]{ipt}{ipv6} = exists $s{$id}{ipv6} ? $s{$id}{ipv6} : 0; | |||||
| } | |||||
| } | |||||
| sub log_add { | |||||
| open(F,'>>'.$conf{path}.'/'.$conf{statefile}.'.tmp'); | |||||
| printf F @_; | |||||
| close(F); | |||||
| } | |||||
| sub log_commit { | |||||
| rename $conf{path}.'/'.$conf{statefile}.'.tmp', $conf{path}.'/'.$conf{statefile}; | |||||
| } | } |