#!/usr/bin/perl -w
#
#* OSPF linkstate database dumper
#
#  Copyright (C) 2007      Maurice Massar
#
#  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 2 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, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

use strict;
use Net::SNMP;
use POSIX qw(strftime);
use Socket;
use Data::Dumper;
use vars qw($config $debug);

$debug = 0;
require '/etc/ospfmap.conf';

my $oid = {
	ospfRouterId          => ".1.3.6.1.2.1.14.1.1.0",
	ospfSpfRuns           => ".1.3.6.1.2.1.14.2.1.4",
	ospfAreaLsaCount      => ".1.3.6.1.2.1.14.2.1.7",
	ospfAreaLsaCksumSum   => ".1.3.6.1.2.1.14.2.1.8",
	ospfLsdbSequence      => ".1.3.6.1.2.1.14.4.1.5",
	ospfLsdbAdvertisement => ".1.3.6.1.2.1.14.4.1.8",
};

sub mask2len($) {
	my $mask = shift;
	my $len = 32;
	
	$len-- while ($mask + 2**(32-$len) < 2**32);
	
	return $len;
}

sub newsnmpsession($) {
	my ($config) = @_;
	
	my ($snmp, $error) = Net::SNMP->session(
		-hostname      => $config->{router},
		( (defined($config->{community}) and length($config->{community}) > 0) ? (
			-version   => 1,
			-community => $config->{community},
		) : (
			-version      => 3,
			-username     => $config->{username},
			-authprotocol => $config->{authproto},
			-privprotocol => $config->{privproto},
			-authpassword => $config->{authpw},
			-privpassword => $config->{privpw},
		))
	);
	
	unless (defined($snmp)) {
		die "failed to create SNMP session: $error";
	}
	
	return $snmp;
}

sub getospflsdb($$) {
	my ($snmp, $area) = @_;
	my $lsdb;
	
	my $result = $snmp->get_entries( -columns => [ $oid->{ospfLsdbAdvertisement}, $oid->{ospfLsdbSequence} ]);
	unless (defined($result)) { die "failed to get SNMP data: ".$snmp->error; }
	map {
		my $data = $result->{$_};
		$data =~ s/^0x//;
		push @$lsdb, pack "H*", $data;
	} sort grep {Net::SNMP::oid_base_match($oid->{ospfLsdbAdvertisement} . "." . $area, $_)} keys %$result;
	
	return $lsdb;
}


sub getlsdb($) {
	my $config = shift;
	my $snmp = newsnmpsession($config);
	my $lsdb = getospflsdb($snmp, $config->{area});
	$snmp->close();
	return $lsdb;
}

