Paul lectures at the Institute of Technology, Carlow, in Ireland, and can be reached at [email protected]. His web site is http:// glasnost.itcarlow.ie/~barryp/.
In the July 2004 issue of TPJ, Robert Casey wrote an excellent article on using the Net::Pcap module to capture and process network traffic. Robert assumed that the reader was familiar with the inner workings of libpcap, the C library to which Net::Pcap provides a Perl interface. The API provided is very C-like, as Net::Pcap was purposely designed to match libpcap's C API call-for-call and, as a result, is very complex. Although this gives the Perl programmer total control over what's going on, the requirement to work at the "C level" can often make things harder than they need to be. And trust me, learning the inner workings of libpcapdocumented in the pcap(3) and Net::Pcap man pagesis not for the faint hearted.
Is There Something Easier than Net::Pcap?
With this in mind, Tim Potter, the author of Net::Pcap, produced the companion module Net::PcapUtils to encapsulate most of the Net::Pcap functionality, concentrating on providing a set of default initialization values. Only three functions make up the entire Net::PcapUtils API, as opposed to the more than 15 provided by Net::Pcap. In this article, I'll rewrite Robert's code to use Net::PcapUtils instead of Net::Pcap. In doing so, I'll use just two of the former module's functions. What's gained is a program that is smaller and, one hopes, easier to understand and maintain. What's lost is the ability to minutely customize the initialization of the packet-capturing environment. By taking advantage of Net::PcapUtils default initialization values, we can concentrate on processing the captured packets as opposed to worrying about specific initialization details.
Rewriting Robert's Analyzer with Net::PcapUtils
Rather than use the Net::Pcap module at the top of the program, my code (in Listing 1) brings in Net::PcapUtils instead, together with the other required modules:
use Net::PcapUtils; use NetPacket::Ethernet; use NetPacket::IP; use NetPacket::TCP;
As with Robert's code, the NetPacket::* modulesalso by Tim Potterare used to decode chunks of network traffic as they are captured. After setting a Boolean constant, I define two packet-capturing constant values:
use constant CAPTURE_FILTER => '(dst 127.0.0.1) && (tcp[13] & 2 != 0)'; use constant CAPTURE_DEVICE => 'eth0';
The first, CAPTURE_FILTER, holds the filter string to use when deciding when to process a captured packet. This string is identical to that used by Robert and it filters on TCP segments that have the SYN flag set. Be sure to change the 127.0.0.1 address to the IP address of the interface you want to capture traffic on. The CAPTURE_DEVICE constant identifies the network card to use when capturing and is set to eth0. Robert's code was able to work this value out for itself, but as eth0 is the default network device on most machines, it's quicker and easier to hard-code this value into the program. If you later want to capture on eth1 instead, simply change this constant. In fact, we need not specify eth0 at all, as Net::PcapUtils uses it by default. However, I find the extra effort required to define the constant to be worthwhile. Changing the filter is neater now, too, as constant values are easily identified at the top of the source code.
The just-defined constant values are used on the very next piece of code, which calls the Net::PcapUtils open function to prepare the network card to capture traffic:
my $pkt_descriptor = Net::PcapUtils::open( FILTER => CAPTURE_FILTER, DEV => CAPTURE_DEVICE );
The open function does a number of things for us. The most useful is that it automatically puts the identified network card into promiscuous mode, which saves us the trouble. If the call to open succeeds, a reference to a valid "packet descriptor" is returned to our code. If the call fails, the reference is undefined. My code checks the status of the packet descriptor and exits with an appropriate message if something goes wrong. Typically, the code in this if statement fires if an attempt is made to put the network card in promiscuous mode while running as a regular user on Linux/UNIX systems. Only root can put the network card into promiscuous mode:
if ( !ref( $pkt_descriptor ) ) { print "Net::PcapUtils::open returned: $pkt_descriptor\n"; exit; }
With a valid packet descriptor created, my code enters an infinite loop, which captures and processes packets:
while( TRUE ) { my ( $packet, %header ) = Net::PcapUtils::next( $pkt_descriptor ); syn_packets( $packet ); }
The next function from the Net::PcapUtils module blocks, waiting for a packet to arrive on the network interface associated with the packet descriptor. When one does, next returns the entire packet (as a scalar), as well as its header information (as a hash). The entire packet is sent to the syn_packets subroutine for processing, which differs only slightly from that provided by Robert (in how it processes its parameters).
In my view, my program is more Perlish than that written by Robert, even though both programs do roughly the same thing. I hope it is easier to understand, maintain, and extend.
Building Another Analyzer
Writing filter specifications, as defined in the CAPTURE_FILTER constant in Listing 1 (and borrowed from Robert's code), requires a little bit of research and learning. Fortunately, the tcpdump(8) man page has all the details. To continue to keep things simple, here's a straightforward filter specification that captures all TCP traffic on port 8080:
( tcp port 8080 )
In Listing 2, this filter is used to create a simple analyzer that captures and displays any and all traffic that matches the specification. Perhaps you've implemented a custom web server or application that communicates on port 8080, and you need to debug (or postprocess) the traffic generated. As this version of the analyzer isn't interested in displaying the to and from IP address information (unlike Robert's code), we can dispense with all of the decoding when the captured packet is processed. Instead, we simply strip away any header information associated with lower level protocolsIP and Ethernet, in this caseand decode what's left as a chunk as TCP traffic. Here's how to do this:
sub process_packet { my $packet = shift; my $tcp = NetPacket::TCP->decode( ip_strip ( eth_strip ( $packet ) ) ); print $tcp->{ data }; }
Note how, in this program, what was called syn_packet has been changed to the more generic process_packet. The eth_strip and ip_strip functions come from their respective NetPacket modules. These are imported into the code using the predefined export tags as follows:
use NetPacket::Ethernet qw( :strip ); use NetPacket::IP qw( :strip );
When next returns a captured packet, it returns only the first 100 bytes captured, which is a Net::PcapUtils default. This is enough packet data if all you are interested in is the header data; for instance, IP addresses, protocol ports, or Ethernet types. However, if what you are interested in is the actual data sentas well as the protocol-generated datathen you need to tell Net::PcapUtils to return more of the captured packet. In Listing 2, I define another constant, CAPTURE_AMOUNT, and set it to the value of 1500, which is the largest chunk of traffic that an Ethernet network interface can transmit at any one time:
use constant CAPTURE_AMOUNT => 1500;
This constant is then used as part of the call to open and sets the value for the SNAPLEN parameter:
my $pkt_descriptor = Net::PcapUtils::open( FILTER => CAPTURE_FILTER, DEV => CAPTURE_DEVICE, SNAPLEN => CAPTURE_AMOUNT );
When the program in Listing 2 is executed (as root, you'll recall), all of the output generated by the print statement can be piped to a file for later perusal at your leisure. The program in Listing 2 can be made even more generic by adding some simple command-line processing to set the values for the constants using the standard modules Getopt::Std, Getopt::Long, or good ol' shift. Doing so is left as an exercise for the keen reader.
Learning More
For more on Net::PcapUtils and packet capturing, refer to Chapter 2 of my first book Programming the Network with Perl; and if you want to use this technology to decode traffic for a protocol not covered by the NetPacket::* modules (such as DNS), see issue 0.6 of The Perl Review. And be sure to check out Robert's original articleit's a great read.
References
The tcpdump(8) man page.
Casey, Robert. "Monitoring Network Traffic with Net::Pcap," TPJ, July 2004.
Barry, Paul. "Who's Doing What? Analyzing Ethernet LAN Traffic," The Perl Review, Volume 0, Issue 6, November 2002.
Barry, Paul. Programming the Network with Perl, Wiley, 2002, ISBN 0471486701.
TPJ
#! /usr/bin/perl -w use strict; # Use the correct set of modules. use Net::PcapUtils; use NetPacket::Ethernet; use NetPacket::IP; use NetPacket::TCP; use constant TRUE => 1; use constant CAPTURE_FILTER => '(dst 127.0.0.1) && (tcp[13] & 2 != 0)'; use constant CAPTURE_DEVICE => 'eth0'; # Open the network card for capturing. my $pkt_descriptor = Net::PcapUtils::open( FILTER => CAPTURE_FILTER, DEV => CAPTURE_DEVICE ); # Check that the card "opened" OK. if ( !ref( $pkt_descriptor ) ) { print "Net::PcapUtils::open returned: $pkt_descriptor\n"; exit; } while( TRUE ) # i.e., forever, or until "killed" ... { # Capture a packet from the network card. my ( $packet, %header ) = Net::PcapUtils::next( $pkt_descriptor ); # Process the captured packet. syn_packets( $packet ); } sub syn_packets { my $packet = shift; # Strip Ethernet encapsulation of captured packet. my $ether_data = NetPacket::Ethernet::strip($packet); # Decode contents of TCP/IP packet contained within # captured Ethernet packet. my $ip = NetPacket::IP->decode($ether_data); my $tcp = NetPacket::TCP->decode($ip->{'data'}); # Print all out where its coming from and where its # going to! print $ip->{'src_ip'}, ":", $tcp->{'src_port'}, " -> ", $ip->{'dest_ip'}, ":", $tcp->{'dest_port'}, "\n"; }Back to article
Listing 2
#! /usr/bin/perl -w use Net::PcapUtils; use NetPacket::Ethernet qw( :strip ); use NetPacket::IP qw( :strip ); use NetPacket::TCP; use strict; use constant TRUE => 1; use constant CAPTURE_FILTER => '(tcp port 8080)'; use constant CAPTURE_DEVICE => 'eth0'; use constant CAPTURE_AMOUNT => 1500; my $pkt_descriptor = Net::PcapUtils::open( FILTER => CAPTURE_FILTER, DEV => CAPTURE_DEVICE, SNAPLEN => CAPTURE_AMOUNT ); if ( !ref( $pkt_descriptor ) ) { print "Net::PcapUtils::open returned: $pkt_descriptor\n"; exit; } while( TRUE ) { my ( $packet, %header ) = Net::PcapUtils::next( $pkt_descriptor ); process_packet( $packet ); } sub process_packet { my $packet = shift; my $tcp = NetPacket::TCP->decode( ip_strip ( eth_strip ( $packet ) ) ); print $tcp->{ data }; }Back to article