Network programming in Linux

Internet Protocol (IP)


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


A datagram at the Internet layer of the TCP/IP stack is called a packet. The following figure illustrates a Internet Protocol version 4 (IPv4) packet format, where each dark blue box represents a single byte while longer boxes represent more than one byte:

IPv4 packet

IPv4 is a connectionless protocol for use on packet-switched networks. It operates on a best effort delivery model, in that it does not guarantee delivery, nor does it assure proper sequencing or avoidance of duplicate delivery. These aspects, including data integrity, are addressed by an upper layer transport protocol, such as the Transmission Control Protocol (TCP), studied later on.

IPv4 uses 32-bit (four-byte) addresses, which limits the address space to 4294967296 (232) addresses. As addresses are assigned to users, the number of unassigned addresses decreases. To address the eventual shortage of IPv4 addresses, a new release of the protocol, Internet Protocol version 6 (IPv6), is intended to replace IPv4. IPv6 uses a 128-bit address, allowing 2128, or approximately 3.4×1038 addresses, or more than 7.9×1028 times as many as the IPv4 32-bit addresses. The two protocols are not designed to be interoperable, complicating the transition to IPv6. IPv6 is currently beyond the scope of this tutorial, so we won't discuss it further and limit our implementation of the Internet Protocol to IPv4.

Here is a brief description of the various IPv4 header fields:

  • Version: The first header field is the four-bit version field. For IPv4, this has a value of 4.

  • Internet Header Length (IHL): The 4-bit field indicates the number of 32-bit words in the header. Since an IPv4 header may contain a variable number of options, this field specifies the size of the header. The minimum value for this field is 5, which is a length of 5×32 = 160 bits = 20 bytes. Being a 4-bit value, the maximum length is 15 words (15×32 bits) or 60 bytes.

  • Type Of Service (TOS): The TOS field specifies a packet's priority and requests a route for low-delay, high-throughput, or highly-reliable service. This field has had various purposes over the years; its modern redefinition is a six-bit Differentiated Services (DS) field and a two-bit Explicit Congestion Notification (ECN) field. While DS is somewhat backwards compatible with TOS, ECN is not.

  • Total Length: This 16-bit field defines the entire packet (fragment) size, including header and data, in bytes. The minimum-length packet is 20 bytes (20-byte header + 0 bytes data) and the maximum is 65,535 bytes — the maximum value of a 16-bit word.

  • Identification: This field is an identification field and is primarily used for uniquely identifying fragments of an original IP datagram.
  • Flags: A three-bit field follows and is used to control or identify fragments. They are (in order, from high order to low order):
        bit 0: Reserved; must be zero.
        bit 1: Don't Fragment (DF)
        bit 2: More Fragments (MF)
    If the DF flag is set and fragmentation is required to route the packet, the packet is dropped. For non fragmented packets, the MF flag is cleared. For fragmented packets, all fragments except the last have the MF flag set. The last fragment has a non-zero Fragment Offset field, differentiating it from a non fragmented packet.

  • Fragment Offset: Measured in units of eight-byte blocks, this field is 13 bits long and specifies the offset of a particular fragment relative to the beginning of the original non fragmented IP datagram. The first fragment has an offset of zero.

  • Time To Live (TTL): This eight-bit field helps prevent datagrams from circulating forever on an Internet. It limits a datagram's lifetime. It value is a maximum hop count — when the datagram arrives at a router, the router decrements the TTL field by one. When the TTL field hits zero, routers discard the packet.

  • Protocol: This field defines the protocol transported in the data portion of the IP datagram.

  • Checksum: The 16-bit checksum field is used for error-checking of the header.

  • Source IP: This field contains the IPv4 address of the sender of the packet.

  • Destination IP: This field contains the IPv4 address of the targeted receiver of the packet.

  • Options: The options field is not often used. Note that the value in the IHL field must include enough extra 32-bit words to hold all the options (plus any padding needed to ensure that the header contains an integer number of 32-bit words). The interpretation of these options is beyond the scope of this tutorial.

Implementing the Internet Protocol packet - the IPPacket class

The IPPacket class maps raw captured bytes (stored in a Datagram instance) into corresponding IPv4 header fields; it is therefore derived from the DatagramFragment class.

ippacket.h
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
#ifndef IPPACKET_H
#define IPPACKET_H

#include <iostream>

#include "datagramfragment.h"   // DatagramFragment
#include "ipaddress.h"          // IPAddress

using namespace std;

/* IPPacket: class mapping the inherited data block as an IP packet.
 *
 * Attributes
 *   p_data (inherited) : array of bytes
 *   p_len (inherited)  : size of p_data
 *
 * Notes
 *   1. the data block referenced by p_data may not be owned by the instance
 *      but instead owned by a Datagram instance which shares its data with
 *      instances of classes derived from DatagramFragment, including this
 *      class.
 */
class IPPacket : public DatagramFragment {
  public:
    // Enumeration of most commonly transported protocols 
    typedef enum {
      ipp_icmp, ipp_igmp, ipp_udp, ipp_tcp, ipp_other, ipp_none
    } IPProtocol;

    IPPacket(bool = false);                            // default constructor
    IPPacket(bool, unsigned char *, unsigned int);     // parameterized constructor
  
    unsigned int header_length() const;                // length of IP packet header in bytes
  
    // Routines returning header field values
    unsigned int version() const;                      // access to Version field
    unsigned int ihl() const;                          // access to IHL field
    unsigned int tos() const;                          // access to Type Of Service field
    unsigned int total_length() const;                 // access to Total Length field

    unsigned int fragment_id() const;                  // access to Fragmentation ID field
    unsigned int fragment_flags() const;               // access to Fragmentation flags
    unsigned int fragment_pos() const;                 // access to Fragmentation position field

    bool fragmented(bool &, bool &) const;             // indicates if datagram is fragmented

    unsigned int ttl() const;                          // access to Time To Live field
    unsigned int checksum() const;                     // access to Checksum field

    unsigned int protocol_id() const;                  // access to Protocol field
    IPProtocol protocol() const;                       // protocol transported in payload

    // Access to IP header options, if any
    unsigned int count_options() const;
    bool option_header(unsigned int, unsigned int &, unsigned int &, unsigned int &) const;

    // Return IP addresses within the header
    IPAddress destination_ip() const;
    IPAddress source_ip() const;
  
    // Operator overloads
    friend ostream & operator<<(ostream &, const IPPacket &);
  
  protected:
};

#endif

And here are the member definitions for IPPacket:

ippacket.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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#ifndef IPPACKET_CPP
#define IPPACKET_CPP

#include "ippacket.h"
#include "exceptions.h"

// Default constructor
IPPacket::IPPacket(bool owned) : DatagramFragment(owned) {
}

// Parameterized constructor
IPPacket::IPPacket(bool owned, unsigned char * s, unsigned int l) : DatagramFragment(owned, s, l) {
}

// Returns the IP header length in bytes
unsigned int IPPacket::header_length() const {
  if (!p_data) 
    return 0;
  else
    return 4 * ihl();
}
  
// Returns the content of the header's Version field
unsigned int IPPacket::version() const {
  return p_data[0] >> 4;
}

// Returns the content of the header's IHL (Internet Header Length) field
unsigned int IPPacket::ihl() const {
  return p_data[0] & 0x0F;
}

// Returns the content of the header's Type Of Service field
unsigned int IPPacket::tos() const {
  return p_data[1];
}

// Returns the content of the header's Total Length field
unsigned int IPPacket::total_length() const {
  return char2word(p_data+2);
}

// Returns the content of the header's Fragmentation ID field
unsigned int IPPacket::fragment_id() const {
  return char2word(p_data+4);
}

// Returns the content of the header's Fragmentation flags
unsigned int IPPacket::fragment_flags() const {
  return p_data[6] >> 5;
}

// Returns the content of the header's Fragmentation position field
unsigned int IPPacket::fragment_pos() const {
  unsigned int i = p_data[6] & 0x1F;
  return i << 8 | p_data[7];
}

// Indicates if the packet is fragmented, and if so, if it's the first and/or 
// last fragment
bool IPPacket::fragmented(bool &first, bool &last) const {
  first = fragment_pos() == 0;
  last  = (fragment_flags() & 0x1) == 0;

  return !(first && last);
}

// Returns the content of the header's Time To Live field
unsigned int IPPacket::ttl() const {
  return p_data[8];
}

// Returns the content of the header's Checksum field
unsigned int IPPacket::checksum() const {
  return char2word(p_data+10);
}

// Returns the content of the header's Protocol field
unsigned int IPPacket::protocol_id() const {
  return p_data[9];
}

