#!/usr/local/bin/perl

# anycast.pl Copyright (c) 2007061501 Jeremy Kister
# http://jeremy.kister.net./code/anycast.pl
# Released under Perl's Artistic License.
# 
# Inject /32 routes into BGP for the purpose of anycast routing for our
# services.  When the service is broken, the route must go away.
# Currently supports UDP/DNS, TCP/DNS, and UDP/RADIUS.
#
# Recommended to run under djb's daemontools.  modify %our, %peer, %table.

use strict;
use Net::BGP::Process;
use Net::BGP::Peer;
use Net::BGP::Update;
use Net::BGP::ASPath;
my %modules = ('Net::DNS' => (eval{require Net::DNS}) ? 1 : 0,
               'Authen::Radius' => (eval{require Authen::Radius}) ? 1 : 0, );
 
my $DEBUG=1;

my %our   = (addr => '10.115.0.126', as => 65523);
my %peer  = (addr => '10.115.5.52', as => 65534);
my %table = ('10.115.0.9'      => { service  => 'DNS',
                                    qtype    => 'NS',
                                    host     => '.',
                                    recurse  => 1,
                                    protocol => 'TCP,UDP',
                                    expect   => '^[A-M]\.ROOT-SERVERS\.NET$', }, # a regex
             '10.115.0.40'     => { service  => 'DNS',
                                    qtype    => 'A',
                                    host     => 'ns1.example.net',
                                    recurse  => 0,
                                    protocol => 'UDP',
                                    expect   => '^10\.115\.0\.9$', }, # a regex
             '10.115.0.16'     => { service  => 'RADIUS',
                                    secret   => 'secret',
                                    username => 'username',
                                    password => 'password', },
            ); # 30 servers max


my @servers = keys %table;
foreach my $server (@servers){
	# taint the table - make sure all seems okay
	if($table{$server}{service} eq 'DNS'){
		die "Net::DNS module not found for service DNS\n" unless($modules{'Net::DNS'});
		unless($table{$server}{qtype} && $table{$server}{host} && $table{$server}{recurse} &&
		       $table{$server}{protocol} && $table{$server}{expect}){
			die "Configuration failed for server $server - check %table\n";
		}
		unless($table{$server}{protocol} =~ /^(?:TCP|UDP|TCP,UDP|UDP,TCP)$/){
			die "unknown protocol for server $server - check %table\n";
		}
	}elsif($table{$server}{service} eq 'RADIUS'){
		die "Authen::Radius module not found for service RADIUS\n" unless($modules{'Authen::Radius'});
		unless($table{$server}{secret} && $table{$server}{username} && $table{$server}{password}){
			die "Configuration failed for server $server - check %table\n";
		}
	}else{
		die "$server using unknown service: $table{$server}{service}\n";
	}
}

my $origin = ($our{as} == $peer{as}) ? 0 : 1; # 0 = igp, 1 = egp, 2 = incomplete
my $aspath = new Net::BGP::ASPath($our{as});
my %broken = map { $_ => 2 } @servers; # assume all broken, prove good.
my $timer = int(30/@servers); # check hosts equally during the 30 second period
my $loops_to_switch = (@servers * 2); # we check and confirm each server on start
my ($bgp,$peer,%retry,$run_loops);
my $id = 0;

connect_to_peer(); # infinite loop

sub connect_to_peer {
	warn "Connecting to Peer [$peer{addr}].\n";
	$bgp = new Net::BGP::Process();
	$peer = new Net::BGP::Peer( Start => 1,
	                            ThisID => $our{addr},
	                            ThisAS => $our{as},
	                            PeerID => $peer{addr},
	                            PeerAS => $peer{as},
	                          );
	$bgp->add_peer($peer);
	$peer->add_timer(\&run_on_timer, 1); # check each asap
	$run_loops = 1;
	$bgp->event_loop();
}


