2010-08-11

Traceroute in C++ using boost::asio

The other week, I was experimenting with raw sockets and learning the structure of IP packets (version 4). It is always good to learn by doing and to learn better in this case, I built a simple traceroute program. Of course, this version of traceroute is very simple compared to the more advanced version distributed with various *NIX systems but it demonstrates nicely how to work with raw sockets using boost::asio.

Traceroute works by sending IP packets with the time-to-live-field initially set to zero and then increasing it by one for each hop along the way to a target host. Routers along the route will reply with a ICMP packet with the time exceeded message code since the TTL field is too low. This is the reason why we can learn the address of routers along the route.

The program that can be found below read the raw data received on the wire and parses out the source IP address from the IP packet. Looking at the IP header structure, this information is located at bit offset 96 in the header:

0               8               16                             31
 +-------+-------+---------------+------------------------------+      ---
 |       |       |               |                              |       ^
 |version|header |    type of    |    total length in bytes     |       |
 |  (4)  |length |    service    |                              |       |
 +-------+-------+---------------+-+-+-+------------------------+       |
 |                               | | | |                        |       |
 |        identification         |0|D|M|    fragment offset     |       |
 |                               | |F|F|                        |       |
 +---------------+---------------+-+-+-+------------------------+       |
 |               |               |                              |       |
 | time to live  |   protocol    |       header checksum        |   20 bytes
 |               |               |                              |       |
 +---------------+---------------+------------------------------+       |
 |                                                              |       |
 |                      source IPv4 address                     |       |
 |                                                              |       |
 +--------------------------------------------------------------+       |
 |                                                              |       |
 |                   destination IPv4 address                   |       |
 |                                                              |       v
 +--------------------------------------------------------------+      ---

I should mention that, even though boost::asio should be highly platform independent, I could not get this program to do its job properly on Windows (Vista). It turns out that setting the TTL fails on that system using boost version 1.43. On a Linux Ubuntu box with boost version 1.42 it works as expected. (You can read more about the evolution of this problem on the ticket created for it.)

The code follows. Please read it and you will understand what has been done.

#include <iostream>
#include <sstream>
#include <stdexcept>
#include <vector>
#include <boost/asio.hpp>
#include "icmp_header.hpp"

using boost::asio::ip::icmp;

class traceroute: public boost::noncopyable
{
   public:
      traceroute(const char* host):
         io(),
         resolver(io),
         socket(io, icmp::v4()),
         sequence_number(0)
      {
        icmp::resolver::query query(icmp::v4(), host, "");
        destination = *resolver.resolve(query);
      }
      ~traceroute() { socket.close(); }
      void trace()
      {
         for( int ttl(1); ttl < 31 ; ttl++)
         {
            const boost::asio::ip::unicast::hops option( ttl );
            socket.set_option(option);

            boost::asio::ip::unicast::hops op;
            socket.get_option(op);
            if( ttl !=  op.value() )
            {
               std::ostringstream o;
               o << "TTL not set properly. Should be "
                 << ttl << " but was set to "
                 << op.value() << '.';
               throw std::runtime_error(o.str());
            }

            // Create an ICMP header for an echo request.
            icmp_header echo_request;
            echo_request.type(icmp_header::echo_request);
            echo_request.code(0);
            echo_request.identifier(get_identifier());
            echo_request.sequence_number(++sequence_number);
            const std::string body("");
            compute_checksum(echo_request, body.begin(), body.end());

            // Encode the request packet.
            boost::asio::streambuf request_buffer;
            std::ostream os(&request_buffer);
            os << echo_request << body;

            // Send the request.
            socket.send_to(request_buffer.data(), destination);

            // Recieve some data and parse it.
            std::vector<boost::uint8_t> data(64,0);
            const std::size_t nr(
               socket.receive(
                  boost::asio::buffer(data) ) );
            if( nr < 16 )
            {
               throw std::runtime_error("To few bytes returned.");
            }
            std::ostringstream remote_ip;
            remote_ip  
               << (int)data[12] << '.'
               << (int)data[13] << '.'
               << (int)data[14] << '.'
               << (int)data[15];
            std::cout << remote_ip.str() << '\n';
            if( boost::asio::ip::address_v4::from_string( remote_ip.str() )
                  == destination.address() )
            {
               break;
            }
         }
      }
   private:
      static const int port = 33434;
      boost::asio::io_service io;
      boost::asio::ip::icmp::resolver resolver;
      boost::asio::ip::icmp::socket socket ;
      unsigned short sequence_number;
      boost::asio::ip::icmp::endpoint destination;
      static unsigned short get_identifier()
      {
#if defined(BOOST_WINDOWS)
         return static_cast<unsigned short>(::GetCurrentProcessId());
#else
         return static_cast<unsigned short>(::getpid());
#endif
      }
};

int main( int argc, char** argv)
{
   try
   {
      if( argc != 2 )
      {
         throw std::invalid_argument("Usage: traceroute host");
      }
      traceroute T( argv[1] );
      T.trace();
   }
   catch( const std::exception& e )
   {
      std::cerr << e.what() << '\n';
   }
}

The class icmp_header that is included was simply copied from the boost website:

//
// icmp_header.hpp
// ~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2010 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#ifndef ICMP_HEADER_HPP
#define ICMP_HEADER_HPP

#include <istream>
#include <ostream>
#include <algorithm>

// ICMP header for both IPv4 and IPv6.
//
// The wire format of an ICMP header is:
// 
// 0               8               16                             31
// +---------------+---------------+------------------------------+      ---
// |               |               |                              |       ^
// |     type      |     code      |          checksum            |       |
// |               |               |                              |       |
// +---------------+---------------+------------------------------+    8 bytes
// |                               |                              |       |
// |          identifier           |       sequence number        |       |
// |                               |                              |       v
// +-------------------------------+------------------------------+      ---

class icmp_header
{
public:
  enum { echo_reply = 0, destination_unreachable = 3, source_quench = 4,
    redirect = 5, echo_request = 8, time_exceeded = 11, parameter_problem = 12,
    timestamp_request = 13, timestamp_reply = 14, info_request = 15,
    info_reply = 16, address_request = 17, address_reply = 18 };

  icmp_header() { std::fill(rep_, rep_ + sizeof(rep_), 0); }

  unsigned char type() const { return rep_[0]; }
  unsigned char code() const { return rep_[1]; }
  unsigned short checksum() const { return decode(2, 3); }
  unsigned short identifier() const { return decode(4, 5); }
  unsigned short sequence_number() const { return decode(6, 7); }

  void type(unsigned char n) { rep_[0] = n; }
  void code(unsigned char n) { rep_[1] = n; }
  void checksum(unsigned short n) { encode(2, 3, n); }
  void identifier(unsigned short n) { encode(4, 5, n); }
  void sequence_number(unsigned short n) { encode(6, 7, n); }

  friend std::istream& operator>>(std::istream& is, icmp_header& header)
    { return is.read(reinterpret_cast<char*>(header.rep_), 8); }

  friend std::ostream& operator<<(std::ostream& os, const icmp_header& header)
    { return os.write(reinterpret_cast<const char*>(header.rep_), 8); }

private:
  unsigned short decode(int a, int b) const
    { return (rep_[a] << 8) + rep_[b]; }

  void encode(int a, int b, unsigned short n)
  {
    rep_[a] = static_cast<unsigned char>(n >> 8);
    rep_[b] = static_cast<unsigned char>(n & 0xFF);
  }

  unsigned char rep_[8];
};

template <typename Iterator>
void compute_checksum(icmp_header& header,
    Iterator body_begin, Iterator body_end)
{
  unsigned int sum = (header.type() << 8) + header.code()
    + header.identifier() + header.sequence_number();

  Iterator body_iter = body_begin;
  while (body_iter != body_end)
  {
    sum += (static_cast<unsigned char>(*body_iter++) << 8);
    if (body_iter != body_end)
      sum += static_cast<unsigned char>(*body_iter++);
  }

  sum = (sum >> 16) + (sum & 0xFFFF);
  sum += (sum >> 16);
  header.checksum(static_cast<unsigned short>(~sum));
}

#endif // ICMP_HEADER_HPP

1 kommentarer:

  1. Anyone tried this code on Vista using boost 1.46? Perhaps the issue is resolved?

    ReplyDelete