Network programming in Linux

Injecting datagrams with libnet


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


While libpcap is a library designed for capturing datagrams from the local network, libnet is a library designed for injecting (i.e. sending) datagrams into the local network.

Like libpcap, libnet is a generic networking C API providing access to several protocols. It is not designed as a 'all in one' solution to networking. Currently many features that are common in some network protocols are not available with libnet, such as streaming via TCP/IP. Furthermore libnet is limited to the minimal needed to communicate datagrams in order to support multiple platforms such as Linux, DOS and Windows.

The libnet library hides much of the tedium of datagram creation from the programmer such as multiplexing, buffer management, header information, byte-ordering, OS-dependent issues, and much more. It features portable datagram creation interfaces from the application layer down to the link layer, as well as a host of supplementary and complementary functionality. Using libnet, quick and simple datagram assembly applications can be whipped up with minimal effort. When used in combination with libpcap, more complex programs can be written such as ping and traceroute.

Anatomy of a libnet session

Here are the main steps to inject datagrams into the local network:

  1. Create a new session by obtaining a libnet context with libnet_init().

  2. Build all necessary datagram headers, encapsulating them from the highest layer to the lowest. For example, say you want to send a TCP segment with full control over all headers, then you need to invoke in proper sequence (for adequate encapsulation) libnet_build_tcp(), libnet_build_ipv4() and libnet_build_ethernet().

  3. Once the datagram is properly built, it may be injected into the network with libnet_write().

  4. In order to inject the next datagram, either discard the previous one with libnet_clear_packet() and start building the next one from scratch, or simply make changes to the previous datagram.

  5. When done injecting datagrams, free the libnet context with libnet_destroy().

If the above logic is followed, sending datagrams over the local network connection is straightforward.

Creating and destroying a libnet session

So let's get down to it! We first write a simple program using libnet_init() to obtain a libnet session and libnet_destroy() to release it:

inject01.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
#include <iostream>

#include <libnet.h>       // libnet

using namespace std;

// First libnet program
int main(int argc, char *argv[]) {
  libnet_t *libnet_ctx;                  // libnet session context
  char      errbuf[LIBNET_ERRBUF_SIZE];  // buffer to get error messages

  // Validate command line call
  if (argc != 2) {
    cerr << "Usage: " << argv[0] << " <device>" << endl;
    return -1;
  }

  // Get libnet session context and validate 
  libnet_ctx = libnet_init(LIBNET_RAW4, argv[1], errbuf);
  if (libnet_ctx == NULL) {
    cerr << "error - libnet_init() failed (" << errbuf << ")" << endl;
    return -1; 
  }

  // Free libnet session context
  if (libnet_ctx) libnet_destroy(libnet_ctx);

  return 0;
}

Now we compile the program (specifying the libnet library with -lnet) and run it:

%root> g++ -o inject01 inject01.cpp -lnet
%root> ./inject01 eth0
%root>

Since no error message is displayed, everything went smooth!

Let's briefly look at the previous code:

  • Line #003 includes <libnet> to give access to the library API's definition.

  • Line #009 defines a variable to hold a libnet context, libnet_ctx.

  • Line #010 defines a buffer to hold whatever error message libnet produces. The required size of this buffer (LIBNET_ERRBUF_SIZE) is defined by the library.

  • Line #019 requests a libnet context associated with the network device provided through the command line argument. If no context is returned, an error message is displayed and execution stops.

  • Finally, the libnet context is released at line #026.

Note that this first program does not inject traffic yet; we'll get to that shortly. Let's first have a look at the function call to libnet_init(). Here is the function's prototype:

libnet_t * libnet_init(int injection_type, char *device, char *err_buf)

And here is a brief description of the required arguments:

  • injection_type indicates the lowest layer to explicitly encapsulate to, either from the link layer up or from the network layer up. The allowed values are LIBNET_RAW4 (IPv4 and above) and LIBNET_LINK (link layer and above).

  • device is the device’s name (as in eth0) or its IP address (as in 192.168.0.20). If device is set to NULL, libnet will try to find a suitable device automatically.

  • err_buf is a buffer to hold error messages if something goes wrong.

The function returns a pointer to the libnet context, or NULL of an error occurred.