// Indicates which protocol is encapsulated within the packet's 
// payload
IPPacket::IPProtocol IPPacket::protocol() const {
  switch (protocol_id()) {
    case  1 : return ipp_icmp;
    case  2 : return ipp_igmp;
    case  6 : return ipp_tcp;
    case 17 : return ipp_udp;
    default : return ipp_other; 
  }
}

// Returns the packet's destination IP address (i.e. where it's 
// going)
IPAddress IPPacket::destination_ip() const {
  if (version() == 4) {
    IPAddress adr(false, p_data + 16);
    return adr;
  }
  else
    throw EBadTransportException("Internet protocol not IPv4");
}

// Returns the packet's source IP address (i.e. where it's 
// coming from)
IPAddress IPPacket::source_ip() const {
  if (version() == 4) {
    IPAddress adr(false, p_data + 12);
    return adr;
  }
  else
    throw EBadTransportException("Internet protocol not IPv4");
}

// Counts the number of options within the header
unsigned int IPPacket::count_options() const {
  unsigned int cnt = 0;

  // Are there options?
  if (header_length() == 20)
    return cnt;

  unsigned char *p = p_data+20;
  unsigned int optclass, optnumber;
  while ((p - p_data) < header_length() && *p != 0) {
    optclass  = (*p & 0x60) >> 5;
    optnumber = *p & 0x1F;

    if (optnumber < 2) 
      p++;
    else
      p += p[1] + 2;
    
    cnt++;
  }

  return cnt;  
}

// Extract attributes of given option index
bool IPPacket::option_header(unsigned int idx, unsigned int &optclass, 
                             unsigned int &optnumber, unsigned int &optlen) const {
  if (idx < 0 || idx >= count_options())
    return false;

  unsigned char *p = p_data+20;

  do {
    optclass  = (*p & 0x60) >> 5;
    optnumber = *p & 0x1F;
    if (optnumber < 2) {
      optlen = 0;
      p++;
    }
    else {
      optlen = p[1];
      p += optlen + 2;
    }
  } while (idx--);

  return true;
}

// Output operator displaying the IP packet header fields in human readable
// form
ostream & operator<<(ostream & ostr, const IPPacket & ip) {
  if (ip.p_data) {
    char outstr[8];

    ostr << "version = ";
    switch (ip.version()) {
      case 4:  ostr << "IPv4" << endl; break;
      case 6:  ostr << "IPv6" << endl; break;
      default: ostr << "unknown [" << ip.version() << "]" << endl; break;
    }

    ostr << "header length = " << ip.header_length() << " (IHL = " << ip.ihl() << ")" << endl;

    ostr << "type of service = " << ip.tos() << ":" << endl;
    if (ip.tos() > 0) {
      switch (ip.tos() >> 5) {
        case 0: ostr << "  precedence = routine" << endl; break;
        case 1: ostr << "  precedence = priority" << endl; break;
        case 2: ostr << "  precedence = immediate" << endl; break;
        case 3: ostr << "  precedence = flash" << endl; break;
        case 4: ostr << "  precedence = flash override" << endl; break;
        case 5: ostr << "  precedence = critical" << endl; break;
        case 6: ostr << "  precedence = internetwork control" << endl; break;
        case 7: ostr << "  precedence = network control" << endl; break;
      }

      // Type of service in textual form
      if (ip.tos() & 0x10)
        ostr << "  delay = low" << endl;
      else
        ostr << "  delay = normal" << endl;

      if (ip.tos() & 0x08)
        ostr << "  throughput = high" << endl;
      else
        ostr << "  throughput = normal" << endl;

      if (ip.tos() & 0x04)
        ostr << "  reliability = high" << endl;
      else
        ostr << "  reliability = normal" << endl;

      if (ip.tos() & 0x02)
        ostr << "  cost = low" << endl;
      else
        ostr << "  cost = normal" << endl;
    }

    ostr << "total length = " << ip.total_length() << endl;

    sprintf(outstr, "0x%.4x", ip.fragment_id());
    ostr << "fragment ID = " << outstr << endl;
    ostr << "  don't fragment = " << (ip.fragment_flags() & 0x2) << endl;
    ostr << "  more fragments = " << (ip.fragment_flags() & 0x1) << endl;
    ostr << "  fragment position = " << ip.fragment_pos() << endl;

    ostr << "protocol = ";
    switch (ip.protocol()) {
      case IPPacket::ipp_icmp: ostr << "ICMP ["; break; 
      case IPPacket::ipp_igmp: ostr << "IGMP ["; break; 
      case IPPacket::ipp_tcp:  ostr << "TCP ["; break; 
      case IPPacket::ipp_udp:  ostr << "UDP ["; break; 
      default:                 ostr << "unknown ["; break; 
    }
    sprintf(outstr, "0x%.2x", ip.protocol_id());
    ostr << outstr << "]" << endl;

    ostr << "time to live = " << ip.ttl() << endl;

    sprintf(outstr, "0x%.4x", ip.checksum());
    ostr << "checksum = " << outstr << endl;

    // Display IP addresses
    ostr << "destination IP address = " << ip.destination_ip() << endl;
    ostr << "source IP address = " << ip.source_ip() << endl;
    
    if (ip.count_options() > 0) {
      ostr << ip.count_options() << " options: " << endl;
    
      // Display each option
      for (int i = 0; i < ip.count_options(); i++) {
        unsigned int optclass, optnumber, optlen;
        if (ip.option_header(i, optclass, optnumber, optlen))
          ostr << "  option #" << i << ": class = "  << optclass 
                        << ", number = " << optnumber;
          switch (optnumber) {
            case 1: ostr << " (nop)"; break;
            case 2: ostr << " (security)"; break;
            case 3: ostr << " (loose source routing)"; break;
            case 4: ostr << " (internet timestamp)"; break;
            case 7: ostr << " (record route)"; break;
            case 8: ostr << " (stream id)"; break;
            case 9: ostr << " (strict source routing)"; break;
          }
          ostr << ", length = " << optlen << endl;
      }
    }
  }
  
  ostr << flush;
  
  return ostr;
}

