Introduction

So how does libtins work? libtins is conformed by PDU classes, sender and sniffer classes, classes that represent addresses, and some helper functions which make your life easier.

PDUs

We'll first have a look at what a PDU object is. Every PDU implemented in the library(say IP, TCP, UDP, etc) is a class that inherits an abstract class named PDU.

This class contains methods which can retrieve the actual protocol data unit size and its type, among other things. It also contains a method called send which allows you to effectively send that packet through the network.

PDU objects also support stacking. That means one PDU object(disregarding its actual type), can have 0 or 1 inner PDU. This is a very logical way of imagining a network packet. Suppose you create an Ethernet II frame, and then add an IP datagram on top of it, followed by a TCP frame. That structure would look like this inside libtins:

Packet structure

As you may imagine, a PDU's inner pdu can be retrieved using the method PDU::inner_pdu(). Let's see an code example of how this situation could be reproduced:

#include <tins/tins.h>

using namespace Tins;

int main() {
    EthernetII eth;
    IP *ip = new IP();
    TCP *tcp = new TCP();
    // tcp is ip's inner pdu
    ip->inner_pdu(tcp);
    // ip is eth's inner pdu
    eth.inner_pdu(ip);
}

So what have we done here? The method PDU::inner_pdu(PDU*) sets the given parameter, as the callee's inner PDU. The object passed as an argument must have been allocated using operator new, and from that point on, that PDU is now owned by its parent, meaning that the destruction of that object will be handled by it. So in the above example there is no actual memory leak. On eth's destructor, both the allocated IP and TCP objects will be destroyed and their memory released.

Note that if you want to store a copy and not the actual pointer, you can use the PDU::clone function, which returns a copy of that PDU's concrete type, including all of its stacked inner PDUs.

There is a simpler way to nest PDUs. For those who have used scapy, you may be used to creating a PDU stack using the division operator. libtins supports this as well!

The code above can be rewritten as the following:

#include <tins/tins.h>

using namespace Tins;

int main() {
    // Simple stuff, no need to use pointers!
    EthernetII eth = EthernetII() / IP() / TCP();
    // Retrieve a pointer to the stored TCP PDU
    TCP *tcp = eth.find_pdu<TCP>();
    // You can also retrieve a reference. This will throw a
    // pdu_not_found exception if there is no such PDU in this packet.
    IP &ip = eth.rfind_pdu<IP>();
}

Note that the IP and TCP temporary objects created in the example above, are cloned using the PDU::clone() method.

Address classes

Both IP and hardware addresses are handled using the IPv4Address, IPv6Address and HWAddress<> classes. All of these classes can be constructed from an std::string or c-string containing an appropriate representation(dotted-notation for IPv4Address, semicolon notation for IPv6Addresses, etc).

std::string lo_string("127.0.0.1");

IPv4Address lo("127.0.0.1");
IPv4Address empty; // represents the address 0.0.0.0
// IPv6
IPv6Address lo_6("::1");
// Write it to stdout
std::cout << "Lo: " << lo << std::endl;
std::cout << "Empty: " << empty << std::endl;
std::cout << "Lo6: " << lo_6 << std::endl;

This addresses can be implicitly converted to an integral value, but this is used inside the library, so you don't have to worry about it. As you can notice from above, a default constructed IPv4Address corresponds to the dotted-notation address 0.0.0.0.

These classes also provide a constructor that takes an uint32_t, which is extremely useful when using default values for certain parameters to functions/constructors. In the above example's last couple of lines, both an IPv4 and an IPv6 addresses are written to stdout. This classes define the output operator(operator<<), so it's easier to serialize them.

The HWAddress<> class template is defined as follows:

template<size_t n, typename Storage = uint8_t>
class HWAddress;