sub parselsdb($) {
	my $lsdb = shift;
	
	my $networks;
	my $topo;
	my $type;
	my $transit2network;
	
	foreach my $data (@$lsdb) {
		my ($ls_age, $ls_options, $ls_type, $ls_id, $ls_adv_router, $ls_seq, $ls_cksum, $ls_len, $ls_data)
			= unpack("nCCNNNnna*", $data);
		printf "type:%s, link:%s, router:%s\n", $ls_type, map {inet_ntoa(inet_aton($_))} $ls_id, $ls_adv_router if $debug;
		if ($ls_type == 1) {
			my ($rl_flags, $rl_links);
			($rl_flags, $rl_links, $ls_data) = unpack("Cxna*", $ls_data);
			printf "routerLink: flags:%04x, #links:%s\n", $rl_flags, $rl_links if $debug;
			while (length($ls_data) > 0) {
				my ($rll_id, $rll_data, $rll_type, $rll_ntos, $rll_metric);
				($rll_id, $rll_data, $rll_type, $rll_ntos, $rll_metric, $ls_data) = unpack("NNCCna*", $ls_data);
				printf "link-id: %s, link-data: %s, type: %d, #TOS: %d, metric: %d\n",
					inet_ntoa(inet_aton($rll_id)), inet_ntoa(inet_aton($rll_data)),
					$rll_type, $rll_ntos, $rll_metric if $debug;
				foreach my $i (0 .. $rll_ntos - 1) {
					(undef, undef, $ls_data) = unpack("Cxna*", $ls_data);
				}
				if ($rll_type == 1) {
					# p2p link to other ospf router
					my $peer    = inet_ntoa(inet_aton($rll_id));
					my $link    = inet_ntoa(inet_aton($rll_data));
					my $router  = inet_ntoa(inet_aton($ls_adv_router));
					push @{$topo->{$router}{$peer}}, $link;
					$type->{$peer} = "p2p";
				} elsif ($rll_type == 2) {
					# link to transit network
					my $network = inet_ntoa(inet_aton($rll_id));
					my $link    = inet_ntoa(inet_aton($rll_data));
					my $router  = inet_ntoa(inet_aton($ls_adv_router));
					push @{$topo->{$router}{$network}}, $link;
					$type->{$network} = "transit";
				} elsif ($rll_type == 3) {
					my $network = inet_ntoa(inet_aton($rll_id));
					my $masklen = mask2len($rll_data);
					my $router  = inet_ntoa(inet_aton($ls_adv_router));
					push @{$networks->{$network}{$masklen}}, ["stub", $router];
				}
			}
		} elsif ($ls_type == 2) {
			my ($nl_mask, @nl_routers) = unpack("NN*", $ls_data);
			my $network = inet_ntoa(inet_aton($ls_id & $nl_mask));
			my $masklen = mask2len($nl_mask);
			printf "networkLink: netmask:%s\n", inet_ntoa(inet_aton($nl_mask)) if $debug;
			print map {sprintf "router: %s\n", inet_ntoa(inet_aton($_))} @nl_routers if $debug;
			push @{$networks->{$network}{$masklen}}, ["transit", map {inet_ntoa(inet_aton($_))} sort @nl_routers];
			$transit2network->{inet_ntoa(inet_aton($ls_id))} = $network."/".$masklen;
		} elsif ($ls_type == 3 or $ls_type == 4) {
			my ($sa_mask, @sa_metrics) = unpack("NN*", $ls_data);
			my $network = inet_ntoa(inet_aton($ls_id));
			my $masklen = mask2len($sa_mask);
			$masklen = 32 if ($ls_type == 4);
			my $router  = inet_ntoa(inet_aton($ls_adv_router));
			push @{$networks->{$network}{$masklen}}, ["summary", $router];
		} elsif ($ls_type == 5) {
			my ($asl_mask);
			($asl_mask, $ls_data) = unpack("Na*", $ls_data);
			printf "as-external: netmask:%s\n", inet_ntoa(inet_aton($asl_mask)) if $debug;
			my $network = inet_ntoa(inet_aton($ls_id));
			my $masklen = mask2len($asl_mask);
			while (length($ls_data) > 0) {
				my ($asl_flags, $asl_metric, $asl_fwd, $asl_tag);
				($asl_flags, $asl_metric, $asl_fwd, $asl_tag, $ls_data)
					= unpack("Ca3NNa*", $ls_data);
				printf "flags: %02x, metric: %d, forward: %s, tag: %d\n",
					$asl_flags, unpack("N", "\0".$asl_metric),
					inet_ntoa(inet_aton($asl_fwd)), $asl_tag if $debug;
				my $router = inet_ntoa(inet_aton(($asl_fwd != 0) ? $asl_fwd : $ls_adv_router));
				push @{$networks->{$network}{$masklen}}, ["external", $router];
			}
		} else {
			printf "data: %s\n", unpack("H*", $ls_data) if $debug;
		}
		print "\n" if $debug;
	}
	
	return ($networks, $topo, $type, $transit2network);
}

sub lookup(@) {
	return
		join ", ",
		map {s/\Q.uni-kl.de\E$//; $_}
		map {s/\Q.rhrk.uni-kl.de\E$//; $_}
		map {scalar gethostbyaddr(inet_aton($_), AF_INET) || $_}
		@_;
}

sub ip2num($) {
	my $ip = shift;
	return unpack('N', inet_aton($ip));
}
sub num2ip($) {
	my $num = shift;
	return inet_ntoa(pack('N', $num));
}
sub getdefgw($$) {
	my ($network, $masklen) = @_;
	
	return num2ip(ip2num($network) + 2**(32-$masklen) - (($masklen < 31) ? 2 : 1));
}

sub printospf($$$$) {
	my ($networks, $topo, $type, $transit2network) = @_;
	
	foreach my $router (sort {unpack('N', inet_aton($a)) <=> unpack('N', inet_aton($b))} keys %{$topo}) {
		printf "%s:\n", lookup($router);
		foreach my $peer (sort {unpack('N', inet_aton($a)) <=> unpack('N', inet_aton($b))} keys %{$topo->{$router}}) {
			foreach my $link (sort {unpack('N', inet_aton($a)) <=> unpack('N', inet_aton($b))} @{$topo->{$router}{$peer}}) {
				printf "  %-22s -> %s\n", lookup($link), ($type->{$peer} eq "p2p" ? lookup($peer) : defined($transit2network->{$peer}) ? $transit2network->{$peer} : "unknown transit network");
			}
		}
		print "\n";
	}
	
	foreach my $network (sort {unpack('N', inet_aton($a)) <=> unpack('N', inet_aton($b))} keys %{$networks}) {
		foreach my $masklen (sort {$a <=> $b} keys %{$networks->{$network}}) {
			my $networklen = "$network/$masklen";
			my $defgw = lookup(getdefgw($network, $masklen));
			printf "%-20s   %s  %s\n", $networklen, join(" ", map {sprintf "[%-10s %s]", shift(@$_).": ", lookup(@$_)} @{$networks->{$network}{$masklen}}), "($defgw)";
		}
	}
}

foreach my $cfg (@{$config}) {
	my $lsdb = getlsdb($cfg);
	my ($networks, $topo, $type, $transit2network) = parselsdb($lsdb);
	
	my $title = sprintf "OSPF Area %s on %s", $cfg->{area}, $cfg->{router};
	printf "%s\n%s\n\n", $title, "="x(length $title);
	
	printospf($networks, $topo, $type, $transit2network);
	print "\n\n\n";
}

exit 0;
1;
