Network programming in Linux

Implementing ping


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


In the previous section we used libnet to send an ICMP Echo Request to a designated target. In this section we enhance this program into a full ping tool.

The previous program, inject03.cpp, uses libnet functions to send an ICMP Echo Request packet to a given destination. This implementation does not however wait for the returned ICMP Echo Reply packet, nor does it repeat the process multiple times as the traditional ping tool does.

To make inject03.cpp a full implementation of ping, we must therefore add ICMP packet capture to its logic, which asks for the use of libpcap. The following program uses libnet to sequentially send 4 ICMP Echo Request packets, and use libpcap to capture the corresponding ICMP Echo Replies. To properly identify the replies to its requests, the program marks all requests with its PID (process identifier, which is unique to each running process on a host), and implements a filter to capture only the replies with matching identifier value.

So here is a simplified version of ping implemented with libpcap, libnet and some classes presented in the previous sections of the tutorial:

ping.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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
#include <iostream>
#include <cstdlib>        // sprintf, exit
#include <signal.h>       // Ctrl+C handling
#include <sys/time.h>     // gettimeofday
#include <unistd.h>       // sleep

#include "datagram.h"     // Datagram
#include "ippacket.h"     // IPPacket
#include "icmppacket.h"   // ICMPPacket
#include "exceptions.h"   // EBadTransportException

#include <libnet.h>       // libnet
#include <pcap.h>         // libpcap

using namespace std;

libnet_t    *libnet_ctx  = NULL;    // libnet session context
pcap_t      *pcap_ctx    = NULL;    // libpcap session context
bpf_program  pcap_filter;           // libpcap filter for echo replies

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

  // Free libpcap filter
  if (pcap_ctx) 
    pcap_freecode(&pcap_filter);

  // Free libpcap session context
  if (pcap_ctx) 
    pcap_close(pcap_ctx);

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

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

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

// Returns current time in milliseconds
unsigned long get_clock() {
  struct timeval tv;
  gettimeofday(&tv, NULL);

  return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

// Implementation of ping
int main(int argc, char *argv[]) {
  char       *device = NULL;             // device to sniff
  u_int32_t   ip_addr;                   // to manipulate IPv4 addresses
  u_int32_t   target_addr;               // IP of target host
  u_int32_t   host_addr;                 // IP of local host
  bpf_u_int32 netp,                      // network IP of local device 
              maskp;                     // network IP mask of local device

  // We must make error buffer large enough to hold both libpcap and libnet messages
  char errbuf[PCAP_ERRBUF_SIZE > LIBNET_ERRBUF_SIZE? PCAP_ERRBUF_SIZE : LIBNET_ERRBUF_SIZE];  
  
  // Make sure we have a target to ping
  if (argc < 2) {
    cerr << "error - no target to ping" << endl;
    shutdown(-1);    // Cleanup and quit
  }

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

  // Identify device to use
  if ((device = pcap_lookupdev(errbuf)) == NULL) {
    cerr << "error - pcap_lookupdev() failed (" << errbuf << ")" << endl;
    shutdown(-2);    // Cleanup and quit
  }

  // Get libpcap session context and validate
  pcap_ctx = pcap_open_live(device, 1600, 0, 0 /* wait forever */, errbuf);
  if (pcap_ctx == NULL) {
    cerr << "error - pcap_open_live() failed (" << errbuf << ")" << endl;
    shutdown(-3);    // Cleanup and quit
  }

  // 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(-4);    // Cleanup and quit
  }
   
  // Get network mask of local device
  if ((pcap_lookupnet(device, &netp, &maskp, errbuf)) == -1) {
    cerr << "error - libnet_lookupnet() failed (" << errbuf << ")" << endl;
    shutdown(-5);    // Cleanup and quit
  }

  // Get IPv4 address given to device
  ip_addr = libnet_get_ipaddr4(libnet_ctx);
  if (ip_addr == -1)
    cerr << "error - libnet_get_ipaddr4 failed (" << libnet_geterror(libnet_ctx) << ")" << endl;

  // Convert target IP into integer form (with DNS resolution if needed)
  if ((target_addr = libnet_name2addr4(libnet_ctx, argv[1], LIBNET_RESOLVE)) == -1) {
    cerr << "error - can't resolve " << argv[1] << endl;
    shutdown(-6);    // Cleanup and quit
  }
  
  // Build filter to capture only returned echo replies from target host with
  // this process' PID as identifier
  char filter[255];
  sprintf(filter, "icmp && icmp[0]=0 && icmp[4:2]=%d && src host %s", 
                  getpid(), libnet_addr2name4(target_addr, LIBNET_DONT_RESOLVE));

  // Compile BPF filter expression into program if one provided
  if (pcap_compile(pcap_ctx, &pcap_filter, filter, 0x100, maskp) < 0) {
    cerr << "error - pcap_compile() failed (" << pcap_geterr(pcap_ctx) << ")" << endl;
    shutdown(-7);    // Cleanup and quit 
  }

  // Install compiled filter
  if (pcap_setfilter(pcap_ctx, &pcap_filter) < 0) {
    cerr << "error - pcap_setfilter() failed (" << pcap_geterr(pcap_ctx) << ")" << endl; 
    shutdown(-8);    // Cleanup and quit 
  }

  // Display target info
  cout << "PING " << argv[1] << " (" << libnet_addr2name4(target_addr, LIBNET_DONT_RESOLVE) << ")" << endl;
       
  // Injection loop
  for (unsigned int cnt = 0; cnt < 4; cnt++) {
    // 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, getpid(), cnt, NULL, 0, libnet_ctx, icmp_ptag);
    if (icmp_ptag == -1) {
      cerr << "error - can't build ICMP header (" << libnet_geterror(libnet_ctx) << ")" << endl;
      shutdown(-9); 
    }

    // 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(-10); 
    }

    // Inject the resulting datagram and make sure it worked
    int bytes = libnet_write(libnet_ctx);
    if (bytes == -1){
      cerr << "error - failed to inject (" << libnet_geterror(libnet_ctx) << ")" << endl;

      continue;  // proceed to next ping
    }
    
    unsigned int delay = get_clock();  // record time of packet departure

    // Capture upcoming echo reply. We use a do-while loop since pcap_next
    // sometimes fails to read... Dunno why!
    struct pcap_pkthdr hdr;
    u_char * packet;    
    do {
      packet = (u_char *)pcap_next(pcap_ctx, &hdr);
    } while (!packet);
    
    delay = get_clock() - delay;       // calculate response delay
    
    // Make sure we got a response (we may have got a timeout)
    if (packet) {
      try {
        Datagram pkt(packet, hdr.caplen);    // initialized Datagram instance        
        IPPacket ip = pkt.ethernet().ip4();  // get captured IP packet

        // Display ICMP Echo Reply information
        cout << pkt.length() << " bytes from " << ip.source_ip()
             << ": icmp_seq=" << ip.icmp().sequence_number() 
             << " ttl=" << ip.ttl() << " time=" << delay << " ms" << endl;
      }
      catch (EBadTransportException) {
        cerr << "error - unexpected returned datagram!" << endl;
      }
    }

    libnet_clear_packet(libnet_ctx);   // clear datagram associated to context (optional)
    
    sleep(1);                          // delay one second to avoid flooding target
  }
  
  // Shutdown the application
  shutdown(0);
}