Where the n non-type template parameter indicates the length of the address(tipically 6 for network interfaces), and the Storage template parameter indicates the type of each of those n elements (this shouldn't normally be changed, uint8_t should do).

HWAddress objects can be constructed from both std::strings, c-strings, const Storage* and HWAddress of any length. They can also be compared for equality, and provide some helper functions to allow iteration over the address:

HWAddress<6> hw_addr("01:de:22:01:09:af");

std::cout << hw_addr << std::endl;
std::cout << std::hex;
// prints individual bytes
for (auto i : hw_addr) {
    std::cout << static_cast<int>(i) << std::endl;
}

Address range classes

libtins also supports address ranges. This is very useful for several purposes, such as classifying traffic into different subnetworks.

Creating address ranges is very intuitive, using either a slash-dotation, or a netmask:

/* IPv4 */

// 192.168.1.0-255
IPv4Range range1 = IPv4Address("192.168.1.0") / 24;

// Same as above
IPv4Range range2 = IPv4Range::from_mask("192.168.1.0", "255.255.255.0");

/* IPv6 */

// dead:0000:0000:0000:0000:0000:0000:0000-00ff
IPv6Range range3 = IPv6Address("dead::") / 120;

// Same as above
IPv6Range range4 = IPv6Range::from_mask("dead::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00");

Now, what can you do with an address range? You can either iterate it, or ask it if a specific address is inside that network:

IPv4Range range = IPv4Address("192.168.1.0") / 24;

range.contains("192.168.1.250"); // Yey, it belongs to this network
range.contains("192.168.0.100"); // NOPE

// Let's print 'em all
for (const auto &addr : range) {
    std::cout << addr << std::endl;
}

But wait, there's more. You can also create ranges of hardware addresses. Why is this useful? Using this, you can use the OUI specifiers to determine which is the vendor of a specific network device:

// Some OUI which belongs to Intel
auto range = HWAddress<6>("00:19:D1:00:00:00") / 24;

// Does this address belong to Intel?
if (range.contains("00:19:d1:22:33:44")) {
    std::cout << "It's Intel!" << std::endl;
}

Network interfaces

The last helper class reviewed here is NetworkInterface. This class represents the abstraction of a network interface. It can be constructed from the interface's name(as string), and from an IPv4Address. This last constructor creates the interface that would be the gateway if some packet were to be sent to the given ip address:

NetworkInterface lo("lo");
// this would be lo
NetworkInterface lo1(IPv4Address("127.0.0.1"));

You can also retrieve an interface's name using NetworkInterface::name(). Note that this function searches through the system's interfaces and retrieves the name every time it is called, so you might want to call it once and store the return value.

Writing pcap files

Writing packets to a pcap file is very simple as well. The PacketWriter class takes the name of the file in which you want to store packets as its argument, and a flag indicating which will be the lowest layer written to the file. That means, if you're writing EthernetII PDUs, you should use the PacketWriter::ETH2 flag, while on wireless interfaces you should use PacketWriter::RADIOTAP or PacketWriter::DOT11, depending on the encapsulation used in the device.

As of version 3.2 , there's a simpler way to indicate the link layer protocol to be used on the packets written by a PacketWriter. This can be done by instantiating the template class DataLinkType with a link layer protocol PDU as its template argument. This makes it easier to use this class, since you won't have to remember which is the identifier name for some link layer protocol. The following example illustrates this usage:

// We'll write packets to /tmp/test.pcap. Use EthernetII as the link
// layer protocol.
PacketWriter writer("/tmp/test.pcap", DataLinkType<EthernetII>());

// Now create another writer, but this time we'll use RadioTap.
PacketWriter other_writer("bleh.pcap", DataLinkType<RadioTap>());

Once a PacketWriter is created, you can write PDUs to it using the PacketWriter::write method. This method contains 2 overloads: one takes a PDU&, the other one takes two template forward iterators, start and end. The latter will iterate through the range [start, end) and write the PDUs stored in each position of the range. This will work both if *start yields a PDU&, or if dereferecing it several times leads to a PDU&. This means a std::vector<std::unique_ptr<PDU>>::iterator will work as well.

This example creates a std::vector containing one EthernetII PDU, and writes it to a pcap file using both overloads:

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

using namespace Tins;

int main() {
    // We'll write packets to /tmp/test.pcap. The lowest layer will be 
    // EthernetII, so we use the appropriate identifier.
    PacketWriter writer("/tmp/test.pcap", PacketWriter::ETH2);
    // A vector containing one EthernetII PDU.
    std::vector<EthernetII> vec(1, EthernetII("00:da:fe:13:ad:fa"));
    // Write the PDU(s) in the vector(only one, in this case).
    writer.write(vec.begin(), vec.end());
    // Write the same PDU once again, using another overload.
    writer.write(vec[0]);
}

Putting it all together

Now we're going to use most of the classes listed above to create a packet and send it:

#include <tins/tins.h>
#include <cassert>
#include <iostream>
#include <string>

using namespace Tins;

int main() {
    // We'll use the default interface(default gateway)
    NetworkInterface iface = NetworkInterface::default_interface();
    
    /* Retrieve this structure which holds the interface's IP, 
     * broadcast, hardware address and the network mask.
     */
    NetworkInterface::Info info = iface.addresses();
    
    /* Create an Ethernet II PDU which will be sent to 
     * 77:22:33:11:ad:ad using the default interface's hardware 
     * address as the sender.
     */
    EthernetII eth("77:22:33:11:ad:ad", info.hw_addr);
    
    /* Create an IP PDU, with 192.168.0.1 as the destination address
     * and the default interface's IP address as the sender.
     */
    eth /= IP("192.168.0.1", info.ip_addr);
    
    /* Create a TCP PDU using 13 as the destination port, and 15 
     * as the source port.
     */
    eth /= TCP(13, 15);
    
    /* Create a RawPDU containing the string "I'm a payload!".
     */
    eth /= RawPDU("I'm a payload!");
    
    // The actual sender
    PacketSender sender;
    
    // Send the packet through the default interface
    sender.send(eth, iface);
}

Note that the creation of that packet can be done in one line, using operator/ rather than operator/=:

// same as above, just shorter
EthernetII eth = EthernetII("77:22:33:11:ad:ad", info.hw_addr) / 
                 IP("192.168.0.1", info.ip_addr) /
                 TCP(13, 15) /
                 RawPDU("I'm a payload!");

The packet sending mechanism is addressed in the third section of this tutorial.


Next part: Sniffing