Sniffing basics

Sniffing is done through the Sniffer class. This class accepts a libpcap string filter, and lets you sniff on some network device, interpreting the packets sent through it, and giving youPDU objects so you can easily work with them.

Once you've set the filter, there are two functions which allow to retrieve the sniffed packets. One of them is Sniffer::next_packet. This member function lets you retrieve a packet using the provided filter:

// We want to sniff on eth0. This will capture packets of at most 64 kb.
Sniffer sniffer("eth0");
// Only retrieve IP datagrams which are sent from 192.168.0.1
sniffer.set_filter("ip src 192.168.0.1");
// Retrieve the packet.
PDU *some_pdu = sniffer.next_packet();
// Do something with some_pdu...
....
// Delete it.
delete some_pdu;

Sniffer configuration

Since version 3.2, there's a class that represents the different parameters that can be given to the sniffer in order to affect the sniffing sessions. They are all wrappers over the different libpcap functions, such as pcap_setfilter, pcap_set_promisc, etc. It's an improvement over the many parameters that the other Sniffer constructors' take.

For example, if you wanted to capture packets on port 80, sniff on promiscuous mode and set a snapshot length of 400 bytes, you would do it this way:

// Create sniffer configuration object.
SnifferConfiguration config;
config.set_filter("port 80");
config.set_promisc_mode(true);
config.set_snap_len(400);

// Construct a Sniffer object, using the configuration above.
Sniffer sniffer("eth0", config);

Note: if you notice sniffed packets come in bursts or there's a delay in their capture (e.g. 1 second), this is very likely due to libpcap >= v1.5 using a buffered mode by default. If you want to get packets as fast as possible, make sure to use immediate mode by using SnifferConfiguration::set_immediate_mode.

Loop sniffing

There is another way to extract packets from a Sniffer object, apart from Sniffer::next_packet. It's very common that you want to sniff lots packets until some certain condition is met. In that case its better to use Sniffer::sniff_loop.

This method takes a template functor as an argument, which must define an operator with one of the following signatures:

bool operator()(PDU&);
bool operator()(const PDU&);

// These are only allowed when compiling in C++11 mode.
bool operator()(Packet&);
bool operator()(const Packet&);

The call to Sniffer::sniff_loop will make the sniffer start processing packets. The functor will be called using each processed packet as its argument. If at some point, you want to stop sniffing, then your functor should return false. Otherwise return true and the Sniffer object will keep looping.

The functor object will be copy constructed, so it must implement copy semantics. There is a helper template function which takes a pointer to an object of a template parameter type, and a member function, and returns a HandlerProxy. That object implements the required operator, in which it forwards the call to the member function pointer provided, using the object pointer given:

#include <tins/tins.h>

using namespace Tins;

bool doo(PDU&) {
    return false;
}

struct foo {
    void bar() {
        SnifferConfiguration config;
        config.set_promisc_mode(true);
        config.set_filter("ip src 192.168.0.100");
        Sniffer sniffer("eth0", config);
        /* Uses the helper function to create a proxy object that
         * will call this->handle. If you're using boost or C++11,
         * you could use boost::bind or std::bind, that will also
         * work.
         */
        sniffer.sniff_loop(make_sniffer_handler(this, &foo::handle));
        // Also valid
        sniffer.sniff_loop(doo);
    }
    
    bool handle(PDU&) {
        // Don't process anything
        return false;
    }
};

int main() {
    foo f;
    f.bar();
}

As you can see, sniffing using Sniffer::sniff_loop can not only be an easy way to process several packets, but also can make your code a lot tidier when using classes.

Now the interesting part. In the above example we know we are sniffing IP PDUs sent by the ip address 192.168.0.100, but our function takes a PDU&. We want to search the IP PDU stored inside the parameter(which will probably be of type EthernetII). Luckily for us, you can ask a PDU to search for a certain PDU type inside its whole stack of PDUs(including itself), and return a reference to it. If no such PDU is found in the packet, a pdu_not_found exception is thrown:

bool doo(PDU &some_pdu) {
    // Search for it. If there is no IP PDU in the packet, 
    // the loop goes on
    const IP &ip = some_pdu.rfind_pdu<IP>(); // non-const works as well
    std::cout << "Destination address: " << ip->dst_addr() << std::endl;
    // Just one packet please
    return false;
}

void test() {
    SnifferConfiguration config;
    config.set_promisc_mode(true);
    config.set_filter("ip src 192.168.0.100");
    Sniffer sniffer("eth0", config);
    sniffer.sniff_loop(doo);
}

