#!/usr/bin/perl -wT # # Author: Jefferson Ogata # Date: 1998/11/01 # Version: 0.4 # # Please feel free to use or redistribute this program if you find it useful. # If you have suggestions, or even better, bits of new code, send them to me # and I will add them when I have time. The current version of this script # can always be found at the URL: # # http://pobox.com/~ogata/webtools/plog.txt # # Parse ipmon output into a coherent form. This program only handles the # lines regarding filter actions. It does not parse nat and state lines. # # Present lines from ipmon to this program on standard input. One way I # often use is: # grep ' b ' logfile | plog # since a ' b ' sequence indicates a blocked packet. # # TODO: # - Handle output from ipmon -v. # - Handle timestamps from other locales. Anyone with a timestamp problem # please email me the format of your timestamps. # # CHANGES: # 1999/05/03: # - Now accepts hostnames in the source and destination address fields, as # well as port names in the port fields. This allows the people who are # using ipmon -n to still use plog. Note that if you are logging # hostnames, you are vulnerable to forgery of DNS information, modified # DNS information, and your log files will be larger also. If you are # using this program you can have it look up the names for you (still # vulnerable to forgery) and keep your addresses all in numeric format, # so that packets from the same source will always show the same source # address regardless of what's up with DNS. Nevertheless, some people # wanted this, so here it is. # - Added S and n flags to %acts hash. Thanks to Stephen J. Roznowski # . # - Stopped reporting host IPs twice when numeric output was requested. # Thanks, yet again, to Stephen J. Roznowski . # - Number of minor tweaks that might speed it up a bit, and some comments. # - Put the script back up on the web site. I moved the site and forgot to # move the tool. # 1999/02/04: # - Changed log line parser to accept fully-qualified name in the logging # host field. Thanks to Stephen J. Roznowski . # 1999/01/22: # - Changed high port strategy to use 65536 for unknown high ports so that # they are sorted last. # 1999/01/21: # - Moved icmp parsing to output loop. # - Added parsing of icmp codes, and more types. # - Changed packet sort routine to sort by port number rather than service # name. # 1999/01/20: # - Fixed problem matching ipmon log lines. Sometimes they have "/ipmon" in # them, sometimes just "ipmon". # - Added numeric parse option to turn off hostname lookups. # - Moved summary to usage() sub. use strict; use Socket; select STDOUT ; $| = 1 ; my %hosts; my $me = $0; $me =~ s/^([^\/]*\/)*//; my $numeric = 0; # Under IPv4 port numbers are unsigned shorts. The value below is higher # than the maximum value of an unsigned port, and is used in place of # high port numbers that don't correspond to known services. This makes # high ports get sorted behind all others. my $highPort = 0x10000; # Map of log codes for various actions. Not all of these can occur, but # I've included everything in print_ipflog() from ipmon.c. my %acts = ( 'p' => 'pass', 'P' => 'pass', 'b' => 'block', 'B' => 'block', 'L' => 'log', 'S' => 'short', 'n' => 'nomatch', ); while (defined ($_ = shift)) { if (s/^-//) { $numeric += s/n//g; &usage (0) if (s/[h\?]//g); &usage (1) if (length ($_)); next; } &usage (1); } while () { chomp; # For ipmon output that came through syslog, we'll have an asctime # timestamp, hostname, "ipmon"[process id]: prefixed to the line. For # output that was written directly to a file by ipmon, we'll have a date # prefix as dd/mm/yyyy (no y2k problem here!). Both formats then have a # packet timestamp and the log info. my ($time, $log); if (/^(\w+\s+\d+\s+\d+:\d+:\d+)\s+([\w\.]+)\s+\S*ipmon\[\d+\]:\s+(\d+:\d+:\d+\.\d+)\s+(.+)/) { my ($logtime, $loghost); ($logtime, $loghost, $time, $log) = ($1, $2, $3, $4); } elsif (/^(\d+\/\d+\/\d+)\s+(\d+:\d+:\d+\.\d+)\s+(.+)$/) { my $logdate; ($logdate, $time, $log) = ($1, $2, $3); } else { # It don't look like no ipmon output to me, baby. next; } next unless (defined ($log)); # Parse the log line. We're expecting interface name, rule group and # number, an action code, a source host name or IP with possible port # name or number, a destination host name or IP with possible port # number, "PR", a protocol name or number, "len", a header length, a # packet length, and maybe some additional info. $log =~ /^(\w+)\s+@(\d+):(\d+)\s+(\w)\s+([a-zA-Z0-9\-\.,]+)\s+->\s+([a-zA-Z0-9\-\.,]+)\s+PR\s+(\w+)\s+len\s+(\d+)\s+(\d+)\s*(.*)$/; my ($if, $group, $rule, $act, $src, $dest, $proto, $hlen, $len, $more) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10); unless (defined ($len)) { warn ("Bad input line at $.: \"$_\""); next; } my ($sport, $dport); if ($proto eq 'icmp') { if ($more =~ s/^icmp (\d+)\/(\d+)\s*//) { # We save icmp type and code in both sport and dport. $dport = $sport = "$1.$2"; } else { $sport = ''; $dport = ''; } } else { $sport = (($src =~ s/,(\w+)$//) ? &portSimplify ($1, $proto) : ''); $dport = (($dest =~ s/,(\w+)$//) ? &portSimplify ($1, $proto) : ''); } # Make sure addresses are numeric at this point. We want to sort by # IP address later. This has got to do some weird things, but if you # want to use ipmon -n, be ready for weirdness. $src = &hostNumber ($src); $dest = &hostNumber ($dest); # Convert proto to proto number. $proto = &protoNumber ($proto); sub countPacket { my ($host, $dir, $peer, $proto, $packet) = @_; # Make sure host is in the hosts hash. $hosts{$host} = +{ 'out' => +{ }, 'in' => +{ }, } unless (exists ($hosts{$host})); # Get the incoming/outgoing traffic hash for the host in question. my $trafficHash = $hosts{$host}->{$dir}; # Make sure there's a hash for the peer. $trafficHash->{$peer} = +{ } unless (exists ($trafficHash->{$peer})); # Make sure the peer hash has a hash for the protocol number. my $peerHash = $trafficHash->{$peer}; $peerHash->{$proto} = +{ } unless (exists ($peerHash->{$proto})); # Make sure there's a counter for this packet type in the proto hash. my $protoHash = $peerHash->{$proto}; $protoHash->{$packet} = 0 unless (exists ($protoHash->{$packet})); # Increment the counter. ++$protoHash->{$packet}; } # Count the packet as outgoing traffic from the source address. &countPacket ($src, 'out', $dest, $proto, "$sport:$dport:$if:$act"); # Count the packet as incoming traffic to the destination address. &countPacket ($dest, 'in', $src, $proto, "$dport:$sport:$if:$act"); } my $dir; foreach $dir (qw(out in)) { my $order = ($dir eq 'out' ? 'source' : 'destination'); my $arrow = ($dir eq 'out' ? '->' : '<-'); print "### Traffic by $order address:\n"; sub ipSort { my @a = split (/\./, $a); my @b = split (/\./, $b); $a[0] != $b[0] ? $a[0] <=> $b[0] : $a[1] != $b[1] ? $a[1] <=> $b[1] : $a[2] != $b[2] ? $a[2] <=> $b[2] : $a[3] != $b[3] ? $a[3] <=> $b[3] : 0; } my $host; foreach $host (sort ipSort (keys %hosts)) { my $traffic = $hosts{$host}->{$dir}; # Skip hosts with no traffic. next unless (scalar (keys (%{$traffic}))); if ($numeric) { print " $host\n"; } else { print " ", &hostName ($host), " \[$host\]\n"; } my $peer; foreach $peer (sort ipSort (keys %{$traffic})) { my $peerHash = $traffic->{$peer}; my $peerName = &hostName ($peer); my $proto; foreach $proto (sort (keys (%{$peerHash}))) { my $protoHash = $peerHash->{$proto}; my $protoName = &protoName ($proto); sub packetSort { my ($asport, $adport, $aif, $aact) = split (/:/, $a); my ($bsport, $bdport, $bif, $bact) = split (/:/, $b); return $bact cmp $aact if ($aact ne $bact); return $aif cmp $bif if ($aif ne $bif); return $asport <=> $bsport if ($asport != $bsport); return $adport <=> $bdport if ($adport != $bdport); } my $packet; foreach $packet (sort packetSort (keys %{$protoHash})) { my ($sport, $dport, $if, $act) = split (/:/, $packet); my $count = $protoHash->{$packet}; $act = '?' unless (defined ($act = $acts{$act})); if (($protoName eq 'tcp') || ($protoName eq 'udp')) { printf (" %-6s %7s %5d %6s %14s %2s %s.%s\n", $if, $act, $count, $protoName, &portName ($sport, $protoName), $arrow, $peerName, &portName ($dport, $protoName)); } elsif ($protoName eq 'icmp') { printf (" %-6s %7s %5d %6s %14s %2s %s\n", $if, $act, $count, $protoName, &icmpType ($sport), $arrow, $peerName); } else { printf (" %-6s %7s %5d %6s %14s %2s %s\n", $if, $act, $count, $protoName, '', $arrow, $peerName); } } } } } print "\n\n"; } exit (0); # We use this hash to cache port name -> number and number -> name mappings. # Isn't is cool that we can use the same hash for both? my %pn; # Translates a numeric port/named protocol to a port name. Reserved ports # that do # not have an entry in the services database are left numeric. # High ports that do not have an entry in the services database are mapped # to ''. sub portName { my $port = shift; my $proto = shift; my $pname = "$port/$proto"; unless (exists ($pn{$pname})) { my $name = getservbyport ($port, $proto); $pn{$pname} = (defined ($name) ? $name : ($port <= 1023 ? $port : '')); } return $pn{$pname}; } # Translates a named port/protocol to a port number. sub portNumber { my $port = shift; my $proto = shift; my $pname = "$port/$proto"; unless (exists ($pn{$pname})) { my $number = getservbyname ($port, $proto); unless (defined ($number)) { # I don't think we need to recover from this. How did the port # name get into the log file if we can't find it? Log file from # a different machine? Fix /etc/services on this one if that's # your problem. die ("Unrecognized port name \"$port\" at $."); } $pn{$pname} = $number; } return $pn{$pname}; } # Convert all unrecognized high ports to the same value so they are treated # identically. The protocol should be by name. sub portSimplify { my $port = shift; my $proto = shift; # Make sure port is numeric. $port = &portNumber ($port, $proto) unless ($port =~ /^\d+$/); # Look up port name. my $portName = &portName ($port, $proto); # Port is an unknown high port. Return a value that is too high for a # port number, so that high ports get sorted last. return $highPort if ($portName eq ''); # Return original port number. return $port; } # Again, we can use the same hash for both host name -> IP mappings and # IP -> name mappings. my %ip; # Translates a dotted quad into a hostname. Don't pass names to this # function. sub hostName { my $ip = shift; return $ip if ($numeric); unless (exists ($ip{$ip})) { my $addr = inet_aton ($ip); my $name = gethostbyaddr ($addr, AF_INET); if (defined ($name)) { $ip{$ip} = $name; # While we're at it, cache the forward lookup. $ip{$name} = $ip; } else { # Just map the IP address to itself. There's no reverse. $ip{$ip} = $ip; } } return $ip{$ip}; } # Translates a hostname or dotted quad into a dotted quad. sub hostNumber { my $name = shift; if ($name =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) { # Return original value for dotted quads. my $or = int ($1) | int ($2) | int ($3) | int ($4); return $name if ($or == ($or & 0xff)); } unless (exists ($ip{$name})) { my $addr = inet_aton ($name); unless (defined ($addr)) { # Again, I don't think we need to recover from this. If we can't # resolve a hostname that ended up in the log file, punt. We # want to be able to sort hosts by IP address later, and letting # hostnames through will snarl up that code. Users of ipmon -n # will have to grin and bear it for now. die ("Unable to resolve host \"$name\" at $."); } my $ip = inet_ntoa ($addr); $ip{$name} = $ip; # While we're at it, cache the reverse lookup. $ip{$ip} = $name; } return $ip{$name}; } # Hash for protocol number <--> name mappings. my %pr; # Translates a protocol number into a protocol name, or a number if no name # is found in the protocol database. sub protoName { my $code = shift; return $code if ($code !~ /^\d+$/); unless (exists ($pr{$code})) { my $name = scalar (getprotobynumber ($code)); if (defined ($name)) { $pr{$code} = $name; } else { $pr{$code} = $code; } } return $pr{$code}; } # Translates a protocol name or number into a protocol number. sub protoNumber { my $name = shift; return $name if ($name =~ /^\d+$/); unless (exists ($pr{$name})) { my $code = scalar (getprotobyname ($name)); if (defined ($code)) { $pr{$name} = $code; } else { $pr{$name} = $name; } } return $pr{$name}; } sub icmpType { my %icmp = ( 0 => +{ name => 'echo-reply', codes => +{0 => undef}, }, 3 => +{ name => 'dest-unr', codes => +{ 0 => 'net', 1 => 'host', 2 => 'proto', 3 => 'port', 4 => 'need-frag', 5 => 'no-sroute', 6 => 'net-unk', 7 => 'host-unk', 8 => 'shost-isol', 9 => 'net-proh', 10 => 'host-proh', 11 => 'net-tos', 12 => 'host-tos', }, }, 4 => +{ name => 'src-quench', codes => +{0 => undef}, }, 5 => +{ name => 'redirect', codes => +{ 0 => 'net', 1 => 'host', 2 => 'tos', 3 => 'tos-host', }, }, 6 => +{ name => 'alt-host-addr', codes => +{0 => undef}, }, 8 => +{ name => 'echo', codes => +{0 => undef}, }, 9 => +{ name => 'rtr-advert', codes => +{0 => undef}, }, 10 => +{ name => 'rtr-select', codes => +{0 => undef}, }, 11 => +{ name => 'time-excd', codes => +{ 0 => 'in-transit', 1 => 'frag-assy', }, }, 12 => +{ name => 'param-prob', codes => +{ 0 => 'ptr-err', 1 => 'miss-opt', 2 => 'bad-len', }, }, 13 => +{ name => 'time', codes => +{0 => undef}, }, 14 => +{ name => 'time-reply', codes => +{0 => undef}, }, 15 => +{ name => 'info', codes => +{0 => undef}, }, 16 => +{ name => 'info-req', codes => +{0 => undef}, }, 17 => +{ name => 'mask-req', codes => +{0 => undef}, }, 18 => +{ name => 'mask-reply', codes => +{0 => undef}, }, 31 => +{ name => 'dgram-conv-err', codes => +{ }, }, 32 => +{ name => 'mbl-host-redir', codes => +{ }, }, 33 => +{ name => 'ipv6-whereru?', codes => +{ }, }, 34 => +{ name => 'ipv6-iamhere', codes => +{ }, }, 35 => +{ name => 'mbl-reg-req', codes => +{ }, }, 36 => +{ name => 'mbl-reg-rep', codes => +{ }, }, ); my $typeCode = shift; my ($type, $code) = split ('\.', $typeCode); return "?" unless (defined ($code)); my $info = $icmp{$type}; return "\(type=$type/$code?\)" unless (defined ($info)); my $typeName = $info->{name}; my $codeName; if (exists ($info->{codes}->{$code})) { $codeName = $info->{codes}->{$code}; $codeName = (defined ($codeName) ? "/$codeName" : ''); } else { $codeName = "/$code"; } return "$typeName$codeName"; } sub usage { my $ec = shift; print STDERR <