Network programming in Linux

Getting started with libpcap


Introduction
Getting started with libpcap

Extracting Ethernet information
Internet Protocol (IP)

Filtering captured datagrams
Capturing datagrams offline

Address Resolution Protocol (ARP)
Internet Control Message Protocol (ICMP)
Transmission Control Protocol (TCP)
User Datagram Protocol (UDP)
Trivial File Transfer Protocol (TFTP)

Injecting datagrams with libnet
Implementing ping
Implementing traceroute

Download source code


The structure of a libpcap application

A libpcap capture session is a simple process consisting of five basic steps, one of which (step 3) is optional:

  1. We first select an interface to listen to, such as eth0 or wlan0. The interface may be explicitly specified or we may let libpcap select an appropriate one.

  2. We then initialize a libpcap session, where we tell it, among other things, whether to capture local traffic only (i.e. originating from or destined to the local computer) or all network traffic transiting through the interface.

  3. If we want to capture only specific network traffic, we must create, compile and activate a filtering rule set.

  4. We then initiate the sniffing process by entering the application's primary execution loop, where libpcap waits for traffic and execute a user-defined callback function for each datagram captured.

  5. Finally, we close the libpcap session once sniffing is completed.

In this section of the tutorial, we implement a simple sniffing application. In subsequent sections we elaborate on steps 3 and 4 of the capture process.

Selecting an interface

This first step is quite simple. Let's first look at source code selecting a network interface (commonly called a device) to sniff from.

sniff01.cpp
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
#include <iostream>

#include <cstring>     // memset
#include <cstdlib>     // exit
#include <unistd.h>    // getopt()
#include <signal.h>    // Ctrl+C handling

#include <pcap.h>      // libpcap

using namespace std;

// Ctrl+C interrupt handler
void bypass_sigint(int sig_no) {
  cout << endl << "*** Capture process interrupted by user..." << endl;

  exit(0); // we're done!
}

// First libpcap program: device selection
int main(int argc, char *argv[]) {
  char *device = NULL;             // device to sniff
  char argch;                      // to manage command line arguments
  char errbuf[PCAP_ERRBUF_SIZE];   // to handle libpcap error messages

  // Install Ctrl+C handler
  struct sigaction sa, osa;
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = &bypass_sigint;
  sigaction(SIGINT, &sa, &osa);

  // Process command line arguments
  while ((argch = getopt(argc, argv, "hd:")) != EOF)
    switch (argch) {
      case 'd':           // device name
        device = optarg;
        break;

      case 'h':           // show help info
        cout << "Usage: sniff [-d XXX -h]" << endl;
        cout << " -d XXX : device to capture from, where XXX is device name (ex: eth0)." << endl;
        cout << " -h : show this information." << endl;

        // Exit if only argument is -h
        if (argc == 2) return 0;
        break;
    }

  // Identify device to use
  if (device == NULL && (device = pcap_lookupdev(errbuf)) == NULL) {
    cerr << "error - " << errbuf << endl;
    return -2;
  }
  else
    cout << "device = " << device << endl;

  return 0;
}

This first program implements the first step of the libpcap capture session: selecting a network device from which traffic is to be captured. Before examining this code, let's compile and run it.

%root> g++ -o sniff01 sniff01.cpp -lpcap
%root> ./sniff01
device = eth0
%root>

The code is compiled with library libpcap (-lpcap) and the resulting binary, sniff01, is run privileged (i.e. as root); administrative privileges are required for the program to have access to network devices. As expected, the program finds the first available network device, eth0.

Now let's look at the source code:

  • Line #008 is mandatory to access the libpcap API.

  • Lines #026 to #029 install an interrupt handler (routine bypass_sigint()) to capture Ctrl+C keystrokes. The user will usually press Ctrl+C to end the capture process; this routine shuts down the application gracefully.

  • Line #032 uses the getopt() function to parse command-line arguments. The accompanying switch structure checks if a network device is explicitly specified by the user upon calling the program from a console.

  • Finally, line #049 uses the pcap_lookupdev() function to identify an available network device. If none is found (such as it is the case when the program runs without administrative privileges), the error message returned through the function call argument errbuf is displayed.

Initiating a libpcap session

The next source code extends sniff01.cpp to set up a libpcap session and identify the network attached to the selected device. To create a capture session we call pcap_open_live() for the selected network device.

sniff02.cpp
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
#include <iostream>

#include <cstring>     // memset
#include <cstdlib>     // exit
#include <unistd.h>    // getopt()
#include <signal.h>    // Ctrl+C handling
#include <arpa/inet.h> // struct in_addr

