Introduction

The PacketSender class is the responsible for sending packets over the network. Internally, it stores raw sockets for different socket layers(2 and 3 for example).

When calling PacketSender::send(PDU&) the PDU parameter is serialized into a an array of bytes and sent through the appropriate socket.

Sending network layer PDUs

Sending network layer PDUs, such as IP and IPv6 is quite intuitive:

PacketSender sender;
IP pkt = IP("192.168.0.1") / TCP(22) / RawPDU("foo");
sender.send(pkt);

Note that no source address was specified in the IP constructor. This uses by default the address 0.0.0.0. However, when sending network layer PDUs, if the source address is 0.0.0.0, the PDU will perform a lookup on the routing table to find out which should be the source address and automatically sets it. This is done by the network driver already, but some transport layer protocols such as TCP, require this address when calculating the checksum, so this must be done by the library as well.

Sending link layer PDUs

When sending link layer PDUs, such as EthernetII, there is one more thing that should be kept in mind. In this case, the packet must be sent through a specific network interface. You can specify this when sending it:

PacketSender sender;
EthernetII pkt = EthernetII() / IP() / TCP() / RawPDU("foo");
sender.send(pkt, "eth0"); // send it through eth0

// if you're sending multiple packets, you might want to create
// the NetworkInterface object once
NetworkInterface iface("eth0"); 
sender.send(pkt, iface);

That will send the packet through the eth0 interface.

It is quite common to use the same network interface to send several packets. PacketSender contains a default interface, in which link layer PDU's are sent when using the PacketSender::send(PDU&) overload:

PacketSender sender("eth0");
EthernetII pkt = EthernetII() / IP() / TCP() / RawPDU("foo");
sender.send(pkt); // send it through eth0 as well

std::cout << sender.default_interface().name() << std::endl;
sender.default_interface("eth1");
sender.send(pkt); // now we're sending through eth1.

Note that by default this interface is invalid, so you need to set it before sending link layer PDUs as shown above:

PacketSender sender;
EthernetII pkt = EthernetII() / IP() / TCP() / RawPDU("foo");
sender.send(pkt); // throws invalid_interface

Sending and receiving responses

So far we've seen how to send packets, but what if you're expecting a response to that packet? Let's take as an example an ARP request. After it's sent, you'll most likely want to receive a response.

This could be achieved by sniffing while sending the packet, checking each sniffed packet until the response is found. However, in order to match the packet response, it would be necessary to perform several protocol dependent comparisons. In the case of an ARP response, it would be fairly straightforward. However, other protocols require checking destination and source address and ports, identifier numbers, etc.

Fortunately, a sending and receiving mechanism has already been included in the library. This can be achieved by using PacketSender::send_recv, which provides two overloads:

PDU *send_recv(PDU &pdu);
PDU *send_recv(PDU &pdu, const NetworkInterface &iface);

The NetworkInterface parameter serves the same purpose as in PacketSender::send.

Let's see how it could be used to perform an ARP request and receiving its response:

// The address to resolve
IPv4Address to_resolve("192.168.0.1");
// The interface we'll use, since we need the sender's HW address
NetworkInterface iface(to_resolve);
// The interface's information
auto info = iface.addresses();
// Make the request
EthernetII eth = ARP::make_arp_request(to_resolve, info.ip_addr, info.hw_addr);

// The sender
PacketSender sender;
// Send and receive the response.
std::unique_ptr<PDU> response(sender.send_recv(eth, iface));
// Did we receive anything?
if (response) {
    const ARP &arp = response->rfind_pdu<ARP>();
    std::cout << "Hardware address: " << arp.sender_hw_addr() << std::endl;
}

Note that inside PacketSender::send_recv, packets read from the socket will be matched against the sent one until a valid one is found.

Just as a side note, hardware addresses can be resolved much easily, using Utils::resolve_hwaddr:

// The sender
PacketSender sender;
// Will throw std::runtime_error if resolving fails
HWAddress<6> addr = Utils::resolve_hwaddr("192.168.0.1", sender);
std::cout << "Hardware address: " << addr << std::endl;

Going back to the sending and receiving mechanism, we could also use it to determine whether a TCP port is open:

// The sender
PacketSender sender;
// The SYN to be sent.
IP pkt = IP("192.168.0.1") / TCP(22, 1337);
pkt.rfind_pdu<TCP>().set_flag(TCP::SYN, 1);

// Send and receive the response.
std::unique_ptr<PDU> response(sender.send_recv(pkt));
// Did we receive anything?
if (response) {
    TCP &tcp = response->rfind_pdu<TCP>();
    if (tcp.get_flag(TCP::RST)) { 
        std::cout << "Port is closed!" << std::endl;
    }
    else {
        std::cout << "Port is open!" << std::endl;
    }
}

As a last example, the following code resolves a domain name using PacketSender::send_recv:

// The sender
PacketSender sender;
// The DNS request
IP pkt = IP("8.8.8.8") / UDP(53, 1337) / DNS();
// Add the query
pkt.rfind_pdu<DNS>().add_query({ "www.google.com", DNS::A, DNS::IN });
// We want the query to be resolverd recursively
pkt.rfind_pdu<DNS>().recursion_desired(1);

// Send and receive the response.
std::unique_ptr<PDU> response(sender.send_recv(pkt));
// Did we receive anything?
if (response) {
    // Interpret the response
    DNS dns = response->rfind_pdu<RawPDU>().to<DNS>();
    // Print responses
    for (const auto &record : dns.answers()) {
        std::cout << record.dname() << " - " << record.data() << std::endl;
    }
}

Checksum calculation

In the examples above, some of the protocols used, such as IP and TCP contain a checksum field. This checksum must be calculated everytime the packet is sent. libtins does this automatically: everytime a packet is serialized(this happens inside PacketSender::send), the checksums are calculated; so there is no need for you to worry about them.

Thread-safety

One thing that should be noticed is that the raw socket opening operation is not thread-safe, so in case you're having multiple writers, you should explicitly open the required sockets yourself(this can be done through PacketSender::open_l2_socket and PacketSender::open_l3_socket). Otherwise, the sockets will be open when needed.

Previous part: Sniffing
Next part: TCP streams