#!/usr/bin/perl
use strict;
use Data::Hexdumper;
use File::Basename;
use Getopt::Long        qw(:config no_auto_abbrev);
use Net::Pcap           qw(:functions);
use NetPacket::Ethernet qw(:types);
use NetPacket::IP       qw(:protos);
use NetPacket::TCP;
use Pod::Usage;
use Socket              qw(inet_ntoa);


$::PROGRAM = basename($0);
$::VERSION = "0.01";


# globals
my $dumper  = undef;

my %icmp = (
    ICMP_ECHO           => "echo",
    ICMP_ECHOREPLY      => "echo-reply",
    ICMP_IREQ           => "ireq",
    ICMP_IREQREPLY      => "ireq-reply",
    ICMP_MASREQ         => "mask",
    ICMP_MASKREPLY      => "mask-reply",
    ICMP_PARAMPROB      => "param-prob",
    ICMP_REDIRECT       => "redirect",
    ICMP_ROUTERADVERT   => "router-advert",
    ICMP_ROUTERSOLICIT  => "router-solicit",
    ICMP_SOURCEQUENCH   => "source-quench",
    ICMP_TIMXCEED       => "time-exceeded",
    ICMP_TSTAMP         => "timestamp",
    ICMP_TSTAMPREPLY    => "timestamp-reply",
    ICMP_UNREACH        => "unreachable",
);


MAIN: {
    run();
}


sub run {
    $|++;

    # get options
    my %options = (
        count   =>    10, 
        promisc =>     0, 
        snaplen =>   256, 
        timeout =>    10,
    );

    GetOptions(\%options, qw{
        help|h!  version|V!
        count|c=i  interface|i=s  promisc|p!  snaplen|s=i  writeto|w=s
    }) or pod2usage();

    pod2usage({ -verbose => 2, -exitval => 0 }) if $options{help};
    print "$::PROGRAM v$::VERSION\n" if $options{version};

    my ($err, $net, $mask, $filter);
    my $dev = $options{interface} || pcap_lookupdev(\$err);
    my $filter_str = join " ", @ARGV;

    # open the interface
    my $pcap = pcap_open_live($dev, @options{qw(snaplen promisc timeout)}, \$err)
        or die "fatal: can't open network device $dev: $err ",
               "(do you have the privileges?)\n";

    if ($filter_str) {
        # compile the filter
        pcap_compile($pcap, \$filter, $filter_str, 1, 0) == 0
            or die "fatal: filter error\n";
        pcap_setfilter($pcap, $filter);
    }

    if ($options{writeto}) {
        $dumper = pcap_dump_open($pcap, $options{writeto}) 
            or die "fatal: can't write to file '$options{writeto}': $!\n";
    }

    # print some information about the interface we're currently using
    pcap_lookupnet($dev, \$net, \$mask, \$err);
    print "listening on $dev (", dotquad($net), "/", dotquad($mask), ")", 
          ", capture size $options{snaplen} bytes";
    print ", filtering on $filter_str"  if $filter_str;
    print $/;

    # enter the main loop
    pcap_loop($pcap, $options{count}, \&process_packet, '');
    pcap_close($pcap);
}


sub process_packet {
    my ($user_data, $header, $packet) = @_;
    my ($proto, $payload, $src_ip, $src_port, $dest_ip, $dest_port, $flags);

    printf "packet: len=%s, caplen=%s, tv_sec=%s, tv_usec=%s\n", 
        map { $header->{$_} } qw(len caplen tv_sec tv_usec);

    # dump the packet if asked to do so
    pcap_dump($dumper, $header, $packet) if $dumper;

    # decode the Ethernet frame
    my $ethframe = NetPacket::Ethernet->decode($packet);

    if ($ethframe->{type} == ETH_TYPE_IP) {
        # decode the IP payload
        my $ipframe = NetPacket::IP->decode($ethframe->{data});
        $src_ip  = $ipframe->{src_ip};
        $dest_ip = $ipframe->{dest_ip};

        if ($ipframe->{proto} == IP_PROTO_ICMP) {
            my $icmpframe = NetPacket::ICMP->decode($ipframe->{data});
            $proto     = "ICMP";
            $payload   = $icmpframe->{data};
        }
        elsif ($ipframe->{proto} == IP_PROTO_TCP) {
            my $tcpframe = NetPacket::TCP->decode($ipframe->{data});
            $proto     = "TCP";
            $src_port  = $tcpframe->{src_port};
            $dest_port = $tcpframe->{dest_port};
            $payload   = $tcpframe->{data};
            $flags     = flags_of($tcpframe->{flags});
        }
        elsif ($ipframe->{proto} == IP_PROTO_UDP) {
            my $udpframe = NetPacket::UDP->decode($ipframe->{data});
            $proto     = "UDP";
            $src_port  = $udpframe->{src_port};
            $dest_port = $udpframe->{dest_port};
            $payload   = $udpframe->{data};
        }

        printf "IP:%s %s:%d -> %s:%d (%s)\n",
            $proto, $src_ip, $src_port, $dest_ip, $dest_port, $flags;
        print hexdump(data => $payload, start_position => 0) if length $payload;
        print $/;
    }
}


sub flags_of {
    my ($flags) = @_;
    my @strarr = ();
    push @strarr, "urg" if $flags & URG;
    push @strarr, "ack" if $flags & ACK;
    push @strarr, "psh" if $flags & PSH;
    push @strarr, "fin" if $flags & FIN;
    push @strarr, "syn" if $flags & SYN;
    push @strarr, "rst" if $flags & RST;
    push @strarr, "ece" if $flags & ECE;
    push @strarr, "cwr" if $flags & CWR;
    return join ",", @strarr
}


sub dotquad {
    return inet_ntoa( pack("I", $_[0]) )
}


__END__

=head1 NAME

pcapdump - Dump packets from the network

=head1 SYNOPSIS

    pcapdump [-c count] [-i interface] [-s snaplen] [-w file] [expression]

    pcapdump --help
    pcapdump --version

=head1 OPTIONS

=over

=item B<-c>, B<--count>  I<N>

Exit after receiving I<N> packets.

=item B<-i>, B<--interface>  I<device>

Listen on the specified interface. If unspecified, the program will use 
the interface returned by C<pcap_lookupdev()>.

=item B<-s>, B<--snaplen>  I<L>

Capture I<L> bytes of data for each packet. Defaults to 256.

=item B<-w>, B<--writeto>  I<file>

=back


=head1 DESCRIPTION

B<pcapdump> mimics the very basic features of B<tcpdump(1)> and provides
a good example of how to use C<Net::Pcap>.


=head1 AUTHOR

SE<eacute>bastien Aperghis-Tramoni, E<lt>sebastien@aperghis.netE<gt>

=head1 COPYRIGHT

Copyright (C) 2005, 2006, 2007, 2008, 2009 SE<eacute>bastien Aperghis-Tramoni.
All rights reserved.

This program is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut
