Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Static Packet Construction with C++ Templates


Static Packet Construction with C++ Templates

Static Packet Construction with C++ Templates

By Martin Casado

Anyone with even a cursory interest in networks needs to dig below the high-level socket libraries to learn how networks operate by constructing packets — headers and all — and letting them loose on the network. Unfortunately, almost all of today's low-level network code makes use of antiquated data structures, frivolously casts character buffers to user-defined types, is littered with pointer arithmetic, and relies heavily on the C preprocessor. Although I'm not a C++ purest who believes that C is the root of all evil, C does seem to have an iron grip on areas of system programming when languages such as C++ may provide advantages. While there are a number of libraries that provide higher level networking functionality — ACE (http://www.cs.wustl.edu/~schmidt/ACE.html), netxx (http://pmade.org/software/netxx/), and Socket++ (http://users.utu.fi/lanurm/socket++/) come to mind — there is little C++ development support under the networking covers. Thus, almost all popular open-source tools developed at this level, including network scanners, exploits, sniffers, and the like, are written in C using many of the same unsafe, unreadable coding practices.

Instead of continuing this legacy of requiring C in low-level network code, you can leverage C++'s powers to take the "hack" out of hacking and move towards writing more readable, safer tools. In this article, I use C++ templates to facilitate packet construction and show how this can be used to write generic routines to support low-level networking primitives.

Handling Addresses

My main focus is on developing a set of building blocks from which you can construct packets of arbitrary complexity. One of the more frustrating aspects of dealing with packet headers is handling addresses. With addresses come the headaches of byte ordering, converting from user-readable character strings to their respective byte representation, and manipulating those utterly confusing C address structs. While any good C++ networking library contains an address class and you don't want to reinvent the wheel, my needs are particular. I want to use addresses as part of a protocol header. For example, consider this struct that defines an Ethernet packet header:

struct ethernet_header
{
  ethernetaddr dhost;
  ethernetaddr shost;
  uint16_t     type;
};

If ethernetaddr is the class representing an Ethernet address, it is imperative that ethernetaddr's memory footprint be only the 6 bytes representing the address. Any additional member variables interfere with the layout of the protocol header. Also, the byte representation of the address should be stored internally in network byte order, as the packet should look on the wire. While this isn't important for Ethernet addresses, it often comes into play with IPv4 addresses. It is important to ensure that the byte ordering is consistent across all accessors and mutators.

Here, I use two address classes — ethernetaddr (10/100 mb Ethernet) and ipaddr (IPv4) — that handle conversion to/from user-readable strings, bitwise operations, comparison operations, and mathematical operations. Listing 1 is the class declaration for ipaddr. (Listing 2 demonstrates ipaddr in action.) The ethernetaddr class has similar functionality.

Templates and Packet Construction

Given a library of predefined protocol headers, you want to be able to piece them together anyway you like to construct valid, layered network packets. For example, say you want to construct an Ethernet ARP packet in memory, the protocol headers should be nested as follows:

| ethernet header | ARP Header |

Again, the C-style approach is to declare a buffer of the correct size, then dynamically pack it by casting header structs to appropriate offsets within the buffer. Why not let the C++ template mechanism construct the memory layout? For example, consider these two structs:

template <typename T = uint8_t[1484]>
struct ethernet
{
  ethernetaddr dhost; 
      //  —  destination hardware address
  ethernetaddr shost; 
      //  —  source hardware address
  uint16_t     type;  
      //  —  ethernet type
    T data;  
      //  —  nested protocol
};    //  —  struct ethernet
struct arp 
{
  uint16_t hrd;  
      //  —  format of hardware address 
  uint16_t pro;  
      //  —  format of protocol address
  uint8_t hln;   
      //  —  length of hardware address
  ... more ARP header fields
  ipaddr  tip;  
      //  —  target IP address
};    //  —  struct arp

You can now construct a valid ARP packet with an Ethernet header as follows:

ethernet<arp> eth_arp;

eth_arp should be the correct size and should have the appropriate memory layout required by the packet. You can access fields in the Ethernet header directly through eth_arp and, in the ARP header, by accessing eth_arp.data. For example, you could set the Ethernet destination hardware address and ARP target IP address as follows:

eth_arp.dhost = "CA:FE:DE:AD:BE:EF";
eth_arp.data.tip = "127.0.0.1";

You can apply this approach to any nested protocol header for which you know the size at compile time. If you build the appropriate template classes for IP, TCP, UDP, and ICMP, you can construct the correct memory layout for an arbitrarily nested packet. For example, if you want an IP packet with an Ethernet header and 1024 bytes of payload, the declaration looks like:

ethernet<ip <uint8_t[1024]> > eth_ip;

or perhaps a TCP packet with 1024 bytes of data:

ethernet< ip < tcp < uint8_t[1024] > > > eth_ip_tcp;

or IP tunneled over IP tunneling ARP:

ethernet< ip < ip < arp > > > eth_ip_ip_arp;

Empty Headers

On the surface, using templates for packet construction is straightforward. Under this veneer, however, lie nasty design demons that must be conquered to create a truly versatile library.

The first problem arises with the question, "How do I construct an Ethernet header with no packet body?" Say, for example, you wanted to construct an Ethernet header that has no payload. This is simple, right? The temptation is to define an empty struct (call it empty) and use that as the template argument when constructing an Ethernet header.

struct empty{};  //  —  0 length struct?
ethernet< empty >;

The problem is that most compilers allocate at least 1 byte to empty; that is, sizeof(ethernet<empty>) is equivalent to sizeof(ethernet< uint8_t>). If you are relying on the type size to send the packet, you have a packet that is an extra byte long hitting the wire. A better solution is to specialize struct ethernet on empty to not include the template parameter:

//  —  specialization of template struct ethernet to
//    not include data type for payload.
template <>
struct ethernet<empty>
{
   ethernetaddr dhost;
   ethernetaddr shost;
   uint16_t     type;
};

Now, when you create an ethernet<empty>, you have a structure of the correct size. However, if you've declared any member methods or static member variables in ethernet, you no longer have access to them. Making redundant copies of all the methods in ethernet<> within the specialization is cumbersome. To minimize the amount of code you have to write, a cleaner solution (though perhaps counterintuitive) is to have the specialization include all member variables, static variables, and member methods, then inherit the specialization from the general struct template like this:

// derive from specialization to get access
// to member methods and static variables
template <typename T = uint8_t[1484]>
struct ethernet : ethernet<empty> 
{
  T data; 
}; //  —  struct ethernet
    template <>
    struct ethernet<empty>
    {
      ethernetaddr dhost;
      ethernetaddr shost;
      uint16_t     type;
      //  —   static variables here
      //  — - member methods here
    };

Any methods that operate on data should be included only in the general template definition.

Initialization

Unlike the C-style method of constructing packets, C++ should be smart enough to fill in a number of the header fields based on the class type and the template parameter. Again, consider the declaration:

ethernet < arp > eth_arp;

The ethernet header has a field called type that should be set to the correct value for the protocol being carried, ARP in this case. If each template parameter defines a static variable ETHTYPE, setting the ethernet type is trivial:

type = T::ETHTYPE;

Unfortunately, since T will not necessarily be a known protocol type, how do you set this field for all valid protocol types known to ethernet (such as ARP, RARP, or IP) and not for arbitrary data types such as the default template parameter value of uint8_t[1484]? One way is to give template class ethernet a hint. You can do this by having all valid protocol types for ethernet inherit from an otherwise useless struct ether_type. This inheritance indicates to class ethernet that the template parameter does, in fact, define its ethernet type. If Ethernet protocol developers follow this general rule, ethernet can then use the Boost (http://www.boost.org/) type-traits's is_base_and_derived to determine whether to look for the ethernet type within the template argument.

You can construct a template class called get_ether_type, which sets a static variable val to the ethernet type if the initial template parameter derives from ether_type; otherwise, it sets it to some default value. Such a class might look like this:

template <typename T, uint16_t default_>
struct get_ether_type 
{
   static const uint16_t val = ether_type_if_true <
       boost::is_base_and_derived<ether_type,T>::value,
       T, default_ >::val;
};

ether_type_if_true is a class template that sets a static variable val to the ethernet type if the first template parameter is True; otherwise, it sets val to the provided default value:

template <bool B, typename T,  uint16_t default_>
  struct ether_type_if_true
  { //  —  false
      static const uint16_t val = default_;
  };  
  template <typename T, uint16_t default_>
  struct ether_type_if_true <true, T, default_>
  { //  —  true
      static const uint16_t val = T::ETHTYPE;
  };

struct ethernet may then, in its constructor, simply set the ethernet type like this:

type = get_ether_type<T, ethernet_base::DEFTYPE>::val;

Thus, any protocol that inherits from ether_type and defines ETHTYPE automatically sets the ether type; otherwise, the ether type gets initialized to the default type.

Using this scheme, you can add new protocols (say, a new protocol supported by IP, of which there are more than 100) to have your new protocol type derive from the indicator class (perhaps ip_type in the case of IP) and define the protocol value as a static variable.

Setting the protocol type field is just one example of how you can automatically fill the fields in the protocol header based on information you know at compile time. Other fields may be set automatically during construction that are not directly related to the template parameter. The simplest are fields with set values, such as the IPv4 version fields. You can also fill in fields based on header length, such as the IP header length field, since you know (at compile time) the full size of the header. You can also come up with template schemes to handle complex header fields such as IP options. However, the terrible demands of content triage require that I save that discussion for another article.

Static-Endian Conversion

All network programmers have dealt with endian conversions using htonl(), ntohl(), htons(), and ntohs(). Unfortunately, these "methods" are often preprocessor macros, which makes them cumbersome to deal with in C++ programs (::htonl(0xdeadbeef) doesn't cut it). For static-endian conversion (that is, when you know what you want to convert at compile time), you can use simple template classes. This code performs an endian conversion for Little-endian machines:

template <uint16_t in>
struct shtons // static host to network endian 
              // conversion on 16-bit word
{
   static const uint16_t val = (in<<8&0xff00)|(in>>8&0x00ff);
};

I often use static-endian conversion classes such as shtons<> in static-variable declarations such as setting the values for ethernet types:

static const uint16_t ARPTYPE = shtons<0x0001>::val;
static const uint16_t PUP    =  shtons<0x0200>::val;
static const uint16_t IP     =  shtons<0x0800>::val;
static const uint16_t ARP    =  shtons<0x0806>::val;
static const uint16_t REVARP =  shtons<0x8035>::val;

Calculating Checksums

With packet construction comes the inevitable requirement of calculating Internet checksums. Ideally, you'd come up with a generic template method that accepts an arbitrary packet, iterates over each of the headers in the packet, and calculates checksums, if applicable. This is nontrivial.

For starters, the IP transport checksum breaks the layered model of networks and requires a checksum over a pseudoheader requiring fields from the IP header. Therefore, a generic approach that considers each layer independently is not possible. Even without this hang-up, how easy is it to iterate over all the headers? That is, given an arbitrary packet type T, how can you be sure to visit all of the headers contained within T and call an appropriate checksum routine? If you could determine that T does contain a nested header, then a recursive template function will dive down the header stack until you reach a header without a nested header or with a nonheader data type. One solution would be to have the general (nonspecialized) header structs derive from a struct (say, struct im_a_header) that basically says, "I have a nested data type of some sort." You can then use the same trick you used before in setting the protocol types (using Boost's is_base_and_derived) to determine if you have a header and if that header has a nested header. Not pretty, but it works.

PacMan

PacMan (short for "Packet Manipulation") is a packet-construction library written by Norman Franke and myself that supports all of the techniques discussed within this article along with many additional features. PacMan is freely available for download and use at http://www.cuj.com/code/. At this writing, PacMan supports Ethernet (10/100 mb), ARP, IP, TCP, ICMP, and UDP, as well as rudimentary checksum functionality that can perform network level (IP) and transport level (TCP, UDP) checksums. The PacMan protocol structs are more complicated than the demonstrative structs presented here (in part to support more complicated features such as IP and TCP options), but generally, the techniques are the same.

Reading/Writing Packets

Using this packet-construction technique, you can create some gnarly packets, but what do you do with them? The next step is to provide a convenient method for sending packets (and capturing responses). The actual transmission of the packets may be done a number of ways — standard UNIX raw sockets, libnet (http://www.packetfactory.net/Projects/Libnet/), libdnet (http://libdnet.sourceforge.net/), and so on. What you are interested in is creating wrapper methods to provide general interfaces to these lower level mechanisms for packet injection.

For this discussion, assume you have two generic, low-level mechanisms for sending/receiving packets at the IP level — you provide the IP header, but the operating system's protocol stack handles datalink functionality such as setting the Ethernet headers and ARP.

The prototypes for the low-level send functions are:

int llsend(uint8_t* packet, uint32_t len)
int llrecv(uint8_t* packet, uint32_t len)

Begin by creating a simple send method that sends any arbitrary IP packet out on the wire. Since you want the method to be simple, a first stab at the send function looks like this:

template <typename T>
int send(const ip<T>& in)
{
   return llsend(reinterpret_cast<uint8_t*>(in), sizeof(in));
}

Using this technique to send a TCP SYN packet looks like:

ip < syn < > > ip_syn;
 ... packet initialization here ...
send(ip_syn);

Using sizeof(..) to determine the packet size may by problematic in some cases. For example, if the packet was initially declared as:

ip < tcp <uint8_t[1500]> > ip_large;

and then packed with 64 bytes of data, the correct size would be the size of the IP header plus the size of the TCP header plus 64 bytes — far less than 1500. Another option for determining the size would be to use the total length field in the IP header. Again, this may not be correct in all cases because an arbitrary packet could have an incorrect total length field, in which case, you would have to rely on users entering the correct length. A reasonable approach would be to add a second parameter to the send method that defaults to 0. If the parameter is 0, the sizeof(..) of the packet is used; otherwise, the user-provided length is used.

Along with an IP level send() method, you would likely want to provide a send(..) implementation that injects raw Ethernet packets:

template <typename T>
int send(const ethernet<T>& in);

Providing multiple implementations of send(..) lets you write generic network functions that operate over distinct packet types (such as a ping method that can operate at the Ethernet and IP level).

Receiving packets on the wire using a packet-capture library such as libpcap (http://www.tcpdump.org/) is trivial. However, you want to make sure that the packet you receive was sent in response to a packet you sent previously.

Say you sent a TCP SYN packet to 10.0.0.2. What sort of responses can you expect back? If the port you sent to is open, you should get a SYN, ACK response from the host; otherwise, the host may send back an RST, ACK packet. You may also receive any number of ICMP responses such as "host unreachable," "time exceeded," "administrative filter," and the like.

At a high level, you want the method to:

//  —  grab a packet from the wire and determine 
//  if it was sent in response to "reference"
template <typename T, typename T2>
int recv(const ip<T>& reference, ip<T2>& buffer)
{
  //  — 
  llrecv(reinterpret_cast<uint8_t*>(&buffer), sizeof(buffer));

</p>
  ... correlate response with original packet
  ... handle ICMP response
}

The correlation step is highly dependent on the packet type originally sent. Instead of using a large spaghetti-esque set of nested if statements, you'll rely on C++'s argument-deduction mechanism to help out. Create a correlation method for TCP:

template <typename T, typename T2>
bool correlate(const ip<tcp<T> >& reference, const ip<T>& received)
{
     // check to see if we have a TCP packet
     if ( received.protocol != reference.protocol )
     { return false; }
     // ouch don't look :-/
     ip< tcp<empty> >* tcphdr = 
          reinterpret_cast<tcphdr*>(&received.data);
     //  —  basic sanity check on addresses and ports
     if ( reference.daddr != received.saddr ||
          reference.data.dport != tcphdr->sport)
          { return false; }
     //  —  Ack?
     if ( tcphdr->flags & tcp<empty>::ACK )
     {
       //  —  does acknowledgment number match original sequence
       //    + 1?
       if ( tcphdr->ack == reference.data.seq + 1)
       { return true;} //  —  valid response 
     }
     if ( tcphdr->seq == reference.data.seq + 1 )
     {
        return true; //  —  valid response 
     }
     return false; //  —  not a response to reference
}

Also, you may want to write a correlate() method for UDP, ICMP, or any other transport protocols you might send.

Any IP packet you send may get an ICMP packet back as a response. To further complicate matters, the ICMP packet may originate from any interface along the route to the destination of the original IP packet. Correlating a received ICMP packet to a sent packet often requires comparing the 64 bytes of data from the ICMP packet with the original packet sent. In the interest of saving space, assume you have a correctly functioning icmp correlation method that returns a -1 if the packet does not correlate with the sent packet; otherwise, it returns a 16-bit return code with the first 8 bits holding the ICMP type and the last 8 bits holding the code. The prototype is as follows:

template <typename T, typename T2>
uint16_t icmp_correlate 
            (const ip<T>& reference, const ip<T >& received);

The simplified recv(..) function now looks like:

template <typename T, typename T2>
int recv(const ip<T>& reference, ip<T2>& buffer)
{
  int ret = 
     llrecv(reinterpret_cast<uint8_t*>(&buffer), sizeof(buffer));
  int icmp_val = 0 ;
  if ( correlate(reference, buffer ) 
  { return ret; }
  else if ( (icmp_val = icmp_correlate(reference, buffer)) >= 0 )
  {
     //  —  print out message?
     //  —  return negative error code
     return -icmp_val;
  }
  return 0;
}

I've only discussed an IP level recv(..) method; you could just as easily write a recv(..) method that works over ethernet<arp> or any other protocol in which you can logically correlate responses.

The send and receive methods are simplistic and real-world applications require timeouts, better result reporting, and improved error handling. However, they provide the basic functionality you need to develop more complex network functionality.

Your bag of tricks now consists of fancy address classes, a complicated packet-construction mechanism, and primitives for sending/receiving/correlating packets.

General Ping

To demonstrate the power of the techniques discussed thus far, I'll present a quasi-generic ping method that operates over any IP packet for which you have overridden the correlate(..) methods. The example code is skeletal since a full implementation would be a bit tedious:

template <typename T>
void ping(const ip<T>& in)
{
  ip < uint8_t[1500] > result; //  —  ethernet MTU is 1500 we should be OK
  send(in); //  —  send out arbitrary IP packet
  int result = 0;
  //  —  fish for a response that correlates to original packet
  while ( ! (result = recv( in , buffer )) )
  {
   //  —  do some timeout mechanism thingy here
  }
  //  —  report results
  if ( result < 0 )
  { //  —  ICMP 
  } else if ( result > 0 )
  { //  —  correctly correlated response 
  }else
  { //  —  assume timeout
  }
}

You could use this method to quickly craft a tool that pings a target with an arbitrary IP packet. For example, a simple SYN probe could be constructed like this:

ip < syn < empty > > synp;
synp.saddr = "10.0.0.1"; //  —  our IP
synp.daddr = "10.0.0.2"; //  —  victims addr
synp.data.sport = 123523; //  —  arbitrary
synp.data.dport = 80; //  —  http
checksum(synp);  //  —  calculate checksum
ping(synp); //  —  and send it out!

ARP Request Generator

This example accepts an IP address and a subnet mask and pings each IP on the address with an ARP request. This could be useful for host enumeration, such as when you've landed on a computer in a foreign network and want to find out which other IPs are listening on the Ethernet.

void arp_enum(const ipaddr& tip, const ipaddr& mask)
{
  ethernet < arpreq< > > arprequest;
  arprequest.dhost = "ff:ff:ff:ff:ff:ff";
  arprequest.shost = "ca:fe:de:ad:be:ef";
  arprequest.data.sha = arprequest.shost;
  arprequest.data.tha = arprequest.dhost;
  arprequest.data.sip = (const std::string)("10.0.0.1");
  for(ipaddr the_ip = tip & mask; the_ip <= (tip | (~mask)); ++the_ip)
  {
    arprequest.data.tip = the_ip;
    cout << "Sending ARP request to " << the_ip.toString() << endl;
    ping(arprequest);
  }
}

This function assumes a working ping(..) method that operates over ethernet<arpreq>, which is trivial to write using the techniques discussed for writing the IP ping.

Conclusion

The techniques and examples I present here only touch on the possibilities for leveraging C++ for low-level network programming. You may want to continue by extending the ping example to develop a generic traceroute method or even contribute to the PacMan library by extending it to support more protocols.

Certainly, there are limits to the applicability of these techniques. As you start to climb the network protocol stack, things start to get a bit more complicated. Protocols such as DNS, which traditionally sit on top of TCP or UDP, are often all but impossible to construct statically. Using static-packet construction to facilitate the inspection of received packets doesn't buy you much because, at compile time, you don't know what sorts of packets you'll be receiving or even the size of expected headers. However, for tools that generate a variety of packets, static-packet construction can greatly reduce code complexity, provide a safer type infrastructure for handling protocol headers, and improve readability. o


Martin is pursing a Ph.D. in computer science at Stanford University. He can be contacted at [email protected].



Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.