Simple packet sniffer reading and dumping raw network packets

The Sniffer application is a simple command line network sniffer like tcpdump or tethereal. The application uses a packet socket to read Ethernet packets from the eth0 interface and dumps the parsed packets out to the standard output.

To try out the example application, check out the library, go to the Sniffer directory and execute

# scons -u
# ./sniffer loop
< Hit Ctrl-C when you've seen enough >
# ./sniffer scheduler
< Hit Ctrl-C when you've seen enough >

We will now look at the code which is found in Sniffer.cc in the Examples/Sniffer directory. The code starts out by including the necessary headers

// Custom includes
#include <string>
#include <iostream>
#include <iomanip>
#include <senf/Socket/Protocols/Raw.hh>
#include <senf/Utils/membind.hh>
#include <senf/Utils/hexdump.hh>
#include <senf/Packets/DefaultBundle/EthernetPacket.hh>
#include <senf/Scheduler/Scheduler.hh>

The example includes two implementations, one using blocking calls and a while loop, the other using the senf::Scheduler for asynchronous event notification. They are implemented in loop_main() and scheduler_main(). They will be documented below. For now, we skip these implementations and go straight to the main() function

int main(int argc, char const * argv[])
{
    std::cout << "Registered packets:\n\n";
    senf::dumpPacketRegistries(std::cout);

    if (argc >= 3) {
        if (std::string(argv[1]) == "loop")
            return loop_main(argc,argv);
        else if (std::string(argv[1]) == "scheduler")
            return scheduler_main(argc,argv);
    }

    std::cerr << "Usage: sniffer { loop | scheduler } [interface]" << std::endl;
    return 1;
}

This routine simply interprets the first command line argument and dispatches to the required implementation.

Now lets go back and study each implementation in detail.

A Blocking Implementation

This implementation is found in the loop_main function.
int loop_main (int argc, char const * argv[])
{
    try {

We catch all exceptions in a try block. This is good for a deliverable binary. When debugging the application, it might be better to let the exception abort the execution so you can get a backtrace of the exception origin in the debugger.

We create a packet socket and bind it to the interface given as second command line argument. A packet socket is a linux specific type of socket which returns ethernet packets directly from the network wire. By uncommenting the last line, you may switch the interface into promiscuous mode.

        senf::PacketSocketHandle sock;
        sock.bind(senf::LLSocketAddress(argv[2]));
        // sock.protocol().promisc("eth0",senf::PacketProtocol::Promiscuous);

We will now read packets from the socket forever, that is until the user hits Ctrl-C

        while (true) { // forever

The next step is, to parse the data read from the socket as an Ethernet packet

            senf::EthernetPacket packet (senf::EthernetPacket::create(
                                             senf::noinit));
            sock.read(packet.data(),0);

There are several ways to read and parse a packet with different tradeoffs between efficiency and simplicity. The Version we use here is already quite efficient.

We begin by pre-declaring an uninitialized senf::EthernetPacket instance. By uninitialized we mean, that the instance is not parseable and has a length of 0 bytes. This differs from a default-constructed packet instance which may have initial content and is parseable.

We then tell the socket to read as much data as is available into the packet. The second arg to read specifies the maximum number of bytes to read or 0 to read as much as possible. We pass packet.data() to socket.read which is an STL compatible container holding the data bytes of our previously created senf::EthernetPacket instance (which is currently empty).

The next step is to write out the packet to the standard output

            packet.dump(std::cout);
            senf::hexdump(
                    packet.last().data().begin(),
                    packet.last().data().end(),
                    std::cout);
            std::cout << "\n\n";

The dump call will write out a complete representation of the parsed packet data. The Packet library will not try to interpret payload data as long as no exact indication of the payload type is available (example: A UDP Payload is not parsed further unless you explicitly tell the library, how to parse it). Tools like tethereal guess the payload type by checking port numbers and the payload data, however this is not advisable for a general purpose packet library.

The next line, hexdump, will write out the last packet component. Packets are managed as a chain of headers. The last header is normally a DataPacket holding the payload data.

That's it. We finish of by catching the exception and giving as much detail as possible if an exception is caught

        }
    }
    catch (std::exception const & ex) {
        std::cerr << senf::prettyName(typeid(ex)) << ": " << ex.what() << "\n";
    }
    return 0;
}

The prettyName function from the Utils library is used, to get a nice, printable representation of the dynamic type of the exception instance. It is an interface to the g++ de-mangler. This is necessary since the name member of the C++ type_info instance is a mangled name in g++.

That's it for the simple blocking implementation.

Using the Scheduler

However, we have another one which uses the Scheduler.
class Sniffer
{
    senf::PacketSocketHandle sock;
    senf::scheduler::FdEvent event;

public:
    Sniffer(std::string const & interface)
        : event ("Sniffer", senf::membind(&Sniffer::dumpPacket, this),
                 sock, senf::scheduler::FdEvent::EV_READ)
    {
        sock.bind(senf::LLSocketAddress(interface));
    }

The class constructor binds the socket defined as a data member to the correct interface. To tell the scheduler to call us back whenever data is available on the socket, we add a senf::scheduler::FdEvent instance to out class.

The senf::scheduler::FdEvent constructor takes several arguments:

  • a string describing the event.
  • the callback to call whenever the event occurs. The callback is specified as a Boost.Function object. We use the senf::membind helper from the Utils library to build such a function object. This helper takes an arbitrary class member and binds it to a specific instance.
  • the handle or file descriptor to monitor.
  • and the events to watch for.
    void run()
    {
        senf::scheduler::process();
    }

The public run() member is called to run the sniffer. Here we just forward the call to the scheduler. Calling the Schedulers process() method will start the event loop. This call does not return (ok, that's a lie. It does return when senf::scheduler::terminate() is called which does not apply here).

private:
    void dumpPacket(int event)
    {

The dumpPacket() member is called by the scheduler whenever an event on the socket is encountered. The scheduler calls this function with a mask of the events which triggered the call.

        senf::EthernetPacket packet (
            senf::EthernetPacket::create(senf::noinit));
        sock.read(packet.data(),0);
        packet.dump(std::cout);
        senf::hexdump(
                packet.last().data().begin(),
                packet.last().data().end(),
                std::cout);
        std::cout << "\n\n";
    }
};

The body is absolutely identical to the body of the while loop of the blocking implementation. However, the scheduler guarantees, that a read on the socket will not block if the socket is triggered to be readable (even if the socket is not set to non-blocking mode).

What's left is the scheduler_main() function to utilize this code

int scheduler_main(int argc, char const * argv[])
{
    try {
        Sniffer sniffer (argv[2]);
        sniffer.run();
    }
    catch (std::exception const & ex) {
        std::cerr << senf::prettyName(typeid(ex)) << ": " << ex.what() << "\n";
    }
    return 0;
}

This function is straight forward. The exception handling is the same as in loop_main().

See also:
The SENF modules
Building the SENF framework
libSocket API reference
libPackets API reference
libUtils API reference