#!/usr/bin/perl -wT # # Author: Jefferson Ogata (JO317) # Date: 2000/04/22 # Version: 0.10 # # 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://www.antibozo.net/ogata/webtools/plog.pl # 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. # # EXAMPLES # # plog -AF block,log < /var/log/ipf # # Generate source and destination reports of all packets logged with # block or log actions, and report TCP flags and keep state actions. # # plog -S -s ./services www.example.com < /var/log/ipf # # Generate a source report of traffic to or from www.example.com using # the additional services defined in ./services. # # plog -nSA block < /var/log/ipf # # Generate a source report of all blocked packets with no hostname # lookups. This is handy for an initial pass to identify portscans or # other aggressive traffic. # # plog -SFp 192.168.0.0/24 www.example.com/24 < /var/log/ipf # # Generate a source report of all packets whose source or destination # address is either in 192.168.0.0/24 or an address associated with # the host www.example.com, report packet flags and perform paranoid # hostname lookups. This is a handy usage for examining traffic more # closely after identifying a potential attack. # # TODO # # - Handle output from ipmon -v. # - Handle timestamps from other locales. Anyone with a timestamp problem # please email me the format of your timestamps. # - It looks as though short TCP or UDP packets will break things, but I # haven't seen any yet. # # CHANGES # # 2000/04/22 (0.10): # - Restructured host name and address caches. Hosts are now cached using # packed addresses as keys. Conversion to IPv6 should be simple now. # - Added paranoid hostname lookups. # - Added netmask qualifications for address arguments. # - Tweaked usage info. # 2000/04/20: # - Added parsing and tracking of TCP and state flags. # 2000/04/12 (0.9): # - Wasn't handling underscore in hostname,servicename fields; these may be # logged using ipmon -n. Observation by . # - Hadn't properly attributed observation and fix for repetition counter in # 0.8 change log. Added John Ladwig to attribution. Thanks, John. # # 2000/04/10 (0.8): # - Service names can also have hyphens, dummy. I wasn't allowing these # either. Observation and fix thanks to Taso N. Devetzis # . # - IP Filter now logs a repetition counter. Observation and fixes (changed # slightly) from Andy Kreiling and John Ladwig # . # - Added fix to handle new Solaris log format, e.g.: # 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 # Fix thanks to Taso N. Devetzis . # - Added services map option. # - Added options for generating only source/destination tables. # - Added verbosity option. # - Added option for reporting traffic for specific hosts. # - Added some more ICMP unreachable codes, and made code and type names # match the ones in IP Filter parse.c. # - Condensed output format somewhat. # - Various minor improvements, perhaps slight speed improvements. # - Documented new options in usage() and tried to improve wording. # # 1999/08/02 (0.7): # - Hostnames can have hyphens, dummy. I wasn't allowing them in the syslog # line. Fix from Antoine Verheijen . # # 1999/05/05 (0.6): # - IRIX syslog prefixes the hostname with a severity code. Handle it. Fix # from John Ladwig . # # 1999/05/05 (0.5): # - Protocols other than TCP, UDP, or ICMP have packet lengths reported in # parentheses for some reason. The script now handles this. Thanks to # Dispatcher . # - I had mixed up info-request and info-reply ICMP codes, and omitted the # traceroute code. Sorted this out. I had also missed code 0 for type 6 # (alternate address for host). Thanks to John Ladwig . # # 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 logged 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. Obviously, I don't # favor using ipmon -n. 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 had moved the site and # forgotten 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; use IO::File; select STDOUT; $| = 1; my %hosts; my $me = $0; $me =~ s/^.*\///; # 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', ); # Map of ICMP types and their relevant codes. my %icmpTypeMap = ( 0 => +{ name => 'echorep', codes => +{0 => undef}, }, 3 => +{ name => 'unreach', codes => +{ 0 => 'net-unr', 1 => 'host-unr', 2 => 'proto-unr', 3 => 'port-unr', 4 => 'needfrag', 5 => 'srcfail', 6 => 'net-unk', 7 => 'host-unk', 8 => 'isolate', 9 => 'net-prohib', 10 => 'host-prohib', 11 => 'net-tos', 12 => 'host-tos', 13 => 'filter-prohib', 14 => 'host-preced', 15 => 'preced-cutoff', }, }, 4 => +{ name => 'squench', codes => +{0 => undef}, }, 5 => +{ name => 'redir', codes => +{ 0 => 'net', 1 => 'host', 2 => 'tos', 3 => 'tos-host', }, }, 6 => +{ name => 'alt-host-addr', codes => +{ 0 => 'alt-addr' }, }, 8 => +{ name => 'echo', codes => +{0 => undef}, }, 9 => +{ name => 'routerad', codes => +{0 => undef}, }, 10 => +{ name => 'routersol', codes => +{0 => undef}, }, 11 => +{ name => 'timex', codes => +{ 0 => 'in-transit', 1 => 'frag-assy', }, }, 12 => +{ name => 'paramprob', codes => +{ 0 => 'ptr-err', 1 => 'miss-opt', 2 => 'bad-len', }, }, 13 => +{ name => 'timest', codes => +{0 => undef}, }, 14 => +{ name => 'timestrep', codes => +{0 => undef}, }, 15 => +{ name => 'inforeq', codes => +{0 => undef}, }, 16 => +{ name => 'inforep', codes => +{0 => undef}, }, 17 => +{ name => 'maskreq', codes => +{0 => undef}, }, 18 => +{ name => 'maskrep', codes => +{0 => undef}, }, 30 => +{ name => 'tracert', codes => +{ }, }, 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 => +{ }, }, ); # Arguments we will parse from argument list. my $numeric = 0; # Don't lookup hostnames. my $paranoid = 0; # Do paranoid hostname lookups. my $verbosity = 0; # Bla' bla' bla'. my $sTable = 0; # Generate source table. my $dTable = 0; # Generate destination table. my @services = (); # Preload services tables. my $showFlags = 0; # Show TCP flag combinations. my %selectAddrs; # Limit report to these hosts. my %selectActs; # Limit report to these actions. # Parse argument list. while (defined ($_ = shift)) { if (s/^-//) { while (s/^([vnpSD\?hsAF])//) { my $flag = $1; if ($flag eq 'v') { ++$verbosity; } elsif ($flag eq 'n') { $numeric = 1; } elsif ($flag eq 'p') { $paranoid = 1; } elsif ($flag eq 'S') { $sTable = 1; } elsif ($flag eq 'D') { $dTable = 1; } elsif ($flag eq 'F') { $showFlags = 1; } elsif (($flag eq '?') || ($flag eq 'h')) { &usage (0); } else { my $arg = shift; defined ($arg) || &usage (1, qq{-$flag requires an argument}); if ($flag eq 's') { push (@services, $arg); } elsif ($flag eq 'A') { my @acts = split (/,/, $arg); my $a; foreach $a (@acts) { my $aa; my $match = 0; foreach $aa (keys (%acts)) { if ($acts{$aa} eq $a) { ++$match; $selectActs{$aa} = $a; } } $match || &usage (1, qq{unknown action $a}); } } } } &usage (1, qq{unknown option: -$_}) if (length); next; } # Add host to hash of hosts we're interested in. (/^(.+)\/([\d+\.]+)$/) || (/^(.+)$/) || &usage (1, qq{invalid CIDR address $_}); my ($addr, $mask) = ($1, $2); my @addr = &hostAddrs ($addr); (scalar (@addr)) || &usage (1, qq{cannot resolve hostname $_}); if (!defined ($mask)) { $mask = (2 ** 32) - 1; } elsif (($mask =~ /^\d+$/) && ($mask <= 32)) { $mask = (2 ** 32) - 1 - ((2 ** (32 - $mask)) - 1); } elsif (defined ($mask = &isDottedAddr ($mask))) { $mask = &integerAddr ($mask); } else { &usage (1, qq{invalid CIDR address $_}); } foreach $addr (@addr) { # Save mask unless we already have a less specific one for this address. my $a = &integerAddr ($addr) & $mask; $selectAddrs{$a} = $mask unless (exists ($selectAddrs{$a}) && ($selectAddrs{$a} < $mask)); } } # Which tables will we generate? $dTable = $sTable = 1 unless ($dTable || $sTable); my @dirs; push (@dirs, 'd') if ($dTable); push (@dirs, 's') if ($sTable); # Are we interested in specific hosts? my $selectAddrs = scalar (keys (%selectAddrs)); # Are we interested in specific actions? if (scalar (keys (%selectActs)) == 0) { %selectActs = %acts; } # We use this hash to cache port name -> number and number -> name mappings. # Isn't it cool that we can use the same hash for both? my %pn; # Preload any services maps. my $sm; foreach $sm (@services) { my $sf = new IO::File ($sm, "r"); defined ($sf) || &quit (1, qq{cannot open services file $sm}); while (defined ($_ = $sf->getline ())) { my $text = $_; chomp; s/#.*$//; s/\s+$//; next unless (length); my ($name, $spec, @aliases) = split (/\s+/); ($spec =~ /^([\w\-]+)\/([\w\-]+)$/) || &quit (1, qq{$sm:$.: invalid definition: $text}); my ($pnum, $proto) = ($1, $2); # Enter service definition in pn hash both forwards and backwards. my $port; my $pname; foreach $port ($name, @aliases) { $pname = "$pnum/$proto"; $pn{$pname} = $port; } $pname = "$name/$proto"; $pn{$pname} = $pnum; } $sf->close (); } # Cache for host name -> addr mappings. my %ipAddr; # Cache for host addr -> name mappings. my %ipName; # Hash for protocol number <--> name mappings. my %pr; # Under IPv4 port numbers are unsigned shorts. The value below is higher # than the maximum value of an unsigned short, 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; while () { chomp; # For ipmon output that came through syslog, we'll have an asctime # timestamp, an optional severity code (IRIX), the 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 ($log); 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+//) { $log = $_; } elsif (s/^(?:\d+\/\d+\/\d+)\s+(?:\d+:\d+:\d+\.\d+)\s+//) { $log = $_; } else { # It don't look like no ipmon output to me, baby. next; } next unless (defined ($log)); print STDERR "$log\n" if ($verbosity); # 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 (which will be in parentheses for protocols other than # TCP, UDP, or ICMP), and maybe some additional info. 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); unless (scalar (@fields)) { print STDERR "$me:$.: cannot parse: $_\n"; next; } my ($count, $if, $group, $rule, $act, $src, $dest, $proto, $hlen, $len, $more) = @fields; # Skip actions we're not interested in. next unless (exists ($selectActs{$act})); # Packet count defaults to 1. $count = 1 unless (defined ($count)); my ($sport, $dport, @flags); if ($proto eq 'icmp') { if ($more =~ s/^icmp (\d+)\/(\d+)\s*//) { # We save icmp type and code in both sport and dport. This # allows us to sort icmp packets using the normal port-sorting # code. $dport = $sport = "$1.$2"; } else { $sport = ''; $dport = ''; } } else { if ($showFlags) { if (($proto eq 'tcp') && ($more =~ s/^\-([A-Z]+)\s*//)) { push (@flags, $1); } if ($more =~ s/^K\-S\s*//) { push (@flags, 'state'); } } if ($src =~ s/,([\-\w]+)$//) { $sport = &portSimplify ($1, $proto); } else { $sport = ''; } if ($dest =~ s/,([\-\w]+)$//) { $dport = &portSimplify ($1, $proto); } else { $dport = ''; } } # Make sure addresses are numeric at this point. We want to sort by # IP address later. If the hostname doesn't resolve, punt. If you # must use ipmon -n, be ready for weirdness. Use only the first # address returned. my $x; $x = (&hostAddrs ($src))[0]; unless (defined ($x)) { print STDERR "$me:$.: cannot resolve hostname $src\n"; next; } $src = $x; $x = (&hostAddrs ($dest))[0]; unless (defined ($x)) { print STDERR "$me:$.: cannot resolve hostname $dest\n"; next; } $dest = $x; # Skip hosts we're not interested in. if ($selectAddrs) { my ($a, $m); my $s = &integerAddr ($src); my $d = &integerAddr ($dest); my $cute = 0; while (($a, $m) = each (%selectAddrs)) { if ((($s & $m) == $a) || (($d & $m) == $a)) { $cute = 1; last; } } next unless ($cute); } # Convert proto to proto number. $proto = &protoNumber ($proto); sub countPacket { my ($host, $dir, $peer, $proto, $count, $packet, @flags) = @_; # Make sure host is in the hosts hash. $hosts{$host} = +{ 'd' => +{ }, 's' => +{ }, } unless (exists ($hosts{$host})); # Get the source/destination 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 and mark flags. my $packetHash = $protoHash->{$packet}; $packetHash->{''} += $count; map { $packetHash->{$_} = undef; } (@flags); } # Count the packet as outgoing traffic from the source address. &countPacket ($src, 's', $dest, $proto, $count, "$sport:$dport:$if:$act", @flags) if ($sTable); # Count the packet as incoming traffic to the destination address. &countPacket ($dest, 'd', $src, $proto, $count, "$dport:$sport:$if:$act", @flags) if ($dTable); } my $dir; foreach $dir (@dirs) { my $order = ($dir eq 's' ? 'source' : 'destination'); my $arrow = ($dir eq 's' ? '->' : '<-'); print "###\n"; print "### Traffic by $order address:\n"; print "###\n"; sub ipSort { &integerAddr ($a) <=> &integerAddr ($b); } sub packetSort { my ($asport, $adport, $aif, $aact) = split (/:/, $a); my ($bsport, $bdport, $bif, $bact) = split (/:/, $b); $bact cmp $aact || $aif cmp $bif || $asport <=> $bsport || $adport <=> $bdport; } 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 &dottedAddr ($host), "\n"; } else { print &hostName ($host), " \[", &dottedAddr ($host), "\]\n"; } my $peer; foreach $peer (sort ipSort (keys %{$traffic})) { my $peerHash = $traffic->{$peer}; my $peerName = ($numeric ? &dottedAddr ($peer) : &hostName ($peer)); my $proto; foreach $proto (sort (keys (%{$peerHash}))) { my $protoHash = $peerHash->{$proto}; my $protoName = &protoName ($proto); my $packet; foreach $packet (sort packetSort (keys %{$protoHash})) { my ($sport, $dport, $if, $act) = split (/:/, $packet); my $packetHash = $protoHash->{$packet}; my $count = $packetHash->{''}; $act = '?' unless (defined ($act = $acts{$act})); if (($protoName eq 'tcp') || ($protoName eq 'udp')) { printf (" %-6s %7s %4d %4s %16s %2s %s.%s", $if, $act, $count, $protoName, &portName ($sport, $protoName), $arrow, $peerName, &portName ($dport, $protoName)); } elsif ($protoName eq 'icmp') { printf (" %-6s %7s %4d %4s %16s %2s %s", $if, $act, $count, $protoName, &icmpType ($sport), $arrow, $peerName); } else { printf (" %-6s %7s %4d %4s %16s %2s %s", $if, $act, $count, $protoName, '', $arrow, $peerName); } if ($showFlags) { my @flags = sort (keys (%{$packetHash})); if (scalar (@flags)) { shift (@flags); print ' (', join (',', @flags), ')' if (scalar (@flags)); } } print "\n"; } } } } print "\n"; } exit (0); # 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; } # Translates a numeric address into a hostname. Pass only packed numeric # addresses to this routine. sub hostName { my $ip = shift; return $ipName{$ip} if (exists ($ipName{$ip})); # Do an inverse lookup on the address. my $name = gethostbyaddr ($ip, AF_INET); unless (defined ($name)) { # Inverse lookup failed, so map the IP address to its dotted # representation and cache that. $ipName{$ip} = &dottedAddr ($ip); return $ipName{$ip}; } # For paranoid hostname lookups. if ($paranoid) { # If this address already matches, we're happy. unless (exists ($ipName{$ip}) && (lc ($ipName{$ip}) eq lc ($name))) { # Do a forward lookup on the resulting name. my @addr = &hostAddrs ($name); my $match = 0; # Cache the forward lookup results for future inverse lookups, # but don't stomp on inverses we've already cached, even if they # are questionable. We want to generate consistent output, and # the cache is growing incrementally. foreach (@addr) { $ipName{$_} = $name unless (exists ($ipName{$_})); $match = 1 if ($_ eq $ip); } # Was this one of the addresses? If not, tack on a ?. $name .= '?' unless ($match); } } else { # Just believe it and cache it. $ipName{$ip} = $name; } return $name; } # Translates a hostname or dotted address into a list of packed numeric # addresses. sub hostAddrs { my $name = shift; my $ip; # Check if it's a dotted representation. return ($ip) if (defined ($ip = &isDottedAddr ($name))); # Return result from cache. $name = lc ($name); return @{$ipAddr{$name}} if (exists ($ipAddr{$name})); # Look up the addresses. my @addr = gethostbyname ($name); splice (@addr, 0, 4); unless (scalar (@addr)) { # Again, I don't think we need to recover from this gracefully. # 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. The # functions that get undef back should treat it as an error or # as some default address, e.g. 0 just to make things work. return (); } $ipAddr{$name} = [ @addr ]; return @{$ipAddr{$name}}; } # If the argument is a valid dotted address, returns the corresponding # packed numeric address, otherwise returns undef. sub isDottedAddr { my $addr = shift; if ($addr =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) { my @a = (int ($1), int ($2), int ($3), int ($4)); foreach (@a) { return undef if ($_ >= 256); } return pack ('C*', @a); } return undef; } # Unpacks a packed numeric address and returns an integer representation. sub integerAddr { my $addr = shift; return unpack ('N', $addr); # The following is for generalized IPv4/IPv6 stuff. For now, it's a # lot faster to assume IPv4. my @a = unpack ('C*', $addr); my $a = 0; while (scalar (@a)) { $a = ($a << 8) | shift (@a); } return $a; } # Unpacks a packed numeric address into a dotted representation. sub dottedAddr { my $addr = shift; my @a = unpack ('C*', $addr); return join ('.', @a); } # 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 $typeCode = shift; my ($type, $code) = split ('\.', $typeCode); return "?" unless (defined ($code)); my $info = $icmpTypeMap{$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 quit { my $ec = shift; my $msg = shift; print STDERR "$me: $msg\n"; exit ($ec); } sub usage { my $ec = shift; my @msg = @_; if (scalar (@msg)) { print STDERR "$me: ", join ("\n", @msg), "\n\n"; } print <