AMPR-Rip44
view release on metacpan or search on metacpan
# Local gateway addresses (whose routes are skipped)
my %my_addresses;
# Allowed route destination networks
my $net_44_regexp = '^44\.';
# We do not accept routes less specific than /15
my $minimum_prefix_len = 15;
# tcp window to set
my $tcp_window = 840;
# time (in seconds) to use routes which are no longer advertised
# - this is set to a large value, so that if the rip advertisements
# from mirrorshades stop, the network won't go down right away.
my $route_ttl = 7*24*60*60;
my %current_routes;
my $me = 'rip44d';
my $VERSION = '1.1';
# help and version texts
$Getopt::Std::STANDARD_HELP_VERSION = 1;
sub HELP_MESSAGE()
{
my($fh) = @_;
print $fh "Usage:\n"
. " $me [-v] [-d] [-i <tunnelif>] [-a <localaddrs>] [-p <password>]\n"
. "Options:\n"
. " -v increase verbosity slightly to print error messages on stderr\n"
. " -d increase verbosity greatly (debug mode)\n"
. " -i <tunnelinterface>\n"
. " use the specified tunnel interface, defaults to tunl0\n"
. " -a <comma-separated-ip-list>\n"
. " ignore routes pointing to these (local) gateways\n"
. " (list contains system's local IP addresses by default)\n"
. " -p <password>\n"
. " use RIPv2 password 'authentication', defaults to none\n"
;
}
sub VERSION_MESSAGE()
{
my($fh) = @_;
print $fh "$me version $VERSION\n";
}
# Figure out local interface IP addresses so that routes to them can be ignored
sub fill_local_ifs()
{
my $s = `$ifconfig -a`;
while ($s =~ s/inet addr:(\d+\.\d+\.\d+\.\d+)//) {
warn "found local address: $1\n" if ($verbose);
$my_addresses{$1} = 1;
}
}
# Convert a netmask (in integer form) to the corresponding prefix length,
# and validate it too. This is a bit ugly, optimizations are welcome.
sub mask2prefix($)
{
my($mask) = @_; # integer
# convert to a string of 1's and 0's, like this (/25):
# 11111111111111111111111110000000
my($bits) = unpack("B32", pack('N', $mask));
# There should be a continuous row of 1's in the
# beginning, and a continuous row of 0's in the end.
# Regexp is our hammer, again.
return -1 if ($bits !~ /^(1*)(0*)$/);
# The amount of 1's in the beginning is the prefix length.
return length($1);
}
# delete a route from the kernel's table
sub route_delete($)
{
my($rkey) = @_;
# This is ugly and slow - we fork /sbin/ip twice for every route change.
# Should talk to the netlink device instead, but this is easier to
# do right now, and good enough for this little routing table.
my($out, $cmd);
$cmd = "LANG=C $routebin route del $rkey";
$out = `$cmd 2>&1`;
if ($?) {
if ($verbose > 1 || $out !~ /No such process/) {
warn "route del failed: '$cmd': $out\n";
}
}
}
# expire old routes
sub expire_routes()
{
warn "expiring old routes\n" if ($verbose);
my $exp_t = time() - $route_ttl;
my $now = time();
foreach my $rkey (keys %current_routes) {
if ($current_routes{$rkey}->{'t'} < $exp_t) {
# expire route
warn "route $rkey has expired, deleting\n" if ($verbose);
route_delete($rkey);
delete $current_routes{$rkey};
} elsif ($current_routes{$rkey}->{'t'} > $now) {
# clock has jumped backwards, the time is in
# the future - set 't' to $now so that the route
# will be expired eventually
$current_routes{$rkey}->{'t'} = $now;
}
}
}
&& $current_routes{$rkey}->{'nh'} eq $nexthop
&& $current_routes{$rkey}->{'rtag'} eq $rtag) {
# ok, current route is fine
warn "route $rkey is installed and current\n" if ($verbose > 1);
$current_routes{$rkey}->{'t'} = time();
return;
}
warn "route $rkey updated: via $nexthop rtag $rtag\n" if ($verbose > 1);
$current_routes{$rkey} = {
'nh' => $nexthop,
'rtag' => $rtag,
't' => time()
};
# now go and update the routing table
route_delete($rkey);
my($out, $cmd);
$cmd = "LANG=C $routebin route add $rkey via $nexthop dev $tunnel_if window $tcp_window onlink";
$out = `$cmd 2>&1\n`;
if ($?) {
warn "route add failed: '$cmd': $out\n";
}
}
# process a RIPv2 password authentication entry
sub process_rip_auth_entry($)
{
my($entry) = @_;
my $e_af = unpack('n', substr($entry, 0, 2));
if ($e_af != 0xFFFF) {
warn "RIPv2 first message does not contain auth password: ignoring\n" if ($verbose);
return 0;
}
my $e_type = unpack('n', substr($entry, 2, 2));
if ($e_type != RIP_AUTH_PASSWD) {
warn "ignoring unsupported rip auth type $e_type\n" if ($verbose);
return 0;
}
my $e_passwd = substr($entry, 4, 16);
$e_passwd =~ s/\0*$//; # it's null-padded in the end
if (!defined $rip_passwd) {
warn "RIPv2 packet contains password $e_passwd but we require none\n" if ($verbose);
return 0;
}
if ($e_passwd ne $rip_passwd) {
warn "RIPv2 invalid password $e_passwd\n" if ($verbose);
return 0;
}
return 1;
}
# validate a route entry, make sure we can rather safely
# insert it in the routing table
sub validate_route($$$$$)
{
my($e_net_i, $e_net_s, $e_netmask, $e_netmask_s, $e_nexthop_s) = @_;
# netmask is correct and not too wide
my $prefix_len = mask2prefix($e_netmask);
if ($prefix_len < 0) {
warn "invalid netmask: $e_netmask_s\n" if ($verbose);
return (0, 'invalid netmask');
}
if ($prefix_len < $minimum_prefix_len) {
warn "$e_net_s/$e_netmask_s => $e_nexthop_s blocked, prefix too short\n";
return (0, 'prefix length too short');
}
# the network-netmask pair makes sense: network & netmask == network
if (($e_net_i & $e_netmask) != $e_net_i) {
#print "e_net '$e_net_i' e_netmask '$e_netmask' ANDs to " . ($e_net_i & $e_netmask) . "\n";
warn "$e_net_s/$e_netmask_s => $e_nexthop_s blocked, subnet-netmask pair does not make sense\n" if ($verbose);
return (0, 'invalid subnet-netmask pair');
}
# network is in 44/8
if ($e_net_s !~ /$net_44_regexp/) {
warn "$e_net_s/$e_netmask_s => $e_nexthop_s blocked, non-amprnet address\n" if ($verbose);
return (0, 'net not in 44/8');
}
# nexthop address is not in 44/8
if ($e_nexthop_s =~ /$net_44_regexp/) {
warn "$e_net_s/$e_netmask_s => $e_nexthop_s blocked, nexthop is within amprnet\n" if ($verbose);
return (0, 'nexthop is in 44/8');
}
# nexthop address does not point to self
if (defined $my_addresses{$e_nexthop_s}) {
warn "$e_net_s/$e_netmask_s => $e_nexthop_s blocked, local gw\n" if ($verbose);
return (0, 'local gw');
}
return (1, 'ok');
}
# process a RIPv2 route entry
sub process_rip_route_entry($)
{
my($entry) = @_;
my $e_af = unpack('n', substr($entry, 0, 2));
my $e_rtag = unpack('n', substr($entry, 2, 2));
if ($e_af == 0xFFFF) {
process_rip_auth_entry($entry);
return -1;
}
if ($e_af != AF_INET) {
warn "$me: RIPv2 entry has unsupported AF $e_af\n";
return 0;
}
my $e_net = substr($entry, 4, 4);
my $e_net_i = unpack('N', $e_net);
my $e_netmask = substr($entry, 8, 4);
my $e_netmask_i = unpack('N', $e_netmask);
my $e_nexthop = substr($entry, 12, 4);
my $e_metric = unpack('N', substr($entry, 16, 4));
my $e_net_s = inet_ntoa($e_net);
my $e_netmask_s = inet_ntoa($e_netmask);
my $e_nexthop_s = inet_ntoa($e_nexthop);
# Validate the route
my($result, $reason) = validate_route($e_net_i, $e_net_s, $e_netmask_i, $e_netmask_s, $e_nexthop_s);
if (!$result) {
warn "entry ignored ($reason): af $e_af rtag $e_rtag $e_net_s/$e_netmask_s via $e_nexthop_s metric $e_metric\n" if ($verbose);
return 0;
}
warn "entry: af $e_af rtag $e_rtag $e_net_s/$e_netmask_s via $e_nexthop_s metric $e_metric\n" if ($verbose > 1);
# Ok, we have a valid route, consider adding it in the kernel's routing table
consider_route($e_net_s, $e_netmask_s, $e_nexthop_s, $e_rtag);
return 1;
}
# process a RIP message
sub process_msg($$$)
{
my($addr_s, $perr_port, $msg) = @_;
# validate packet's length
if (length($msg) < RIP_HDR_LEN + RIP_ENTRY_LEN) {
warn "$me: ignored too short packet from $addr_s: " . length($msg) . "\n";
return -1;
}
if (length($msg) > RIP_HDR_LEN + RIP_ENTRY_LEN*25) {
warn "$me: ignored too long packet from $addr_s: " . length($msg) . "\n";
return -1;
}
# packet's length must be divisible by the length of an entry
if ((length($msg) - RIP_HDR_LEN) % RIP_ENTRY_LEN != 0) {
warn "$me: ignored invalid length packet from $addr_s: " . length($msg) . "\n";
return -1;
}
# validate RIP packet header
my $hdr = substr($msg, 0, 4);
my $entries = substr($msg, 4);
my($rip_command, $rip_version, $zero1, $zero2) = unpack('C*', $hdr);
if ($rip_command != RIP_CMD_RESPONSE) {
warn "$me: ignored non-response RIP packet from $addr_s\n";
return -1;
}
if ($rip_version != 2) {
warn "$me: ignored RIP version $rip_version packet from $addr_s (only accept v2)\n";
return -1;
}
if ($zero1 != 0 || $zero2 != 0) {
warn "$me: ignored RIP packet from $addr_s: zero bytes are not zero in header\n";
return -1;
}
my $init_msg = 0;
# if password auth is required, require it!
if (defined $rip_passwd) {
return -1 if (!process_rip_auth_entry(substr($entries, 0, RIP_ENTRY_LEN)));
$init_msg += RIP_ENTRY_LEN;
}
# Ok, process the actual route entries
my $routes = 0;
for (my $i = $init_msg; $i < length($entries); $i += RIP_ENTRY_LEN) {
my $entry = substr($entries, $i, RIP_ENTRY_LEN);
my $n = process_rip_route_entry($entry);
return -1 if ($n < 0);
$routes += $n;
}
return $routes;
}
#
####### main #############################################
#
# command line parsing
my %opts;
getopts('i:p:a:vd', \%opts);
if (defined $opts{'i'}) {
$tunnel_if = $opts{'i'};
}
if (defined $opts{'p'}) {
$rip_passwd = $opts{'p'};
}
if ($opts{'v'} && !$verbose) {
$verbose = 1;
}
if ($opts{'d'}) {
$verbose = 2;
}
if ($opts{'a'}) {
foreach my $a (split(',', $opts{'a'})) {
$my_addresses{$a} = 1;
( run in 0.926 second using v1.01-cache-2.11-cpan-140bd7fdf52 )