#include <pcap.h>      // libpcap

using namespace std;

pcap_t *pcap_session = NULL; // libpcap session handle

// Ctrl+C interrupt handler
void bypass_sigint(int sig_no) {
  cout << endl << "*** Capture process interrupted by user..." << endl;

  // Close libpcap session
  if (pcap_session != NULL)
    pcap_close(pcap_session);

  exit(0); // we're done!
}

// First libpcap program : device selection
int main(int argc, char *argv[]) {
  char *device = NULL;            // device to sniff
  char  argch;                    // to manage command line arguments
  char  errbuf[PCAP_ERRBUF_SIZE]; // to handle libpcap error messages
  int   siz     = 1518,           // max number of bytes captured for each datagram
        promisc = 0;              // deactive promiscuous mode

  // Install Ctrl+C handler
  struct sigaction sa, osa;
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = &bypass_sigint;
  sigaction(SIGINT, &sa, &osa);

  // Process command line arguments
  while ((argch = getopt(argc, argv, "hpd:")) != EOF)
    switch (argch) {
      case 'd':           // device name
        device = optarg;
        break;

      case 'h':           // show help info
        cout << "Usage: sniff [-d XXX -h]" << endl;
        cout << " -d XXX : device to capture from, where XXX is device name (ex: eth0)." << endl;
        cout << " -h : show this information." << endl;
        cout << " -p : activate promiscuous capture mode." << endl;

        // Exit if only argument is -h
        if (argc == 2) return 0;
        break;

      case 'p':           // active promiscuous mode
        promisc = 1;
        break;
    }

  // Identify device to use
  if (device == NULL && (device = pcap_lookupdev(errbuf)) == NULL) {
    cerr << "error - " << errbuf << endl;
    return -2;
  }
  else
    cout << "device = " << device << (promisc ? " (promiscuous)" : "") << endl;

  // Extract IP information for network connected to device
  bpf_u_int32 netp,  // ip address of network
              maskp; // network mask
  if ((pcap_lookupnet(device, &netp, &maskp, errbuf)) == -1) {
    cerr << "error - " << errbuf << endl;
    return -3;
  }

  // Translate ip address into textual form for display
  struct  in_addr addr;
  char   *net;
  addr.s_addr = netp;
  if ((net = inet_ntoa(addr)) == NULL)
    cerr << "error - inet_ntoa() failed" << endl;
  else
    cout << "network ip = " << net << endl;

  // Translate network mask into textual form for display
  char *mask;
  addr.s_addr = maskp;
  if ((mask = inet_ntoa(addr)) == NULL)
    cerr << "error - inet_ntoa() failed" << endl;
  else
    cout << "network mask = " << mask << endl;

  // Open a libpcap capture session
  pcap_session = pcap_open_live(device, siz, promisc, 1000, errbuf);
  if (pcap_session == NULL) {
    cerr << "error - pcap_open_live() failed (" << errbuf << ")" << endl;
    return -4;
  }

  // *** Capture code will be inserted here ***

  // Close libpcap session
  pcap_close(pcap_session);

  return 0;
}

Note in the above program (sniff02.cpp), all source code identical to the previous example (sniff01.cpp) is grayed out to emphasize added and/or modified lines of code.

  • Line #013 defines a handle to track a libpcap session. This handle is declared globally in order to be eventually manipulated in multiple routines.

  • Lines #020 and #021 release the libpcap session handle whenever the program's execution is stopped by pressing Ctrl+C.

  • Lines #041 and #057 to #059 implement a new command-line argument (-p) for enabling promiscuous mode on the selected network device. When promiscuous mode is disabled (the default), only network traffic originating from or destined to the local host will be captured. Enabling promiscuous mode in a non-switched environment (such as a hub, or a switch being ARP flooded) allows to capture all traffic transiting through the local network.

  • Lines #071 to #076 call pcap_lookupnet() to obtain the IP address and mask of the network attached to the device. This information is essential as it may be needed later on to apply specific filters. Lines #079 to #093 use inet_ntoa() (in library <arpa/inet.h>) to convert the addresses into dotted textual form for display.

  • Line #096 calls pcap_open_live() to attach a new libpcap session to the selected device, setting up the session to read the first siz bytes of each captured datagram and triggering promiscuous mode if promisc is true. The read timeout delay argument (1000 ms) is used by libpcap to prevent blocking indefinitely the execution of the program when there is no traffic; note however that the capture code we'll implement in the next section will ignore this timeout delay (read timeout delay is ignored by pcap_loop(), but not by the alternate pcap_dispatch()). If no session handle is obtained, such has it might be the case if the program is not run with root privileges, an error message is displayed and the execution ended.

  • Since sniff02.cpp does not capture datagrams yet, line #105 immediately releases the session handle, hereby closing the libpcap session.