#endif

The important aspects of IPPacket's implementation are:

  • Line #026 in ippacket.h defines an enumeration of common payloads.

  • IPPacket provides a read-only method for accessing each field of the packet header.

  • Routine header_length() uses the header's IHL value to compute total header length in bytes.

  • Routines at lines #039, #044 and #074 use char2word(), defined in datagramfragment.cpp, to convert two consecutive bytes into an unsigned integer.

  • Routine fragmented() uses call by reference to indicates whether a fragmented packet is the first and/or the last fragment of the set of fragments needed to rebuild the original non fragmented packet.

  • Routines at lines #097 and #108 convert the destination and source IP addresses into corresponding IPAddress instances. The routines ensure the packet is version 4, otherwise an exception is thrown.

  • Routine count_options() checks the header's length to determine whether there are options appended to it. It then browses through each option sequentially to count them. Routine option_header() also browses through the appended options until the targeted one (parameter idx) is reached, and returns the options' details through call by reference.

  • Finally, the operator<<() overload at line #168 extracts and displays all IP packet header values.

The IPAddress class, like the MacAddress class, is a DatagramFragment descendant for mapping captured bytes into IP addresses:

ipaddress.h
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
#ifndef IPADDRESS_H
#define IPADDRESS_H

#include <iostream>

#include "datagramfragment.h"

using namespace std;

/* IPAddress: class mapping the inherited data block as an IP address.
 *
 * Attributes
 *   p_data (inherited) : array of bytes
 *   p_len (inherited)  : size of p_data
 *
 * Notes
 *   1. the data block referenced by p_data may not be owned by the instance
 *      but instead owned by a Datagram instance which shares its data with
 *      instances of classes derived from DatagramFragment, including this
 *      class.
 */
class IPAddress : public DatagramFragment {
  public:
    IPAddress(bool = false);               // default constructor
    IPAddress(bool, unsigned char *);      // parameterized constructor
  
    unsigned int header_length() const;    // length of IP address in bytes
  
    bool valid() const;                    // indicates if it's a valid IP address

    // Operator overloads
    friend ostream & operator<<(ostream &, const IPAddress &);
    
    bool operator==(const IPAddress &) const;
    bool operator<(const IPAddress &) const;
  
  protected:
};

#endif

ipaddress.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
#ifndef IPADDRESS_CPP
#define IPADDRESS_CPP

#include "ipaddress.h"

#define IPADR_LEN 4    // length of IP address in bytes

// Default constructor
IPAddress::IPAddress(bool owned) : DatagramFragment(owned) {
}

// Parameterized constructor
IPAddress::IPAddress(bool owned, unsigned char * s) : DatagramFragment(owned, s, IPADR_LEN) {
}