Another thing that makes the loop-sniffing mechanism better than fetching packets one by one, is exception handling. Sniffer::sniff_loop catches both pdu_not_found and malformed_packet exceptions thrown in the functor body. This means you can use PDU::rfind_pdu and don't even care if such PDU is not found, since the exception will be caught by the Sniffer, and the sniffing session will continue.

Note to Windows users: you may want to check out the sniffing on Windows extra section of this tutorial to make sure you know what you need to before starting a packet capture on that platform.

Sniffing using iterators

There is yet another way to retrieve packets from a Sniffer object. This class defines two methods, begin() and end(), which return forward iterators. These can be used to retrieve packets while they're being sniffed:

Sniffer s = ...;
for (auto &packet : s) {
    // packet is a Packet&
    process(packet);
}

Packet objects

If you require to store a PDU along with the timestamp object, then you should use the Packet class. Packets contain a PDU and Timestamp, can be copyed and moved.

Let's see an example in which we'll store 10 packets read from the wire into a vector:

#include <vector>
#include <tins/tins.h>

using namespace Tins;

int main() {
    std::vector<Packet> vt;
    
    Sniffer sniffer("eth0");
    while (vt.size() != 10) {
        // next_packet returns a PtrPacket, which can be implicitly converted to Packet.
        vt.push_back(sniffer.next_packet());
    }
    // Done, now let's check the packets
    for (const auto& packet : vt) {
        // Is there an IP PDU somewhere?
        if (packet.pdu()->find_pdu<IP>()) {
            // Just print timestamp's seconds and IP source address
            std::cout << "At: " << packet.timestamp().seconds()
                    << " - " << packet.pdu()->rfind_pdu<IP>().src_addr() 
                    << std::endl;
        }
    }
}

As you may have noticed Packet objects can also be used along with Sniffer::next_packet:

Sniffer sniffer("eth0");
// PDU pointer, as mentioned at the beginning
std::unique_ptr<PDU> pdu_ptr(sniffer.next_packet());

// auto cleanup, no need to use pointers!
Packet packet = sniffer.next_packet();
// If there was some kind of error, packet.pdu() == nullptr,
// so we need to check that.
if (packet) {
    process_packet(packet); // whatever
}

Packets can also be accepted on the functor object used on Sniffer::sniff_loop, but only when you are compiling in C++11 mode.

Reading pcap files

Reading files in pcap format is very straightforward. The FileSniffer class takes the name of the file to be opened as argument, and lets you process the packets in it. Both Sniffer and FileSniffer inherit from BaseSniffer, which is the class that actually implements next_packet and sniff_loop. Therefore, we can use the FileSniffer class in the same way we used Sniffer in the examples above:

#include <tins/tins.h>
#include <iostream>
#include <stddef.h>

using namespace Tins;

size_t counter(0);

bool count_packets(const PDU &) {
    counter++;
    // Always keep looping. When the end of the file is found, 
    // our callback will simply not be called again.
    return true;
}

int main() {
    FileSniffer sniffer("/tmp/some_pcap_file.pcap");
    sniffer.sniff_loop(count_packets);
    std::cout << "There are " << counter << " packets in the pcap file\n";
}

Packet interpretation

Now that we've seen the ways in which you can read pcap files and sniff from network interfaces, we'll have a look at how packet interpretation is performed.

Every time a packet is read from one of those sources, an object of that source's link layer type is created(EthernetII, RadioTap, etc). Each of these types of object detects which is the type of the next PDU based on its internal flags, creates it, adds it as its child, and propagates the same action.

This action is performed by every instantiated PDU, except for transport-layer protocols. This means that, for example, if a DNS packet is sniffed off an ethernet interface, you'll get the following structure:

DNS

You can then interpret that DNS packet constructing a DNS object using that RawPDU's payload:

// This is a handler used in Sniffer::sniff_loop
bool handler(const PDU& pkt) {
    // Lookup the UDP PDU
    const UDP &udp = pkt.rfind_pdu<UDP>();
    // We need source/destination port to be 53
    if (udp.sport() == 53 || udp.dport() == 53) {
        // Interpret it as DNS. This might throw, but Sniffer catches it
        DNS dns = pkt.rfind_pdu<RawPDU>().to<DNS>();
        // Just print out each query's domain name
        for (const auto &query : dns.queries()) {
            std::cout << query.dname() << std::endl;
        }
    }
    return true;
}

The same mechanism should be used for other protocols such as DHCP. In case you're wondering why application-layer protocols aren't interpreted automatically by transport-layer PDUs, the reason is efficiency. Application layer protocols, such as DNS, require much more processing in order to parse them than lower layer protocols. In addition, some applications might not even require to use those protocols, so making them pay for that extra processing is undesirable.

Previous part: Basics
Next part: Sending packets