xref: /netbsd-src/external/bsd/ipf/dist/perl/plog (revision bc4097aacfdd9307c19b7947c13c6ad6982527a9)
1#!/usr/bin/perl -wT
2#
3# Author: Jefferson Ogata (JO317) <jogata@pobox.com>
4# Date: 2000/04/22
5# Version: 0.10
6#
7# Please feel free to use or redistribute this program if you find it useful.
8# If you have suggestions, or even better, bits of new code, send them to me
9# and I will add them when I have time. The current version of this script
10# can always be found at the URL:
11#
12#    http://www.antibozo.net/ogata/webtools/plog.pl
13#    http://pobox.com/~ogata/webtools/plog.txt
14#
15# Parse ipmon output into a coherent form. This program only handles the
16# lines regarding filter actions. It does not parse nat and state lines.
17#
18# Present lines from ipmon to this program on standard input.
19#
20# EXAMPLES
21#
22# plog -AF block,log < /var/log/ipf
23#
24#    Generate source and destination reports of all packets logged with
25#    block or log actions, and report TCP flags and keep state actions.
26#
27# plog -S -s ./services www.example.com < /var/log/ipf
28#
29#    Generate a source report of traffic to or from www.example.com using
30#    the additional services defined in ./services.
31#
32# plog -nSA block < /var/log/ipf
33#
34#    Generate a source report of all blocked packets with no hostname
35#    lookups. This is handy for an initial pass to identify portscans or
36#    other aggressive traffic.
37#
38# plog -SFp 192.168.0.0/24 www.example.com/24 < /var/log/ipf
39#
40#    Generate a source report of all packets whose source or destination
41#    address is either in 192.168.0.0/24 or an address associated with
42#    the host www.example.com, report packet flags and perform paranoid
43#    hostname lookups. This is a handy usage for examining traffic more
44#    closely after identifying a potential attack.
45#
46# TODO
47#
48# - Handle output from ipmon -v.
49# - Handle timestamps from other locales. Anyone with a timestamp problem
50#   please email me the format of your timestamps.
51# - It looks as though short TCP or UDP packets will break things, but I
52#   haven't seen any yet.
53#
54# CHANGES
55#
56# 2000/04/22 (0.10):
57# - Restructured host name and address caches. Hosts are now cached using
58#   packed addresses as keys. Conversion to IPv6 should be simple now.
59# - Added paranoid hostname lookups.
60# - Added netmask qualifications for address arguments.
61# - Tweaked usage info.
62# 2000/04/20:
63# - Added parsing and tracking of TCP and state flags.
64# 2000/04/12 (0.9):
65# - Wasn't handling underscore in hostname,servicename fields; these may be
66#   logged using ipmon -n. Observation by <ark@eltex.ru>.
67# - Hadn't properly attributed observation and fix for repetition counter in
68#   0.8 change log. Added John Ladwig to attribution. Thanks, John.
69#
70# 2000/04/10 (0.8):
71# - Service names can also have hyphens, dummy. I wasn't allowing these
72#   either. Observation and fix thanks to Taso N. Devetzis
73#   <devetzis@snet.net>.
74# - IP Filter now logs a repetition counter. Observation and fixes (changed
75#   slightly) from Andy Kreiling <Andy@ntcs-inc.com> and John Ladwig
76#   <jladwig@nts.umn.edu>.
77# - Added fix to handle new Solaris log format, e.g.:
78#     Nov 30 04:49:37 raoul ipmon[121]: [ID 702911 local0.warning] 04:49:36.420541 hme0 @0:34 b 205.152.16.6,58596 -> 204.60.220.24,113 PR tcp len 20 44
79#   Fix thanks to Taso N. Devetzis <devetzis@SNET.Net>.
80# - Added services map option.
81# - Added options for generating only source/destination tables.
82# - Added verbosity option.
83# - Added option for reporting traffic for specific hosts.
84# - Added some more ICMP unreachable codes, and made code and type names
85#   match the ones in IP Filter parse.c.
86# - Condensed output format somewhat.
87# - Various minor improvements, perhaps slight speed improvements.
88# - Documented new options in usage() and tried to improve wording.
89#
90# 1999/08/02 (0.7):
91# - Hostnames can have hyphens, dummy. I wasn't allowing them in the syslog
92#   line. Fix from Antoine Verheijen <antoine.verheijen@ualberta.ca>.
93#
94# 1999/05/05 (0.6):
95# - IRIX syslog prefixes the hostname with a severity code. Handle it. Fix
96#   from John Ladwig <jladwig@nts.umn.edu>.
97#
98# 1999/05/05 (0.5):
99# - Protocols other than TCP, UDP, or ICMP have packet lengths reported in
100#   parentheses for some reason. The script now handles this. Thanks to
101#   Dispatcher <dispatch@blackhelicopters.org>.
102# - I had mixed up info-request and info-reply ICMP codes, and omitted the
103#   traceroute code. Sorted this out. I had also missed code 0 for type 6
104#   (alternate address for host). Thanks to John Ladwig <jladwig@nts.umn.edu>.
105#
106# 1999/05/03:
107# - Now accepts hostnames in the source and destination address fields, as
108#   well as port names in the port fields. This allows the people who are
109#   using ipmon -n to still use plog. Note that if you are logging
110#   hostnames, you are vulnerable to forgery of DNS information, modified
111#   DNS information, and your log files will be larger also. If you are
112#   using this program you can have it look up the names for you (still
113#   vulnerable to forgery) and keep your logged addresses all in numeric
114#   format, so that packets from the same source will always show the same
115#   source address regardless of what's up with DNS. Obviously, I don't
116#   favor using ipmon -n. Nevertheless, some people wanted this, so here it
117#   is.
118# - Added S and n flags to %acts hash. Thanks to Stephen J. Roznowski
119#   <sjr@home.net>.
120# - Stopped reporting host IPs twice when numeric output was requested.
121#   Thanks, yet again, to Stephen J. Roznowski <sjr@home.net>.
122# - Number of minor tweaks that might speed it up a bit, and some comments.
123# - Put the script back up on the web site. I had moved the site and
124#   forgotten to move the tool.
125#
126# 1999/02/04:
127# - Changed log line parser to accept fully-qualified name in the logging
128#   host field. Thanks to Stephen J. Roznowski <sjr@home.net>.
129#
130# 1999/01/22:
131# - Changed high port strategy to use 65536 for unknown high ports so that
132#   they are sorted last.
133#
134# 1999/01/21:
135# - Moved icmp parsing to output loop.
136# - Added parsing of icmp codes, and more types.
137# - Changed packet sort routine to sort by port number rather than service
138#   name.
139#
140# 1999/01/20:
141# - Fixed problem matching ipmon log lines. Sometimes they have "/ipmon" in
142#   them, sometimes just "ipmon".
143# - Added numeric parse option to turn off hostname lookups.
144# - Moved summary to usage() sub.
145
146use strict;
147use Socket;
148use IO::File;
149
150select STDOUT; $| = 1;
151
152my %hosts;
153
154my $me = $0;
155$me =~ s/^.*\///;
156
157# Map of log codes for various actions. Not all of these can occur, but
158# I've included everything in print_ipflog() from ipmon.c.
159my %acts = (
160    'p'	=> 'pass',
161    'P'	=> 'pass',
162    'b'	=> 'block',
163    'B'	=> 'block',
164    'L'	=> 'log',
165    'S' => 'short',
166    'n' => 'nomatch',
167);
168
169# Map of ICMP types and their relevant codes.
170my %icmpTypeMap = (
171    0	=> +{
172	name	=> 'echorep',
173	codes	=> +{0	=> undef},
174    },
175    3	=> +{
176	name	=> 'unreach',
177	codes	=> +{
178	    0	=> 'net-unr',
179	    1	=> 'host-unr',
180	    2	=> 'proto-unr',
181	    3	=> 'port-unr',
182	    4	=> 'needfrag',
183	    5	=> 'srcfail',
184	    6	=> 'net-unk',
185	    7	=> 'host-unk',
186	    8	=> 'isolate',
187	    9	=> 'net-prohib',
188	    10	=> 'host-prohib',
189	    11	=> 'net-tos',
190	    12	=> 'host-tos',
191	    13	=> 'filter-prohib',
192	    14	=> 'host-preced',
193	    15	=> 'preced-cutoff',
194	},
195    },
196    4	=> +{
197	name	=> 'squench',
198	codes	=> +{0	=> undef},
199    },
200    5	=> +{
201	name	=> 'redir',
202	codes	=> +{
203	    0	=> 'net',
204	    1	=> 'host',
205	    2	=> 'tos',
206	    3	=> 'tos-host',
207	},
208    },
209    6	=> +{
210	name	=> 'alt-host-addr',
211	codes	=> +{
212	    0	=> 'alt-addr'
213	},
214    },
215    8	=> +{
216	name	=> 'echo',
217	codes	=> +{0	=> undef},
218    },
219    9	=> +{
220	name	=> 'routerad',
221	codes	=> +{0	=> undef},
222    },
223    10	=> +{
224	name	=> 'routersol',
225	codes	=> +{0	=> undef},
226    },
227    11	=> +{
228	name	=> 'timex',
229	codes	=> +{
230	    0	=> 'in-transit',
231	    1	=> 'frag-assy',
232	},
233    },
234    12	=> +{
235	name	=> 'paramprob',
236	codes	=> +{
237	    0	=> 'ptr-err',
238	    1	=> 'miss-opt',
239	    2	=> 'bad-len',
240	},
241    },
242    13	=> +{
243	name	=> 'timest',
244	codes	=> +{0	=> undef},
245    },
246    14	=> +{
247	name	=> 'timestrep',
248	codes	=> +{0	=> undef},
249    },
250    15	=> +{
251	name	=> 'inforeq',
252	codes	=> +{0	=> undef},
253    },
254    16	=> +{
255	name	=> 'inforep',
256	codes	=> +{0	=> undef},
257    },
258    17	=> +{
259	name	=> 'maskreq',
260	codes	=> +{0	=> undef},
261    },
262    18	=> +{
263	name	=> 'maskrep',
264	codes	=> +{0	=> undef},
265    },
266    30	=> +{
267	name	=> 'tracert',
268	codes	=> +{ },
269    },
270    31	=> +{
271	name	=> 'dgram-conv-err',
272	codes	=> +{ },
273    },
274    32	=> +{
275	name	=> 'mbl-host-redir',
276	codes	=> +{ },
277    },
278    33	=> +{
279	name	=> 'ipv6-whereru?',
280	codes	=> +{ },
281    },
282    34	=> +{
283	name	=> 'ipv6-iamhere',
284	codes	=> +{ },
285    },
286    35	=> +{
287	name	=> 'mbl-reg-req',
288	codes	=> +{ },
289    },
290    36	=> +{
291	name	=> 'mbl-reg-rep',
292	codes	=> +{ },
293    },
294);
295
296# Arguments we will parse from argument list.
297my $numeric = 0;	# Don't lookup hostnames.
298my $paranoid = 0;	# Do paranoid hostname lookups.
299my $verbosity = 0;	# Bla' bla' bla'.
300my $sTable = 0;		# Generate source table.
301my $dTable = 0;		# Generate destination table.
302my @services = ();	# Preload services tables.
303my $showFlags = 0;	# Show TCP flag combinations.
304my %selectAddrs;	# Limit report to these hosts.
305my %selectActs;		# Limit report to these actions.
306
307# Parse argument list.
308while (defined ($_ = shift))
309{
310    if (s/^-//)
311    {
312	while (s/^([vnpSD\?hsAF])//)
313	{
314	    my $flag = $1;
315	    if ($flag eq 'v')
316	    {
317		++$verbosity;
318	    }
319	    elsif ($flag eq 'n')
320	    {
321		$numeric = 1;
322	    }
323	    elsif ($flag eq 'p')
324	    {
325		$paranoid = 1;
326	    }
327	    elsif ($flag eq 'S')
328	    {
329		$sTable = 1;
330	    }
331	    elsif ($flag eq 'D')
332	    {
333		$dTable = 1;
334	    }
335	    elsif ($flag eq 'F')
336	    {
337		$showFlags = 1;
338	    }
339	    elsif (($flag eq '?') || ($flag eq 'h'))
340	    {
341		&usage (0);
342	    }
343	    else
344	    {
345		my $arg = shift;
346		defined ($arg) || &usage (1, qq{-$flag requires an argument});
347		if ($flag eq 's')
348		{
349		    push (@services, $arg);
350		}
351		elsif ($flag eq 'A')
352		{
353		    my @acts = split (/,/, $arg);
354		    my $a;
355		    foreach $a (@acts)
356		    {
357			my $aa;
358			my $match = 0;
359			foreach $aa (keys (%acts))
360			{
361			    if ($acts{$aa} eq $a)
362			    {
363				++$match;
364				$selectActs{$aa} = $a;
365			    }
366			}
367			$match || &usage (1, qq{unknown action $a});
368		    }
369		}
370	    }
371	}
372
373	&usage (1, qq{unknown option: -$_}) if (length);
374
375	next;
376    }
377
378    # Add host to hash of hosts we're interested in.
379    (/^(.+)\/([\d+\.]+)$/) || (/^(.+)$/) || &usage (1, qq{invalid CIDR address $_});
380    my ($addr, $mask) = ($1, $2);
381    my @addr = &hostAddrs ($addr);
382    (scalar (@addr)) || &usage (1, qq{cannot resolve hostname $_});
383    if (!defined ($mask))
384    {
385	$mask = (2 ** 32) - 1;
386    }
387    elsif (($mask =~ /^\d+$/) && ($mask <= 32))
388    {
389	$mask = (2 ** 32) - 1 - ((2 ** (32 - $mask)) - 1);
390    }
391    elsif (defined ($mask = &isDottedAddr ($mask)))
392    {
393	$mask = &integerAddr ($mask);
394    }
395    else
396    {
397	&usage (1, qq{invalid CIDR address $_});
398    }
399    foreach $addr (@addr)
400    {
401	# Save mask unless we already have a less specific one for this address.
402	my $a = &integerAddr ($addr) & $mask;
403	$selectAddrs{$a} = $mask unless (exists ($selectAddrs{$a}) && ($selectAddrs{$a} < $mask));
404    }
405}
406
407# Which tables will we generate?
408$dTable = $sTable = 1 unless ($dTable || $sTable);
409my @dirs;
410push (@dirs, 'd') if ($dTable);
411push (@dirs, 's') if ($sTable);
412
413# Are we interested in specific hosts?
414my $selectAddrs = scalar (keys (%selectAddrs));
415
416# Are we interested in specific actions?
417if (scalar (keys (%selectActs)) == 0)
418{
419    %selectActs = %acts;
420}
421
422# We use this hash to cache port name -> number and number -> name mappings.
423# Isn't it cool that we can use the same hash for both?
424my %pn;
425
426# Preload any services maps.
427my $sm;
428foreach $sm (@services)
429{
430    my $sf = new IO::File ($sm, "r");
431    defined ($sf) || &quit (1, qq{cannot open services file $sm});
432
433    while (defined ($_ = $sf->getline ()))
434    {
435	my $text = $_;
436	chomp;
437	s/#.*$//;
438	s/\s+$//;
439	next unless (length);
440	my ($name, $spec, @aliases) = split (/\s+/);
441	($spec =~ /^([\w\-]+)\/([\w\-]+)$/)
442	    || &quit (1, qq{$sm:$.: invalid definition: $text});
443	my ($pnum, $proto) = ($1, $2);
444
445	# Enter service definition in pn hash both forwards and backwards.
446	my $port;
447	my $pname;
448	foreach $port ($name, @aliases)
449	{
450	    $pname = "$pnum/$proto";
451	    $pn{$pname} = $port;
452	}
453	$pname = "$name/$proto";
454	$pn{$pname} = $pnum;
455    }
456
457    $sf->close ();
458}
459
460# Cache for host name -> addr mappings.
461my %ipAddr;
462
463# Cache for host addr -> name mappings.
464my %ipName;
465
466# Hash for protocol number <--> name mappings.
467my %pr;
468
469# Under IPv4 port numbers are unsigned shorts. The value below is higher
470# than the maximum value of an unsigned short, and is used in place of
471# high port numbers that don't correspond to known services. This makes
472# high ports get sorted behind all others.
473my $highPort = 0x10000;
474
475while (<STDIN>)
476{
477    chomp;
478
479    # For ipmon output that came through syslog, we'll have an asctime
480    # timestamp, an optional severity code (IRIX), the hostname,
481    # "ipmon"[process id]: prefixed to the line. For output that was
482    # written directly to a file by ipmon, we'll have a date prefix as
483    # dd/mm/yyyy (no y2k problem here!). Both formats then have a packet
484    # timestamp and the log info.
485    my ($log);
486    if (s/^\w+\s+\d+\s+\d+:\d+:\d+\s+(?:\d\w:)?[\w\.\-]+\s+\S*ipmon\[\d+\]:\s+(?:\[ID\s+\d+\s+[\w\.]+\]\s+)?\d+:\d+:\d+\.\d+\s+//)
487    {
488	$log = $_;
489    }
490    elsif (s/^(?:\d+\/\d+\/\d+)\s+(?:\d+:\d+:\d+\.\d+)\s+//)
491    {
492	$log = $_;
493    }
494    else
495    {
496	# It don't look like no ipmon output to me, baby.
497	next;
498    }
499    next unless (defined ($log));
500
501    print STDERR "$log\n" if ($verbosity);
502
503    # Parse the log line. We're expecting interface name, rule group and
504    # number, an action code, a source host name or IP with possible port
505    # name or number, a destination host name or IP with possible port
506    # number, "PR", a protocol name or number, "len", a header length, a
507    # packet length (which will be in parentheses for protocols other than
508    # TCP, UDP, or ICMP), and maybe some additional info.
509    my @fields = ($log =~ /^(?:(\d+)x)?\s*(\w+)\s+@(\d+):(\d+)\s+(\w)\s+([\w\-\.,]+)\s+->\s+([\w\-\.,]+)\s+PR\s+(\w+)\s+len\s+(\d+)\s+\(?(\d+)\)?\s*(.*)$/ox);
510    unless (scalar (@fields))
511    {
512	print STDERR "$me:$.: cannot parse: $_\n";
513	next;
514    }
515    my ($count, $if, $group, $rule, $act, $src, $dest, $proto, $hlen, $len, $more) = @fields;
516
517    # Skip actions we're not interested in.
518    next unless (exists ($selectActs{$act}));
519
520    # Packet count defaults to 1.
521    $count = 1 unless (defined ($count));
522
523    my ($sport, $dport, @flags);
524
525    if ($proto eq 'icmp')
526    {
527	if ($more =~ s/^icmp (\d+)\/(\d+)\s*//)
528	{
529	    # We save icmp type and code in both sport and dport. This
530	    # allows us to sort icmp packets using the normal port-sorting
531	    # code.
532	    $dport = $sport = "$1.$2";
533	}
534	else
535	{
536	    $sport = '';
537	    $dport = '';
538	}
539    }
540    else
541    {
542	if ($showFlags)
543	{
544	    if (($proto eq 'tcp') && ($more =~ s/^\-([A-Z]+)\s*//))
545	    {
546		push (@flags, $1);
547	    }
548	    if ($more =~ s/^K\-S\s*//)
549	    {
550		push (@flags, 'state');
551	    }
552	}
553	if ($src =~ s/,([\-\w]+)$//)
554	{
555	    $sport = &portSimplify ($1, $proto);
556	}
557	else
558	{
559	    $sport = '';
560	}
561	if ($dest =~ s/,([\-\w]+)$//)
562	{
563	    $dport = &portSimplify ($1, $proto);
564	}
565	else
566	{
567	    $dport = '';
568	}
569    }
570
571    # Make sure addresses are numeric at this point. We want to sort by
572    # IP address later. If the hostname doesn't resolve, punt. If you
573    # must use ipmon -n, be ready for weirdness. Use only the first
574    # address returned.
575    my $x;
576    $x = (&hostAddrs ($src))[0];
577    unless (defined ($x))
578    {
579	print STDERR "$me:$.: cannot resolve hostname $src\n";
580	next;
581    }
582    $src = $x;
583    $x = (&hostAddrs ($dest))[0];
584    unless (defined ($x))
585    {
586	print STDERR "$me:$.: cannot resolve hostname $dest\n";
587	next;
588    }
589    $dest = $x;
590
591    # Skip hosts we're not interested in.
592    if ($selectAddrs)
593    {
594	my ($a, $m);
595	my $s = &integerAddr ($src);
596	my $d = &integerAddr ($dest);
597	my $cute = 0;
598	while (($a, $m) = each (%selectAddrs))
599	{
600	    if ((($s & $m) == $a) || (($d & $m) == $a))
601	    {
602		$cute = 1;
603		last;
604	    }
605	}
606	next unless ($cute);
607    }
608
609    # Convert proto to proto number.
610    $proto = &protoNumber ($proto);
611
612    sub countPacket
613    {
614	my ($host, $dir, $peer, $proto, $count, $packet, @flags) = @_;
615
616	# Make sure host is in the hosts hash.
617	$hosts{$host} =
618	    +{
619		'd'	=> +{ },
620		's'	=> +{ },
621	    } unless (exists ($hosts{$host}));
622
623	# Get the source/destination traffic hash for the host in question.
624	my $trafficHash = $hosts{$host}->{$dir};
625
626	# Make sure there's a hash for the peer.
627	$trafficHash->{$peer} = +{ } unless (exists ($trafficHash->{$peer}));
628
629	# Make sure the peer hash has a hash for the protocol number.
630	my $peerHash = $trafficHash->{$peer};
631	$peerHash->{$proto} = +{ } unless (exists ($peerHash->{$proto}));
632
633	# Make sure there's a counter for this packet type in the proto hash.
634	my $protoHash = $peerHash->{$proto};
635	$protoHash->{$packet} = +{ '' => 0 } unless (exists ($protoHash->{$packet}));
636
637	# Increment the counter and mark flags.
638	my $packetHash = $protoHash->{$packet};
639	$packetHash->{''} += $count;
640	map { $packetHash->{$_} = undef; } (@flags);
641    }
642
643    # Count the packet as outgoing traffic from the source address.
644    &countPacket ($src, 's', $dest, $proto, $count, "$sport:$dport:$if:$act", @flags) if ($sTable);
645
646    # Count the packet as incoming traffic to the destination address.
647    &countPacket ($dest, 'd', $src, $proto, $count, "$dport:$sport:$if:$act", @flags) if ($dTable);
648}
649
650my $dir;
651foreach $dir (@dirs)
652{
653    my $order = ($dir eq 's' ? 'source' : 'destination');
654    my $arrow = ($dir eq 's' ? '->' : '<-');
655
656    print "###\n";
657    print "### Traffic by $order address:\n";
658    print "###\n";
659
660    sub ipSort
661    {
662	&integerAddr ($a) <=> &integerAddr ($b);
663    }
664
665    sub packetSort
666    {
667	my ($asport, $adport, $aif, $aact) = split (/:/, $a);
668	my ($bsport, $bdport, $bif, $bact) = split (/:/, $b);
669	$bact cmp $aact || $aif cmp $bif || $asport <=> $bsport || $adport <=> $bdport;
670    }
671
672    my $host;
673    foreach $host (sort ipSort (keys %hosts))
674    {
675	my $traffic = $hosts{$host}->{$dir};
676
677	# Skip hosts with no traffic.
678	next unless (scalar (keys (%{$traffic})));
679
680	if ($numeric)
681	{
682	    print &dottedAddr ($host), "\n";
683	}
684	else
685	{
686	    print &hostName ($host), " \[", &dottedAddr ($host), "\]\n";
687	}
688
689	my $peer;
690	foreach $peer (sort ipSort (keys %{$traffic}))
691	{
692	    my $peerHash = $traffic->{$peer};
693	    my $peerName = ($numeric ? &dottedAddr ($peer) : &hostName ($peer));
694	    my $proto;
695	    foreach $proto (sort (keys (%{$peerHash})))
696	    {
697		my $protoHash = $peerHash->{$proto};
698		my $protoName = &protoName ($proto);
699
700		my $packet;
701		foreach $packet (sort packetSort (keys %{$protoHash}))
702		{
703		    my ($sport, $dport, $if, $act) = split (/:/, $packet);
704		    my $packetHash = $protoHash->{$packet};
705		    my $count = $packetHash->{''};
706		    $act = '?' unless (defined ($act = $acts{$act}));
707		    if (($protoName eq 'tcp') || ($protoName eq 'udp'))
708		    {
709			printf ("    %-6s %7s %4d %4s %16s %2s %s.%s", $if, $act, $count, $protoName, &portName ($sport, $protoName), $arrow, $peerName, &portName ($dport, $protoName));
710		    }
711		    elsif ($protoName eq 'icmp')
712		    {
713			printf ("    %-6s %7s %4d %4s %16s %2s %s", $if, $act, $count, $protoName, &icmpType ($sport), $arrow, $peerName);
714		    }
715		    else
716		    {
717			printf ("    %-6s %7s %4d %4s %16s %2s %s", $if, $act, $count, $protoName, '', $arrow, $peerName);
718		    }
719		    if ($showFlags)
720		    {
721			my @flags = sort (keys (%{$packetHash}));
722			if (scalar (@flags))
723			{
724			    shift (@flags);
725			    print ' (', join (',', @flags), ')' if (scalar (@flags));
726			}
727		    }
728		    print "\n";
729		}
730	    }
731	}
732    }
733
734    print "\n";
735}
736
737exit (0);
738
739# Translates a numeric port/named protocol to a port name. Reserved ports
740# that do not have an entry in the services database are left numeric. High
741# ports that do not have an entry in the services database are mapped
742# to '<high>'.
743sub portName
744{
745    my $port = shift;
746    my $proto = shift;
747    my $pname = "$port/$proto";
748    unless (exists ($pn{$pname}))
749    {
750	my $name = getservbyport ($port, $proto);
751	$pn{$pname} = (defined ($name) ? $name : ($port <= 1023 ? $port : '<high>'));
752    }
753    return $pn{$pname};
754}
755
756# Translates a named port/protocol to a port number.
757sub portNumber
758{
759    my $port = shift;
760    my $proto = shift;
761    my $pname = "$port/$proto";
762    unless (exists ($pn{$pname}))
763    {
764	my $number = getservbyname ($port, $proto);
765	unless (defined ($number))
766	{
767	    # I don't think we need to recover from this. How did the port
768	    # name get into the log file if we can't find it? Log file from
769	    # a different machine? Fix /etc/services on this one if that's
770	    # your problem.
771	    die ("Unrecognized port name \"$port\" at $.");
772	}
773	$pn{$pname} = $number;
774    }
775    return $pn{$pname};
776}
777
778# Convert all unrecognized high ports to the same value so they are treated
779# identically. The protocol should be by name.
780sub portSimplify
781{
782    my $port = shift;
783    my $proto = shift;
784
785    # Make sure port is numeric.
786    $port = &portNumber ($port, $proto)
787	unless ($port =~ /^\d+$/);
788
789    # Look up port name.
790    my $portName = &portName ($port, $proto);
791
792    # Port is an unknown high port. Return a value that is too high for a
793    # port number, so that high ports get sorted last.
794    return $highPort if ($portName eq '<high>');
795
796    # Return original port number.
797    return $port;
798}
799
800# Translates a numeric address into a hostname. Pass only packed numeric
801# addresses to this routine.
802sub hostName
803{
804    my $ip = shift;
805    return $ipName{$ip} if (exists ($ipName{$ip}));
806
807    # Do an inverse lookup on the address.
808    my $name = gethostbyaddr ($ip, AF_INET);
809    unless (defined ($name))
810    {
811	# Inverse lookup failed, so map the IP address to its dotted
812	# representation and cache that.
813	$ipName{$ip} = &dottedAddr ($ip);
814	return $ipName{$ip};
815    }
816
817    # For paranoid hostname lookups.
818    if ($paranoid)
819    {
820	# If this address already matches, we're happy.
821	unless (exists ($ipName{$ip}) && (lc ($ipName{$ip}) eq lc ($name)))
822	{
823	    # Do a forward lookup on the resulting name.
824	    my @addr = &hostAddrs ($name);
825	    my $match = 0;
826
827	    # Cache the forward lookup results for future inverse lookups,
828	    # but don't stomp on inverses we've already cached, even if they
829	    # are questionable. We want to generate consistent output, and
830	    # the cache is growing incrementally.
831	    foreach (@addr)
832	    {
833		$ipName{$_} = $name unless (exists ($ipName{$_}));
834		$match = 1 if ($_ eq $ip);
835	    }
836
837	    # Was this one of the addresses? If not, tack on a ?.
838	    $name .= '?' unless ($match);
839	}
840    }
841    else
842    {
843	# Just believe it and cache it.
844	$ipName{$ip} = $name;
845    }
846
847    return $name;
848}
849
850# Translates a hostname or dotted address into a list of packed numeric
851# addresses.
852sub hostAddrs
853{
854    my $name = shift;
855    my $ip;
856
857    # Check if it's a dotted representation.
858    return ($ip) if (defined ($ip = &isDottedAddr ($name)));
859
860    # Return result from cache.
861    $name = lc ($name);
862    return @{$ipAddr{$name}} if (exists ($ipAddr{$name}));
863
864    # Look up the addresses.
865    my @addr = gethostbyname ($name);
866    splice (@addr, 0, 4);
867
868    unless (scalar (@addr))
869    {
870	# Again, I don't think we need to recover from this gracefully.
871	# If we can't resolve a hostname that ended up in the log file,
872	# punt. We want to be able to sort hosts by IP address later,
873	# and letting hostnames through will snarl up that code. Users
874	# of ipmon -n will have to grin and bear it for now. The
875	# functions that get undef back should treat it as an error or
876	# as some default address, e.g. 0 just to make things work.
877	return ();
878    }
879
880    $ipAddr{$name} = [ @addr ];
881    return @{$ipAddr{$name}};
882}
883
884# If the argument is a valid dotted address, returns the corresponding
885# packed numeric address, otherwise returns undef.
886sub isDottedAddr
887{
888    my $addr = shift;
889    if ($addr =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
890    {
891	my @a = (int ($1), int ($2), int ($3), int ($4));
892	foreach (@a)
893	{
894	    return undef if ($_ >= 256);
895	}
896	return pack ('C*', @a);
897    }
898    return undef;
899}
900
901# Unpacks a packed numeric address and returns an integer representation.
902sub integerAddr
903{
904    my $addr = shift;
905    return unpack ('N', $addr);
906
907    # The following is for generalized IPv4/IPv6 stuff. For now, it's a
908    # lot faster to assume IPv4.
909    my @a = unpack ('C*', $addr);
910    my $a = 0;
911    while (scalar (@a))
912    {
913	$a = ($a << 8) | shift (@a);
914    }
915    return $a;
916}
917
918# Unpacks a packed numeric address into a dotted representation.
919sub dottedAddr
920{
921    my $addr = shift;
922    my @a = unpack ('C*', $addr);
923    return join ('.', @a);
924}
925
926# Translates a protocol number into a protocol name, or a number if no name
927# is found in the protocol database.
928sub protoName
929{
930    my $code = shift;
931    return $code if ($code !~ /^\d+$/);
932    unless (exists ($pr{$code}))
933    {
934	my $name = scalar (getprotobynumber ($code));
935	if (defined ($name))
936	{
937	    $pr{$code} = $name;
938	}
939	else
940	{
941	    $pr{$code} = $code;
942	}
943    }
944    return $pr{$code};
945}
946
947# Translates a protocol name or number into a protocol number.
948sub protoNumber
949{
950    my $name = shift;
951    return $name if ($name =~ /^\d+$/);
952    unless (exists ($pr{$name}))
953    {
954	my $code = scalar (getprotobyname ($name));
955	if (defined ($code))
956	{
957	    $pr{$name} = $code;
958	}
959	else
960	{
961	    $pr{$name} = $name;
962	}
963    }
964    return $pr{$name};
965}
966
967sub icmpType
968{
969    my $typeCode = shift;
970    my ($type, $code) = split ('\.', $typeCode);
971
972    return "?" unless (defined ($code));
973
974    my $info = $icmpTypeMap{$type};
975
976    return "\(type=$type/$code?\)" unless (defined ($info));
977
978    my $typeName = $info->{name};
979    my $codeName;
980    if (exists ($info->{codes}->{$code}))
981    {
982	$codeName = $info->{codes}->{$code};
983	$codeName = (defined ($codeName) ? "/$codeName" : '');
984    }
985    else
986    {
987	$codeName = "/$code";
988    }
989    return "$typeName$codeName";
990}
991
992sub quit
993{
994    my $ec = shift;
995    my $msg = shift;
996
997    print STDERR "$me: $msg\n";
998    exit ($ec);
999}
1000
1001sub usage
1002{
1003    my $ec = shift;
1004    my @msg = @_;
1005
1006    if (scalar (@msg))
1007    {
1008	print STDERR "$me: ", join ("\n", @msg), "\n\n";
1009    }
1010
1011    print <<EOT;
1012usage: $me [-nSDF] [-s servicemap] [-A act1,...] [address...]
1013
1014Parses logging from ipmon and presents it in a comprehensible format. This
1015program generates two reports: one organized by source address and another
1016organized by destination address. For the first report, source addresses are
1017sorted by IP address. For each address, all packets originating at the address
1018are presented in a tabular form, where all packets with the same source and
1019destination address and port are counted as a single entry. Any port number
1020greater than 1023 that does not match an entry in the services table is treated
1021as a "high" port; all high ports are coalesced into the same entry. The fields
1022for the source address report are:
1023    iface action packet-count proto src-port dest-host.dest-port \[\(flags\)\]
1024The fields for the destination address report are:
1025    iface action packet-count proto dest-port src-host.src-port \[\(flags\)\]
1026
1027Options are:
1028-n           Disable hostname lookups, and report only IP addresses.
1029-p           Perform paranoid hostname lookups.
1030-S           Generate a source address report.
1031-D           Generate a destination address report.
1032-F           Show all flag combinations associated with packets.
1033-s map       Supply an alternate services map to be preloaded. The map should
1034	     be in the same format as /etc/services. Any service name not found
1035             in the map will be looked for in the system services file.
1036-A act1,...  Limit the report to the specified actions. The possible actions
1037	     are pass, block, log, short, and nomatch.
1038
1039If any addresses are supplied on the command line, the report is limited to
1040these hosts. Addresses may be given as dotted IP addresses or hostnames, and
1041may be qualified with netmasks in CIDR \(/24\) or dotted \(/255.255.255.0\) format.
1042If a hostname resolves to multiple addresses, all addresses are used.
1043
1044If neither -S nor -D is given, both reports are generated.
1045
1046Note: if you are logging traffic with ipmon -n, ipmon will already have looked
1047up and logged addresses as hostnames where possible. This has an important side
1048effect: this program will translate the hostnames back into IP addresses which
1049may not match the original addresses of the logged packets because of numerous
1050DNS issues. If you care about where packets are really coming from, you simply
1051cannot rely on ipmon -n. An attacker with control of his reverse DNS can map
1052the reverse lookup to anything he likes. If you haven't logged the numeric IP
1053address, there's no way to discover the source of an attack reliably. For this
1054reason, I strongly recommend that you run ipmon without the -n option, and use
1055this or a similar script to do reverse lookups during analysis, rather than
1056during logging.
1057EOT
1058
1059    exit ($ec);
1060}
1061
1062