Now we compile and execute the program with promiscuous mode enabled:

%root> g++ -o sniff02 sniff02.cpp -lpcap
%root> ./sniff02 -p
device = eth0 (promiscuous)
network ip = 172.16.179.0
network mask = 255.255.255.0
%root>

The program successfully creates a libpcap session (since no error message is displayed). Note that the network IP and its mask will vary from one network to another.

Capturing traffic

Having obtained an active libpcap session handle, we can now start capturing traffic. There are essentially two main techniques to capture datagrams:

  1. Capture a single datagram by calling pcap_next().
  2. Sequentially capture multiple datagrams with pcap_loop() or pcap_dispatch(), processing each one individually with a callback function.

The technique to select depends essentially on why we need to capture traffic. Capturing a single datagram with pcap_next() is usually done in conjunction with filtering (described later in this tutorial) to wait for a specific datagram, such as a response to an ICMP Echo Request. In most other situations, looping to process successive datagrams with a callback function is more appropriate.

The following program uses pcap_loop() to capture datagrams non stop until the user presses Ctrl+C. A new command-line argument is introduced (-n) to allow the user to specify a maximum number of datagrams to capture, after which the execution ends automatically.

sniff03.cpp
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <iostream>

#include <cstring>     // memset
#include <cstdlib>     // exit
#include <unistd.h>    // getopt()
#include <signal.h>    // Ctrl+C handling
#include <arpa/inet.h> // struct in_addr

#include <pcap.h>      // libpcap

using namespace std;

pcap_t *pcap_session = NULL; // libpcap session handle

// Ctrl+C interrupt handler
void bypass_sigint(int sig_no) {
  cout << endl << "*** Capture process interrupted by user..." << endl;

  // Close libpcap session
  if (pcap_session != NULL)
    pcap_close(pcap_session);

  exit(0); // we're done!
}

// Callback given to pcap_loop() for processing captured datagrams
void process_packet(u_char *user, const struct pcap_pkthdr * h, const u_char * packet) {
  cout << "Grabbed " << h->caplen << " bytes (" << static_cast<int>(100.0 * h->caplen / h->len) 
       << "%) of datagram received on " << ctime((const time_t*)&h->ts.tv_sec);
}

// First libpcap program : device selection
int main(int argc, char *argv[]) {
  char *device = NULL;            // device to sniff
  char  argch;                    // to manage command line arguments
  char  errbuf[PCAP_ERRBUF_SIZE]; // to handle libpcap error messages
  int   siz     = 1518,           // max number of bytes captured for each datagram
        promisc = 0,              // deactive promiscuous mode
        cnt     = -1;             // capture indefinitely

  // Install Ctrl+C handler
  struct sigaction sa, osa;
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = &bypass_sigint;
  sigaction(SIGINT, &sa, &osa);

  // Process command line arguments
  while ((argch = getopt(argc, argv, "hpd:n:")) != EOF)
    switch (argch) {
      case 'd':           // device name
        device = optarg;
        break;

      case 'h':           // show help info
        cout << "Usage: sniff [-d XXX -h]" << endl;
        cout << " -d XXX : device to capture from, where XXX is device name (ex: eth0)." << endl;
        cout << " -h : show this information." << endl;
        cout << " -n : number of datagrams to capture." << endl;
        cout << " -p : activate promiscuous capture mode." << endl;

        // Exit if only argument is -h
        if (argc == 2) return 0;
        break;

      case 'n':           // number of datagrams to capture
        cnt = atoi(optarg);
        break;
        
      case 'p':           // active promiscuous mode
        promisc = 1;
        break;
    }

  // Identify device to use
  if (device == NULL && (device = pcap_lookupdev(errbuf)) == NULL) {
    cerr << "error - " << errbuf << endl;
    return -2;
  }
  else
    cout << "device = " << device << (promisc ? " (promiscuous)" : "") << endl;

  // Extract IP information for network connected to device
  bpf_u_int32 netp,  // ip address of network
              maskp; // network mask
  if ((pcap_lookupnet(device, &netp, &maskp, errbuf)) == -1) {
    cerr << "error - " << errbuf << endl;
    return -3;
  }

  // Translate ip address into textual form for display
  struct  in_addr addr;
  char   *net;
  addr.s_addr = netp;
  if ((net = inet_ntoa(addr)) == NULL)
    cerr << "error - inet_ntoa() failed" << endl;
  else
    cout << "network ip = " << net << endl;

  // Translate network mask into textual form for display
  char *mask;
  addr.s_addr = maskp;
  if ((mask = inet_ntoa(addr)) == NULL)
    cerr << "error - inet_ntoa() failed" << endl;
  else
    cout << "network mask = " << mask << endl;

  // Open a libpcap capture session
  pcap_session = pcap_open_live(device, siz, promisc, 1000, errbuf);
  if (pcap_session == NULL) {
    cerr << "error - pcap_open_live() failed (" << errbuf << ")" << endl;
    return -4;
  }

  // Start capturing...
  pcap_loop(pcap_session, cnt, process_packet, NULL);

  // Close libpcap session
  pcap_close(pcap_session);

  return 0;
}

