#!/usr/local/bin/perl -T # # Compute stats by protocol from a tcpdump. # # Copyright (c) 1997 Renaud Waldura # Wed Feb 26 14:15:42 1997 # use Socket; use Fcntl; use FileHandle; # the configuration file $CONFIG_FILE = '/home/renaud/etc/net-stats.conf'; $PROGNAME = 'Net-Stats'; # program name $VERSION = '3.5'; # program version @X11 = (6000 .. 6063); # X11 ports %SERVICENAME = (); # service names cache @SERVICENAME{@X11} = ('x11') x @X11; %HOSTNAME = (); # host names cache $HOSTNAME{'127.0.0.1'} = 'localhost'; undef $ENV{'PATH'}; undef $ENV{'CDPATH'}; # security $| = 1; # debug # read in configuration %CONF = (); read_config(\%CONF, $CONFIG_FILE) or die "Error in configuration file $CONFIG_FILE"; init(\%CONF); # initialize program # install termination handler my @signals = ('HUP', 'INT', 'TERM'); @SIG{@signals} = (\&sig_handler) x @signals; main(); # infinite loop done(); # cleanup sub main { my %packets = ( 'icmp_cnt' => 0, 'icmp' => {}, 'arp_cnt' => 0, 'arp' => {}, 'tcp_cnt' => 0, 'tcp' => {}, 'unknown_cnt' => 0, 'unknown' => [], ); my %data = ( 'start_time' => time(), 'tcp_bytes' => 0, 'second_peak' => 0, 'packets' => \%packets ); my $ip = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'; # IP address pattern my($new_second, $old_second, $second_bytes, $n) = (0) x 4; while () # this loop is terminated by signal only { $new_second = $1 if (s/^\d\d:\d\d:(\d\d).\d+ //); if ($new_second != $old_second) { $data{'second_peak'} = $second_bytes if ($second_bytes > $data{'second_peak'}); $second_bytes = 0; } $old_second = $new_second; if (($host) = /^arp [\w-]+ ($ip) /o) # ARP { count_arp($packets{'arp'}, $host); $packets{'arp_cnt'}++; } elsif (($src_host, $dest_host) = /^($ip) > ($ip): icmp: /o) # ICMP { count_icmp($packets{'icmp'}, $src_host, $dest_host); $packets{'icmp_cnt'}++; } elsif (($src_host, $src_port, $dest_host, $dest_port, $details) = /^($ip)\.(\d+) > ($ip)\.(\d+): (.+)$/o) # TCP/UDP { my($pkt_bytes) = ($details =~ /\((\d+)\)/); count_tcp($packets{'tcp'}, $src_host, $src_port, $dest_host, $dest_port, $pkt_bytes); $data{'tcp_bytes'} += $pkt_bytes; $second_bytes += $pkt_bytes; $packets{'tcp_cnt'}++; } else # unknown { push(@{$packets{'unknown'}}, $_); $packets{'unknown_cnt'}++; } if (++$n % $CONF{'purge_tables'} == 0) # purge tables { $n = 0; purge_tables(\%packets, \%data, @CONF{'tcp_max', 'icmp_max'}); } if ($CONF{'_needs_update'}) # re-read configuration { my %new_config = (); if (read_config(\%new_config, $CONFIG_FILE)) { done(); # cleanup config-dependent objects init(\%new_config); %CONF = %new_config; } else { warn "Error in configuration file $CONFIG_FILE; re-using old values"; } } next unless ($client = accept(CLIENT, SERVER)); # client connected print "Connection from ", host_info($client), " at ", scalar localtime(), "\n"; # make sure data is delivered before the socket is closed linger_on_close(\*CLIENT); if (allowed_host($client)) # client "authenticated" { print_data(\*CLIENT, \%packets, \%data); } else { access_denied(\*CLIENT, $client); # sorry dude } close CLIENT; # close client socket } } sub sig_handler { my($signal) = @_; print "Caught SIG$signal\n"; exit unless ($signal eq 'HUP'); # executes END{} $CONF{'_needs_update'} = 1; # SIGHUP received } sub init { my($config) = @_; open_socket($config->{'port'}); open_tcpdump($config->{'tcpdump'}, $config->{'tcpdump_opts'}); } sub open_socket { my($port) = @_; my $proto = getprotobyname('tcp'); # TCP socket socket(SERVER, PF_INET, SOCK_STREAM, $proto) or die "Cannot create socket: $!"; # so that accept() in main loop above won't block fcntl(SERVER, F_SETFL, O_NONBLOCK) or die "Cannot fcntl socket: $!"; # the old server socket might stick around for a while even after # closing it; make sure that, on restart (SIGHUP received), we'll # succeed in binding the new server socket to the same port setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1) or warn "Cannot set socket option SO_REUSEADDR: $!"; # bind socket to the server port on all interfaces bind(SERVER, sockaddr_in($port, INADDR_ANY)) or die "Cannot bind socket to port $port: $!"; # connections queue listen(SERVER, SOMAXCONN) or die "Cannot listen to socket: $!"; } sub linger_on_close { my($socket) = @_; my $linger = pack('ii', 1, 120); # BSD struct linger setsockopt(CLIENT, SOL_SOCKET, SO_LINGER, $linger) or warn "Cannot set socket option SO_LINGER: $!"; } sub open_tcpdump { my($cmd) = join(' ', @_); open(TCPDUMP, "$cmd |") or die "Cannot execute '$cmd': $!"; } sub done { close SERVER or warn "Cannot close socket: $!"; # close server socket close TCPDUMP or warn "Cannot close tcpdump: $!"; # terminate tcpdump } END { done(); # cleanup } sub purge_tables { my($packets, $data, $tcp_max, $icmp_max) = @_; purge_table($packets->{'tcp'}, $data->{'tcp_bytes'}) if (keys %{$packets->{'tcp'}} > $tcp_max); purge_table($packets->{'icmp'}, $packets->{'icmp_cnt'}) if (keys %{$packets->{'icmp'}} > $icmp_max); } sub purge_table { my($h, $total) = @_; foreach (keys %$h) # remove entries < 1% { delete $h->{$_} if (percent($h->{$_}, $total) < 1); } } sub count_arp { my($h, $host) = @_; $h->{$host}++; } sub count_icmp { my($h, $src, $dest, $data) = @_; $h->{"$src > $dest"}++; } sub count_tcp { my($h, $src_host, $src_port, $dest_host, $dest_port, $bytes) = @_; my($server_port); # the server port is the smallest port number unless in the X11 range. # (this is not always true, but it's enough for now) if ($src_port >= $X11[0] && $src_port <= $X11[$#X11]) { $server_port = $src_port; } elsif ($dest_port >= $X11[0] && $dest_port <= $X11[$#X11]) { $server_port = $dest_port; } else { $server_port = ($src_port < $dest_port) ? $src_port : $dest_port; } # increment counts $h->{$server_port} += $bytes; } sub allowed_host { my($host) = @_; my($hname, $haddr) = host_info($host); my @haddr = split(/\./, $haddr); foreach (@{$CONF{'allowed_hosts'}}) { my($addr, $mask) = @$_; my @addr = split(/\./, $addr); my @mask = split(/\./, $mask); return 1 if (mask_eq(\@haddr, \@addr, \@mask)); } return 0; } sub host_info # returns host name and address (in printable format) from a struct # as returned by accept() { my($host) = @_; my($port, $addr) = sockaddr_in($host); my $hostaddr = inet_ntoa($addr); my $hostname = _gethostbyaddr($hostaddr); return ($hostname, $hostaddr); } sub host_name # returns host name, or address if unable to resolve { my($hostaddr) = @_; my $hostname = _gethostbyaddr($hostaddr); return ($hostname) ? $hostname : $hostaddr; } sub service_name # returns service name, or port number if unable to resolve { my($servport) = @_; my $servname = _getservbyport($servport); return ($servname) ? $servname : $servport; } sub _gethostbyaddr # caches resolved host names; attempts resolution only once { my($addr, $type) = @_; return $HOSTNAME{$addr} if (defined $HOSTNAME{$addr}); my $hostname = gethostbyaddr(inet_aton($addr), $type || AF_INET); return $HOSTNAME{$addr} = ($hostname || ''); } sub _getservbyport # caches resolved service names; attempts resolution only once { my($port, $proto) = @_; return $SERVICENAME{$port} if (defined $SERVICENAME{$port}); my $servname = getservbyport($port, $proto || 'tcp'); return $SERVICENAME{$port} = ($servname || ''); } sub mask_eq # returns true if ($haddr & $mask) == $addr { my($haddr, $addr, $mask) = @_; my $eq = 1; for $i (0..3) { # force numerical, otherwise Perl does an ASCII and :( my($a, $b, $c) = map {$_ + 0} ($haddr->[$i], $addr->[$i], $mask->[$i]); $eq &&= ($a & $c) == $b; } return $eq; } sub access_denied { my($f, $host) = @_; my($hname, $haddr) = host_info($host); my $now = localtime(); print $f < Forbidden