Let's have a closer look at this code:

  • Three global variables are defined at lines #017 through #019 to handle the libnet session, the libpcap session as well as capture filtering. These variables are made global in order to be manipulated in the main program as well as in the shutown() function.

  • The defined routine get_clock() returns the current system clock value in milliseconds. It's used by the program to compute return trip times, i.e. the time between an ICMP Echo Request and its reply.

  • Since the buffer errbuf is used to recover error messages from both libpcap and libnet, it must be made large enough to hold messages from both libraries.

  • The libpcap session is opened with no timeout (line #084), which means the program will wait indefinitely for an ICMP Echo Reply.

  • A BPF filter, defined at line #117, is applied to the capture session in order to ignore all traffic not related to the ICMP Echo Requests sent by the program. All ICMP Echo Requests are tagged with the running program's process identifier (commonly called its PID), a unique integer assigned by the operating system to each running process. Since this PID uniquely identifies the program and it's embedded into all ICMP Echo Requests sent, the filter makes sure only the ICMP Echo Replies from the target host and containing this PID will be captured. Using the process' PID as packet identifier allows to run multiple copies of the program simultaneously targeting the same host; since each running copy has its own unique PID, the replies will be only captured by the proper process and ignored by the others.

  • The main injection loop sequentially sends four ICMP Echo Requests and wait for each reply. At each iteration, an ICMP Echo Request packet (built at line #142) is encapsulated into an IPv4 packet (built at line #149) and sent over the network device. Notice the getpid() call at line #142 to tag the ICMP packet with the process' PID.

  • System time is read at line #163, the ICMP Echo Reply is captured as line #170 and system time is read again at line #173 to calculate the return trip time.

  • From the captured ICMP Echo Reply is retrieved an IPPacket at lines #178 and #179, and its attributes are displayed at line #182.

  • Finally, a one second delay is inserted between each ICMP Echo Request sent to avoid flooding the target.

Here is the program used to ping www.google.ca:

%root> ./ping www.google.ca
PING www.google.ca (74.125.29.94)
60 bytes from 74.125.29.94: icmp_seq=0 ttl=128 time=35 ms
60 bytes from 74.125.29.94: icmp_seq=1 ttl=128 time=44 ms
60 bytes from 74.125.29.94: icmp_seq=2 ttl=128 time=44 ms
60 bytes from 74.125.29.94: icmp_seq=3 ttl=128 time=46 ms
%root> 

ping.cpp could be further enhanced to handle lost replies (would require handling capture timeouts) and gather timing statistics such as average return times. These modifications are straightforward and left to the reader.


Home  |  Previous  |  Next

 
Copyright © 2014 Marco Lavoie