Sep 2008
libpcap Part 2In Part 1 of the series on using libpcap a very simple packet reader (or sniffer was written. Noted at the end of the first text are a few items that need to be taken care of:
In this the second part of the series; all of the above will be addressed to produce a good first draft program for reading network packets using libpcap.
IP addresses should have no holes
(or zeroes) in the
first 13 bits. Checking the first 13 bits is easy; just use a mask.
To make life even easier in the program do not print out data
unless it passes a mask check:
...
len = ntohs(ip->ip_len); /* get packer length */
version = IP_V(ip); /* get ip version */
off = ntohs(ip->ip_off);
if ((off & 0x1fff) == 0 ) { /* aka no 1's in first 13 bits */
fprintf(stdout,"ip: ");
fprintf(stdout,"%s:%u->%s:%u ",
inet_ntoa(ip->ip_src), tcp->th_sport,
inet_ntoa(ip->ip_dst), tcp->th_dport);
fprintf(stdout,
"tos %u len %u off %u ttl %u prot %u cksum %u ",
ip->ip_tos, len, off, ip->ip_ttl,
ip->ip_p, ip->ip_sum);
fprintf(stdout,"seq %u ack %u win %u ",
tcp->th_seq, tcp->th_ack, tcp->th_win);
fprintf(stdout,"%s", payload);
printf("\n");
}
...
The header length is a good pre-check of the packet length; it is known if the header length is wrong and alarm should be thrown (but the program does not need to exit). First add a header length variable to the ip handler, capture it then compare it against 5 (6 actually - but 0-5):
...
u_int hlen, off, version; /* header length, offset, version */
...
hlen = IP_HL(ip); /* get header length */
...
if (hlen < 5 ) {
fprintf(stderr,"Alert: %s bad header length %d\n",
inet_ntoa(ip->ip);
}
...
Checking the header is a first good pass, however, the entire
packet needs to be checked as well. The program already has the
variables needed in the ip handler: length and
len making the check straightforward (note it is
placed immediately after the IP header length check):
...
if (length < len)
fprintf(stderr,"Alert: %s truncated %d bytes missing.\n");
...
The last check is the ethernet header length. Again, the data is
already present in the form of the variable caplen, it
simply needs to be checked. The check should be the first the
operation in the ethernet handler:
...
if (caplen < 14) {
fprintf(stderr,"Packet length is less than header length\n");
return -1;
}
...
A -1 if there is an error, an error of this
magnitude is worth faulting on because it indicates data corruption
(although it does not indicate where).
With the few checks added it is now time to flesh out the program itself.
Libpcap provides an excellent high level interface to network
packets, so much so that the focus on features will not address API
wrappers or stubs because there really is no need for them; if one
wishes to integrate libpcap the library itself is good enough for
the task. Instead; adding flexibility to the program in the texts
for users makes sense. Before going any further,
getopt needs to be setup in the main()
function. Since it is known what we want to add:
The variables needed can be extrapolated less the
filter. The filter will be set using the exact same method that
tcpdump uses. The variables added to
main() are:
... char *oper; /* Filter or Operation */ int npkts; /* Number of polls */ char *dev; /* Device */ ...
The vflag and eflag are set global
to this file only at the top:
... static short int eflag; /* Ethernet flag */ static short int vflag; /* Verbosity flag */
Reasonable defaults need to be set; note that the polls are set
to -1 to loop forever:
... dev = NULL; eflag = 0; npkts = -1; oper = NULL; vflag = 3; ...
Set up the getopt bits in main():
...
while (1) {
static struct option long_options[] = {
{"ethernet", no_argument, 0, 'e'},
{"interface", required_argument, 0, 'i'},
{"polls", required_argument, 0, 'p'},
{"verbose", required_argument, 0, 'v'},
{0,0,0,0}
};
int option_index = 0;
c = getopt_long (argc, argv, "ei:p:v:",
long_options, &option_index);
if (c == -1)
break;
switch (c) {
case 'e':
eflag = 1;
break;
case 'i':
dev = optarg;
break;
case 'p':
npkts = atoi(optarg);
break;
case 'v':
vflag = atoi(optarg);
break;
default:
break;
}
}
...
The device will simply be passed along if it is specified and the pcap callouts will do their best to use it; no additional code beyond adding an option is needed.
Most programs have either a single verbosity flag or verbosity levels. The example program would most benefit from verbosity levels. For instance, what if the user does not wish to see the ethernet data? Ironically, because of the way callbacks are used, verbosity must be handled in the scope of the IP handler and ethernet handler separately. There are many ways to go about the task, the following is just one.
First the ethernet flag, does it exist
test:
...
if (eflag) {
fprintf(stdout,"eth: ");
fprintf(stdout,
"%s ",ether_ntoa((struct ether_addr*)eptr->ether_shost));
fprintf(stdout,
"%s ",ether_ntoa((struct ether_addr*)eptr->ether_dhost));
/* get type and use as the beginning of the message line */
if (ether_type == ETHERTYPE_IP) {
fprintf(stdout,"(ip)");
} else if (ether_type == ETHERTYPE_ARP) {
fprintf(stdout,"(arp)");
} else if (eptr->ether_type == ETHERTYPE_REVARP) {
fprintf(stdout,"(rarp)");
} else {
fprintf(stdout,"(?)");
}
}
...
Next, using varying levels, the TCPIP data, the greater the vflag the more data:
...
if (vflag > 3)
fprintf(stdout,"ip: ");
if (vflag > 0)
fprintf(stdout,"%s:%u->%s:%u ",
inet_ntoa(ip->ip_src), tcp->th_sport,
inet_ntoa(ip->ip_dst), tcp->th_dport);
if (vflag > 1)
fprintf(stdout,
"tos %u len %u off %u ttl %u prot %u cksum %u ",
ip->ip_tos, len, off, ip->ip_ttl,
ip->ip_p, ip->ip_sum);
if (vflag > 2)
fprintf(stdout,"seq %u ack %u win %u ",
tcp->th_seq, tcp->th_ack, tcp->th_win);
if (vflag > 3)
fprintf(stdout,"%s", payload);
if (vflag > 0)
printf("\n");
...
Passing filter options is a little more difficult. The problem posed is to tear off the rest of the input string once options are parsed:
program_name -i eth0 -p 1024 host fubuplus
Where host fubuplus
is the filter to be sent over to
libpcap. Luckily, tcpdump has the same syntax (ironically almost
identical) and a function to pick off a trailing argument
vector:
/*
* copy_argv - Copy the rest of an argument string into a new buffer for
* processing.
*/
char * copy_argv (char **argv)
{
char **p;
u_int len = 0;
char *buf;
char *src, *dst;
p = argv;
if (*p == 0)
return 0;
while (*p)
len += strlen(*p++) + 1;
buf = (char *)malloc(len);
if (buf == NULL) {
fprintf(stdout,"copy_argv: malloc");
exit (1);
}
p = argv;
dst = buf;
while ((src = *p++) != NULL) {
while ((*dst++ = *src++) != '\0')
;
dst[-1] = ' ';
}
dst[-1] = '\0';
return buf;
}
With the new argument vector copy in place, getting the filter is:
oper = copu_argv(oper);
The difficult
part is deciding whether or not there is a
filter then setting it up:
if (oper) {
if (pcap_compile(descr, &filter, oper, 0, net) == -1) {
errorlog(opmode, PACKAGE, "Error calling pcap_compile");
exit (1);
}
if (pcap_setfilter(descr, &filter)) {
errorlog(opmode, PACKAGE, "Error setting filter");
exit (1);
}
}
pcap_loop(descr, -1, pcap_callback, args); /* Loop pcap */
For polls the npkts variable is passed in
pcap_loop() instead of a -1. Recall that
npkts is defaulted to -1:
... pcap_loop(descr, npkts, pcap_callback, args); /* Loop pcap */ ...
Using libpcap is not as difficult as it seems once a fundamental
grasp of leveraging callbacks and where/when to deal with packet
data is ascertained. Most notably; libpcap is easily integrated
into existing softwares with minimal effort offering a powerful
solution for packet reading, recording and manipulation. In the
last part of the series will be the full source listing,
Makefile plus a little breaking out of code.