Forbidden

Host $haddr ($hname) is not allowed access to this service.

EOT } sub print_data { my($f, $pkts, $data) = @_; my $start = $data->{'start_time'}; my $now = time(); my $s1 = localtime($start); my $s2 = localtime($now); my $total_pkts = 0; map {$total_pkts += $pkts->{$_}} grep(/_cnt$/, keys %$pkts); my $unknown_pkts = $pkts->{'unknown_cnt'}; my $total_known_pkts = $total_pkts - $unknown_pkts; my $pps = round($total_pkts / ($now - $start)); print $f < Network Stats

Network Stats

From $s1 to $s2 (now)

$total_pkts total packets
Average $pps packets/sec

EOT my $bytes = $data->{'tcp_bytes'}; my $peak = $data->{'second_peak'}; print_bandwidth($f, $start, $now, $peak, $bytes); print $f "

By Protocol

\n"; print_tcp($f, $pkts->{'tcp'}, $pkts->{'tcp_cnt'}, $total_known_pkts, $bytes); print_icmp($f, $pkts->{'icmp'}, $pkts->{'icmp_cnt'}, $total_known_pkts); print_arp($f, $pkts->{'arp'}, $pkts->{'arp_cnt'}, $total_known_pkts); print_unknown($f, $pkts->{'unknown'}, $unknown_pkts); print $f <
$PROGNAME $VERSION - $s2
EOT } sub print_bandwidth { my($f, $start, $now, $peak, $bytes) = @_; my $total_bandwidth = $CONF{'bandwidth'}; $peak *= 8; my $pp = percent($peak, $total_bandwidth); $peak = bytes2str($peak); my $average = ($bytes * 8) / ($now - $start); my $ap = percent($average, $total_bandwidth); $average = bytes2str($average); $bytes = bytes2str($bytes); print $f <Approximate Bandwidth

Total $bytes bytes transferred with TCP/UDP

Bandwidth bits/sec %
Peak $peak $pp
Average $average $ap

EOT } sub print_tcp { my($f, $h, $n, $total, $total_bytes) = @_; my $p = percent($n, $total); my $ps = bytes2str(int($total_bytes / $n)); print $f <TCP/UDP

$n packets, $p% of total known packets
Average packet size: $ps bytes

Service Bytes % EOT my $gbm = $CONF{'graphbar_max'}; for $service (sort {$h->{$b} <=> $h->{$a}} keys %$h) { my $serv_name = service_name($service); my $bytes = $h->{$service}; my $p = percent($bytes, $total_bytes); my $bs = bytes2str($bytes); my $bar = "[]" x int($bytes * $gbm / $total_bytes); print $f < $serv_name $bs $p $bar EOT } print $f <

EOT } sub print_icmp { my($f, $h, $n, $total) = @_; my $p = percent($n, $total); print $f <ICMP

$n packets, $p% of total known packets

Source Dest Count EOT foreach (sort {$h->{$b} <=> $h->{$a}} keys %$h) { my($src_host, $dest_host) = map {host_name($_)} split / > /; print $f < $src_host $dest_host $h->{$_} EOT } print $f <

EOT } sub print_arp { my($f, $h, $n, $total) = @_; my $p = percent($n, $total); print $f <ARP

$n packets, $p% of total known packets

Host Count EOT foreach (sort {$h->{$b} <=> $h->{$a}} keys %$h) { my $host_name = host_name($_); print $f < $host_name $h->{$_} EOT } print $f <

EOT } sub print_unknown { my($f, $a, $n) = @_; print $f <Unknown Packets

$n packets

EOT print $f map {"
$_\n"} @$a; print $f <

EOT } sub bytes2str { my($bytes) = @_; my($kilo, $mega, $giga, $tera); if (($kilo = $bytes / 1024) < 1) { return $bytes; } elsif (($mega = $kilo / 1024) < 1) { return round($kilo) . 'K'; } elsif (($giga = $mega / 1024) < 1) { return round($mega) . 'M'; } elsif (($tera = $giga / 1024) < 1) { return round($giga) . 'G'; } else { return round($tera) . 'T'; } } sub percent { my($frac, $total) = @_; return round(100 * $frac / $total); } sub round { my($x) = @_; return int(10 * $x + 0.5) / 10; } sub read_config { my($config, $file) = @_; my $ip = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'; # IP address pattern open(CONF, $file) or die "Cannot open configuration file $file: $!"; while () { next if (/^#/ || /^\s*$/); my($cmd, $arg, @args) = split; $cmd = lc $cmd; if ($cmd eq 'port' || $cmd eq 'bandwidth' || $cmd eq 'tcp_max' || $cmd eq 'icmp_max' || $cmd eq 'purge_tables' || $cmd eq 'graphbar_max') { unless ($arg =~ /^(\d+)$/) { warn "Configuration directive '$cmd' expects number (got $arg)"; return 0; } $config->{$cmd} = $1; } elsif ($cmd eq 'tcpdump' ) { unless ($arg =~ /^([\.\w\/-]+)$/ && -e $arg) { warn "Configuration directive '$cmd' expects valid pathname (got $arg)"; return 0; } $config->{$cmd} = $1; } elsif ($cmd eq 'tcpdump_opts') { my $options = join(' ', $arg, @args); unless ($options =~ /^([-\w ]+)$/) { warn "Configuration directive '$cmd' expects valid program options (got $options)"; return 0; } $config->{$cmd} = $1; } elsif ($cmd eq 'allow_hosts') { my $arg2 = pop @args; unless ($arg =~ /^$ip$/o && $arg2 =~ /^$ip$/o) { warn "Configuration value '$arg/$arg2' is not a valid network number/mask"; return 0; } push(@{$config->{'allowed_hosts'}}, [$arg, $arg2]); } else { warn "Invalid configuration directive '$cmd'"; return 0; } } close CONF; $config->{'_update'} = 0; # configuration updated return 1; }