Getting addresses

IP and MAC addresses in C are usually stored as integers: u_int32_t (unsigned 32 bits) for an IPv4 address and u_int8_t[6] array (6 unsigned bytes) for Ethernet. However, user input/output address entries are usually strings (e.g. "192.168.10.121" and "00:0A:23:F5:9B:02"). The following libnet functions help convert addresses from one form to the other:

char* libnet_addr2name4(u_int32_t in, u_int8_t use_name)

Converts the given integer IPv4 address into a string. If use_name is LIBNET_DONT_RESOLVE, the returned address is in dotted form (e.g. "74.125.131.147"), and if it's LIBNET_RESOLVE, the returned address is in DNS form (e.g. "google.com") if possible.

u_int32_t libnet_name2addr4(libnet_t *l, char *host_name, u_int8_t use_name)

Performs the opposite of libnet_addr2name4(): converting a string-based IPv4 address into integer form, with the ability to perform a DNS query if required.

u_int8_t* libnet_hex_aton(int8_t * s, int * len)

Converts a MAC address from string colon form (e.g. "00:0A:23:F5:9B:02") into integer form. The reverse conversion (i.e. from integer to string form) may be accomplished using sprintf() with "%02X" format for each byte.

Here is the inject01.cpp code enhanced to handle IPv4 addresses. We also use two libnet functions, libnet_get_ipaddr4() and libnet_get_hwaddr(), to get the local device's IPv4 and MAC addresses, as well as libnet_geterror() to get a description string whenever libnet fails. Grayed out code is identical to inject01.cpp:

inject02.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
#include <iostream>
#include <cstdlib>        // sprintf
#include <unistd.h>       // getopt()

#include <libnet.h>       // libnet

using namespace std;

// First libnet program with IPv4 and Ethernet address handling
int main(int argc, char *argv[]) {
  char                     *device = NULL;               // device with which to inject
  char                      argch;                       // to manage command line arguments
  libnet_t                 *libnet_ctx;                  // libnet session context
  char                      errbuf[LIBNET_ERRBUF_SIZE];  // buffer to get error messages
  u_int32_t                 ip_addr;                     // to manipulate IPv4 addresses
  struct libnet_ether_addr *mac_addr;                    // to manipulate MAC addresses

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

      case 'h':           // show help info
        cout << "Usage: inject [-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;
    }

  // Get libnet session context and validate 
  libnet_ctx = libnet_init(LIBNET_RAW4, device, errbuf);
  if (libnet_ctx == NULL) {
    cerr << "error - libnet_init() failed (" << errbuf << ")" << endl;
    return -1; 
  }
   
  // Get IPv4 address given to device
  ip_addr = libnet_get_ipaddr4(libnet_ctx);
  if (ip_addr != -1)
    cout << "IP address = " << libnet_addr2name4(ip_addr, LIBNET_DONT_RESOLVE) << endl;
  else
    cerr << "error - libnet_get_ipaddr4 failed (" << libnet_geterror(libnet_ctx) << ")" << endl;

  // Get MAC address given to device
  mac_addr = libnet_get_hwaddr(libnet_ctx);
  if (mac_addr != NULL) {
    char s[18];
    sprintf(s, "%02x:%02x:%02x:%02x:%02x:%02x", mac_addr->ether_addr_octet[0],
                                                mac_addr->ether_addr_octet[1],
                                                mac_addr->ether_addr_octet[2],
                                                mac_addr->ether_addr_octet[3],
                                                mac_addr->ether_addr_octet[4],
                                                mac_addr->ether_addr_octet[5]);
    cout << "MAC address = " << s << endl;
  }
  else
    cerr << "error - libnet_get_hwaddr failed (" << libnet_geterror(libnet_ctx) << ")" << endl;

  // Free libnet session context
  if (libnet_ctx) libnet_destroy(libnet_ctx);

  return 0;
}