sub run_on_timer {
	if($run_loops == $loops_to_switch){
		# we've checked each host twice and have injected the routes.
		# stop checking asap, now check once per 30 seconds
		$peer->remove_timer(\&run_on_timer);
		$peer->add_timer(\&run_on_timer, $timer);
		# last time counter is incremented so we dont get here again
		$run_loops++; 
	}elsif($run_loops < $loops_to_switch){
		$run_loops++;
	}

	# make sure our session is established
	unless($peer->is_established){
		connect_to_peer(); # keep the infinite loop going
	}

	my $code;
	eval {
		local $SIG{ALRM} = sub { die "TIMEOUT.\n"; };
		alarm(1); # a full second is quite a lot of time
		
		if($table{$servers[$id]}{service} eq 'DNS'){
			# set the resolver to the host we're checking, set recurse appropriately
			my $res = Net::DNS::Resolver->new(nameservers => [$servers[$id]], recurse => $table{$servers[$id]}{recurse});
	
			foreach my $protocol (split /,/, $table{$servers[$id]}{protocol}){
				my $usevc = ($protocol eq 'TCP') ? 1 : 0;
				$res->usevc($usevc); # vc = 1 for tcp, 0 for udp
	
				my $query = $res->query($table{$servers[$id]}{host}, $table{$servers[$id]}{qtype});
				if($query){
					foreach my $rr (grep { $_->type eq $table{$servers[$id]}{qtype} } $query->answer){
						my $object = ($table{$servers[$id]}{qtype} eq 'NS') ? $rr->nsdname : $rr->address;
						unless($object =~ /$table{$servers[$id]}{expect}/i){
							warn "ERROR: [$object] does not match expected regex.\n" if($DEBUG);
							$code=0;
							last; # only one has to fail to know it's broken
						}
					}
				}else{
					warn "query failed: ", $res->errorstring, "\n" if($DEBUG);
					$code=0;
					last; # no use checking other protocols; we're calling it dead
				}
				last if(defined($code));
			}
		}elsif($table{$servers[$id]}{service} eq 'RADIUS'){
			my $radius = new Authen::Radius(Host => $servers[$id], Secret => $table{$servers[$id]}{secret});
			my $result = $radius->check_pwd($table{$servers[$id]}{username}, $table{$servers[$id]}{password});
		
			$code = ($result == 1) ? 1 : 0;
		}
		$code=1 unless(defined($code));

		alarm(0);
	};
	alarm(0);
	if($@){ $code=0; }; # if the eval fails but sets $code=1 -- needed??
	if($broken{$servers[$id]}){
		if($code){
			if($retry{$servers[$id]}){
				# server is working - inject into BGP ASAP
				delete $retry{$servers[$id]};
				delete $broken{$servers[$id]};
				warn "SENDING BGP UPDATE -> inject $servers[$id]/32\n";
	
				my $update = new Net::BGP::Update ( NLRI => [ "$servers[$id]/32" ],
				                                    AsPath => $aspath,
				                                    LocalPref => 100,
				                                    MED       => 0,
				                                    NextHop   => $our{addr},
				                                    Origin    => $origin,
				                                  );
				$peer->update($update);
			}else{
				warn "successful query to $servers[$id]; will retry to confirm.\n";
				$retry{$servers[$id]} = 1; # retry in 30 seconds to make sure its still ok
			}
		}
	}else{
		if($code){
			if($retry{$servers[$id]}){
				warn "successful query to $servers[$id] on retry.\n";
				delete $retry{$servers[$id]};
			}
		}else{
			warn "$servers[$id] down.\n" if($DEBUG);
			if($retry{$servers[$id]}){
				# server was dead before, is dead now - withdraw from BGP ASAP
				delete $retry{$servers[$id]};
				$broken{$servers[$id]} = 1;
				warn "SENDING BGP UPDATE -> withdraw $servers[$id]\n";

				my $update = new Net::BGP::Update ( Withdraw => [ "$servers[$id]/32" ], );
				$peer->update($update);
			}else{
				warn "failed query to $servers[$id]; will retry to confirm.\n";
				$retry{$servers[$id]} = 1; # we will retry once more, in 30 seconds
			}
		}
	}
	$id++; # next server in the loop to check
	$id = 0 if($id == @servers); # if we're out of the loop, start over (arrays 0-based)
}