As previously, all source code in sniff03.cpp identical code in sniff02.cpp is grayed out to emphasize new code.

  • Lines #027 to #030 implement a callback function, process_packet(), to be executed for each datagram captured.

  • Line #039 adds a new variable (cnt) to control the maximum number of datagrams to capture; its default value (-1) disables this constraint so that the program captures datagrams until the user explicitly ends the execution. Lines #048 and #065 to #067 add a new command-line argument to change the value of cnt.

  • Line #115 initiates datagram capture by calling pcap_loop(), passing along the name of the callback function to execute for each datagram captured by libpcap.

Here is the prototype of pcap_loop():

int pcap_loop(pcap_t *, int, pcap_handler, u_char *)
The first argument is the libpcap session handle returned by pcap_open_live(). The second argument is the maximum number of datagrams to capture. The third argument is the name of the callback function to execute for each captured datagram. Finally, the last argument is not explicitly used by libpcap, but made available to the programmer for passing data from the main routine to the callback function; in sniff03.cpp, line #115 sets this last argument to NULL since it's not required.

Now let's have a closer look at the callback function, process_packet(). First of all, notice that no result (i.e. void) is returned by the function since control is not explicitly returned to the main routine once the datagram is processed, but instead relinquished to libpcap until the next datagram comes along. The first parameter (user) corresponds to the last argument provided by pcap_loop() (in our case, user will be NULL). The second parameter, h, is a libpcap header providing information on the captured datagram. The pcap_header structure, defined in <pcap.h> is defined as:

struct pcap_pkthdr {
  struct timeval ts;     // time stamp
  bpf_u_int32    caplen; // length of portion captured
  bpf_u_int32    len;    // length of original packet
};

These attributes are self explanatory: attribute ts is a timestamp indicating when the datagram was captured, attribute caplen gives the number of bytes captured (the maximum value in this attribute should correspond the second argument provided to pcap_open_live()), and attribute len gives the size of the whole datagram in bytes; obviously, caplenlen.

Finally, the last parameter of the callback function, packet, is a pointer to a block of bytes (of size caplen) containing the datagram captured. If caplen is less then len, then packet contains the first caplen bytes of the datagram.

Now, looking back to the callback function, we can now see that it displays the content of its pcap_header structure.When compiling and running sniff03.cpp to capture a few datagrams we get:

%root> g++ -o sniff03 sniff03.cpp -lpcap
%root> ./sniff03 -p
device = eth0 (promiscuous)
network ip = 172.16.179.0
network mask = 255.255.255.0
Grabbed 76 bytes (100%) of datagram received on Tue Jul  2 08:01:32 2013
Grabbed 108 bytes (100%) of datagram received on Tue Jul  2 08:01:32 2013
Grabbed 81 bytes (100%) of datagram received on Tue Jul  2 08:01:33 2013
Grabbed 76 bytes (100%) of datagram received on Tue Jul  2 08:01:33 2013
Grabbed 108 bytes (100%) of datagram received on Tue Jul  2 08:01:33 2013
Grabbed 101 bytes (100%) of datagram received on Tue Jul  2 08:01:34 2013
^C
*** Capture process interrupted by user...
%root>

Notice in the above execution that the maximum number of bytes to capture per datagram (1518, provided to pcap_open_live()) being larger than the size of all datagrams captured (ranging from 76 to 108 bytes), 100% of each datagram captured is available the callback function's packet parameter.

We are now able to capture datagrams! Now, let's make our sniffer a bit more useful by analyzing the captured information in the packet parameter of the process_packet() callback function. The next part of this tutorial implements the backbone of this datagram analysis code by extracting Ethernet frame information from the captured data.


Home  |  Previous  |  Next

 
Copyright © 2014 Marco Lavoie