// Returns length of MAC address (4 bytes)
unsigned int IPAddress::header_length() const {
  return IPADR_LEN;
}  

// Returns true if this is a device IP address (i.e. not 0 of broadcast)
bool IPAddress::valid() const {
  for (unsigned int i = 0; i < this->length(); i++)
    if (this->p_data[i] != 0x00 && this->p_data[i] != 0xFF)
      return true;

  return false;
}

// Output operator displaying the IP address in dot form  (X.X.X.X)
ostream & operator<<(ostream & ostr, const IPAddress & adr) {
  char outstr[4];

  for (unsigned int i = 0; i < adr.length(); i++) {
    sprintf(outstr, "%d", adr.p_data[i]); 
    ostr << outstr;

    if (i < adr.length()-1) ostr << '.';
  }
  
  return ostr;
}

// Relational operator comparing IP addresses at byte level
bool IPAddress::operator==(const IPAddress &adr) const {
  if (this->length() != adr.length())
    return false;
  else {
    for (unsigned int i = 0; i < this->length(); i++)
      if (this->p_data[i] != adr.p_data[i])
        return false;

    return true;
  }
}

// Relational operator comparing IP addresses at byte level
bool IPAddress::operator<(const IPAddress &adr) const {
  unsigned int len = (adr.length() < this->length() ? adr.length() : this->length());

  for (unsigned int i = 0; i < len; i++)
    if (this->p_data[i] < adr.p_data[i])
      return true;
    else if (this->p_data[i] > adr.p_data[i])
      return false;

  return this->length() < adr.length();
}

#endif

As mentioned previously, two utility routines are added to datagramfragment.cpp for casting successive bytes into unsigned integers:

datagramfragment.cpp (partial)
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// Utility function to extract an unsigned int from 2 bytes
unsigned int char2word(const unsigned char *p) {
    unsigned int i = p[0];
    return i << 8 | p[1];
}    

// Utility function to extract an unsigned int from 4 bytes
unsigned int char4word(const unsigned char *p) {
    unsigned int res = p[0];

    res = res << 8 | p[1];
    res = res << 8 | p[2];
    res = res << 8 | p[3];

    return res;
}

Next, we add a new method to the EtherFrame class for mapping its payload into a IPPacket instance:

ethernetframe.cpp (partial)
116
117
118
119
120
121
122
// Returns an instance of the IPv4 datagram transported as payload
IPPacket EthernetFrame::ip4() {
    if (ether_type() != et_IPv4)   // make sure it transports IPv4
        throw EBadTransportException("Ethernet frame not transporting IPv4 traffic");

    return IPPacket(false, data(), length() - header_length());
}

Finally, we update the program's callback function to map the captured bytes into an IPPacket instance for display:

sniff06.cpp (partial)
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
// 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);
       
  Datagram pkt(packet, h->caplen);        // initialized Datagram instance
  if (show_raw) cout << "---------------- Raw data -----------------" << pkt << endl;

  EthernetFrame ether = pkt.ethernet();   // get EthernetFrame instance from transported data
  cout << "---------- Ethernet frame header ----------" << endl << ether;
  
  // Display payload content according to EtherType
  switch (ether.ether_type()) {
    case EthernetFrame::et_IPv4 :         // get IPPacket instance from transported data
      IPPacket ip = ether.ip4();
      cout << "-------- IP packet header --------" << endl << ip;
  }
      
  cout << endl << flush;
}

Here is the resulting display when capturing one IP packets:

%root> ./sniff06 -n 1
device = eth0
network ip = 172.16.179.0
network mask = 255.255.255.0
Grabbed 165 bytes (100%) of datagram received on Mon Aug 26 20:27:46 2013
---------- Ethernet frame header ----------
destination MAC address = ff.ff.ff.ff.ff.ff
source MAC address = 00.50.56.c0.00.08
ether type = IPv4 [0x0800]
-------- IP packet header --------
version = IPv4
header length = 20 (IHL = 5)
type of service = 0:
total length = 151
fragment ID = 0xde7f
  don't fragment = 0
  more fragments = 0
  fragment position = 0
protocol = UDP [0x11]
time to live = 64
checksum = 0xdcb4
destination IP address = 172.16.179.255
source IP address = 172.16.179.1
%root>

The above capture shows a non fragmented packet with no header options and transporting a UDP segment.


Home  |  Previous  |  Next

 
Copyright © 2014 Marco Lavoie