Command line arguments are introduced in inject02.cpp for more flexibility (block of code starting at lines #019). New arguments will be defined later on.

So let's compile and run the program:

%root> g++ -o inject02 inject02.cpp -lnet
%root> ./inject02
IP address = 172.16.179.138
MAC address = 00:0c:29:19:22:3a
%root>

Note that we do not have to provide a device name to libnet_init(); it will attempt to find a suitable device in the local host.

Building and injecting datagrams

libnet makes available a build function for most common types of header you may want to use in your datagram. Each of these functions is proposed in two versions: the traditional build version which allows to specify values for each header field, and the autobuild version which only asks for the most important fields, providing default values for the others. For example, here are both versions of functions for building an IPv4 packet:

libnet_ptag_t libnet_build_ipv4(u_int16_t len, u_int8_t tos, u_int16_t id, u_int16_t frag,
    u_int8_t ttl, u_int8_t prot, u_int16_t sum, u_int32_t src, u_int32_t dst, u_int8_t * payload,
    u_int32_t payload_s, libnet_t * l, libnet_ptag_t ptag)
libnet_ptag_t libnet_autobuild_ipv4(u_int16_t len, u_int8_t prot, u_int32_t dst, libnet_t *l)

In this tutorial we'll use the autobuild versions whenever possible.

So let's enhance inject02.cpp to send an ICMP Echo Request to a destination provided through a command argument (-t):

inject03.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
122
123
124
125
#include <iostream>
#include <cstdlib>        // sprintf, exit
#include <unistd.h>       // getopt()

#include <libnet.h>       // libnet

using namespace std;

libnet_t *libnet_ctx = NULL;     // libnet session context

// Function releasing all resources before ending program execution
void shutdown(int error_code) {
  // Free libnet session context
  if (libnet_ctx) 
    libnet_destroy(libnet_ctx);

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

// libnet program sending a ping (ICMP Echo Request)
int main(int argc, char *argv[]) {
  char                     *device = NULL;               // device with which to inject
  char                      argch;                       // to manage command line arguments
  char                      errbuf[LIBNET_ERRBUF_SIZE];  // buffer to get error messages
  u_int32_t                 ip_addr;                     // to manipulate IPv4 addresses
  struct libnet_ether_addr *mac_addr;                    // to manipulate MAC addresses
  char                     *target = NULL;               // IP of target host in string form
  u_int32_t                target_addr;                  // IP of target host

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

      case 'h':           // show help info
        cout << "Usage: inject [-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 << " -t #.#.#.#  : IP address of ping target." << endl ;

        // Exit if only argument is -h
        if (argc == 2) return 0;
        break;
        
      case 't':           // target IP
        target = optarg;
        break;
    }
    
  // Make sure we have a target to ping
  if (target == NULL) {
    cerr << "error - no target to ping (use option -t)" << endl;
    shutdown(-1); 
  }

  // Get libnet session context and validate 
  libnet_ctx = libnet_init(LIBNET_RAW4, device, errbuf);
  if (libnet_ctx == NULL) {
    cerr << "error - libnet_init() failed (" << errbuf << ")" << endl;
    shutdown(-2); 
  }
   
  // Get IPv4 address given to device
  ip_addr = libnet_get_ipaddr4(libnet_ctx);
  if (ip_addr != -1)
    cout << "IP address = " << libnet_addr2name4(ip_addr, LIBNET_DONT_RESOLVE) << endl;
  else
    cerr << "error - libnet_get_ipaddr4 failed (" << libnet_geterror(libnet_ctx) << ")" << endl;

  // Get MAC address given to device
  mac_addr = libnet_get_hwaddr(libnet_ctx);
  if (mac_addr != NULL) {
    char s[18];
    sprintf(s, "%02x:%02x:%02x:%02x:%02x:%02x", mac_addr->ether_addr_octet[0],
                                                mac_addr->ether_addr_octet[1],
                                                mac_addr->ether_addr_octet[2],
                                                mac_addr->ether_addr_octet[3],
                                                mac_addr->ether_addr_octet[4],
                                                mac_addr->ether_addr_octet[5]);
    cout << "MAC address = " << s << endl;
  }
  else
    cerr << "error - libnet_get_hwaddr failed (" << libnet_geterror(libnet_ctx) << ")" << endl;

  // Convert target IP into integer form (with DNS resolution if needed)
  if ((target_addr = libnet_name2addr4(libnet_ctx, target, LIBNET_RESOLVE)) == -1) {
    cerr << "error - can't resolve " << target << endl;
    shutdown(-3);
  }
	
  // Tags for handling datagram building
  libnet_ptag_t icmp_ptag = LIBNET_PTAG_INITIALIZER;
  libnet_ptag_t ip_ptag   = LIBNET_PTAG_INITIALIZER;

  // Construct an ICMP Echo Request datagram
  icmp_ptag = libnet_build_icmpv4_echo(ICMP_ECHO, 0, 0, 0, 0, NULL, 0, libnet_ctx, 0);
  if (icmp_ptag == -1) {
    cerr << "error - can't build ICMP header (" << libnet_geterror(libnet_ctx) << ")" << endl;
    shutdown(-4); 
  }

  // Construct an IP packet to encapsulate the ICMP datagram
  ip_ptag = libnet_autobuild_ipv4(LIBNET_IPV4_H + LIBNET_ICMPV4_ECHO_H, IPPROTO_ICMP, 
                                  target_addr, libnet_ctx);
  if (ip_ptag == -1) {
    cerr << "error - can't build IP header (" << libnet_geterror(libnet_ctx) << ")" << endl;
    shutdown(-5); 
  }

  // Inject the resulting datagram
  int bytes = libnet_write(libnet_ctx);
  if (bytes == -1)
    cerr << "error - failed to inject (" << libnet_geterror(libnet_ctx) << ")" << endl;
  else {
    cout << "Sending " << bytes << " bytes of data to target " 
         << libnet_addr2name4(target_addr, LIBNET_DONT_RESOLVE) << endl;
    
    libnet_clear_packet(libnet_ctx);   // clear datagram associated to context (optional)
  }

  // Shutdown the application
  shutdown(0);
}

Here are the key points of inject03.cpp:

  • Function shutdown() is reintroduced to handle the release of resources upon program termination. This requires variable libnet_ctx to become global.

  • Command line argument -t is added (line #047) and validated (line #053) to get the target host's IP or domain name. This host address is converted into integer form (line #088) with DNS resolution. This allows the user to specify the target either by its IP address (e.g. 74.125.196.106) or by its domain name (e.g. www.google.com). Note that LIBNET_RESOLVE does nothing whenever an IP address is provided to libnet_name2addr4().

  • Datagram construction tags (icmp_ptag and ip_ptag) are defined at lines #094 and #095 to control datagram creation and encapsulation. The ICMP Echo Request packet is built at line #098, then encapsulated into an IP packet built at line #105. Note the call to libnet_autobuild_ipv4() which provides a header length long enough to encapsulate both the ICMP and the IP packets. Note also the order in which both packets are created: libnet_build_icmpv4_echo() is called before libnet_autobuild_ipv4() since the former must be encapsulated in the latter.

  • The IP packet is not explicitly encapsulated into an Ethernet frame since the libnet context was obtained with LIBNET_RAW4 (line #059). libnet takes care of Ethernet encapsulation.

  • Finally, the constructed datagram is injected into the network at line #113. If injection was successful, the datagram is destroyed (line #120). Note that explicitly destroying the built datagram is not required here since freeing the context afterward will take care of that. It is however always appropriate to explicitly cleanup after ourselves!

The most important key point to remember about using libnet is the encapsulation order of datagrams : the datagram building functions must be called in the same order as the datagrams are to be encapsulated, starting with the uppermost TCP/IP layer (i.e. from the application layer down to the Ethernet layer).

If we run inject03.cpp, we see that 28 bytes were injected into the network: 20 bytes for the IP header and 8 bytes for the ICMP header. We did not add payload to the ICMP packet, and libnet does not count the Ethernet frame bytes (which we did not explicitly create anyway):

%root> ./inject03 www.google.com
IP address = 172.16.179.138
MAC address = 00:0c:29:19:22:3a
Sending 28 bytes of data to target 74.125.196.106
%root>

We now have a program which sends a single ICMP Echo Request packet to a target host. In order to extend inject03.cpp into a true ping program, we must add code to wait for the returned ICMP Echo Reply, which requires integrating datagram capturing into the program using libpcap. That's done in the next section of the tutorial.


Home  |  Previous  |  Next

 
Copyright © 2014 Marco Lavoie