Sep 2008

Packet Reading with libpcap Part 2

In 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:

  • Nowhere is there bit continuity checking for the address portion - this can cause invalid data to be printed and not acted upon.
  • A few checks are missing such as length checks, type checks etc.
  • The example code is far too restrictive; the caller (whether a user or another program) should be able to specify a variety of options such as which interface, a filter, verbosity etc.

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.

Simple Checks

13 Bit Continuity

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");
        }
        ...

IP Header Length

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);
  }
...

IP Packet Truncation

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");
...

Ether Header Length

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.

Adding Operations

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:

  • verbosity and ethernet flags
  • filter setting
  • polls and device

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.

Setting Verbosity

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 Filters

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 */

Polls

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 */
...

Summary & Next Time

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.

prev - next