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.
(based on last 2 months log reports)