Capturing Network Packets in Parallel using Rust - Akmot9/pnet_tutorial GitHub Wiki

Network packet capturing is a technique used to monitor data packets as they traverse a network interface. In this tutorial, we'll show you how to capture packets from multiple network interfaces in parallel using Rust.

Prerequisites

having read the last tutorial

Steps

Step 1: Import Required Libraries

Firstly, let's import the required libraries:

use pnet::datalink::Channel::Ethernet;
use pnet::datalink;
use pnet::packet::ethernet::EthernetPacket;
use pnet::packet::Packet;
use pnet::packet::FromPacket;
use std::thread;

Step 2: Spawn Threads for Each Network Interface

Finally, we will loop through each network interface and spawn a new thread to capture packets:

fn main() {
    let interfaces = datalink::interfaces();
    let mut handles = vec![];

    for interface in interfaces {
        let handle = thread::spawn(move || {
            capture_packets(interface);
        });
        handles.push(handle);
    }

    // Wait for all threads to complete
    for handle in handles {
        handle.join().unwrap();
    }
}

let mut handles = vec![];

The line let mut handles = vec![]; creates an empty mutable vector called handles. This vector is intended to store the handles (identifiers) of threads that will be spawned later in the program.

In the context of the code, for each network interface found, a new thread is spawned to handle packet capture on that interface. The handle (identifier) for each thread is then added to the handles vector with the line handles.push(handle);.

The purpose of collecting these handles in a vector is to be able to wait for all threads to complete before the main program terminates. This is done by using the join method on each handle, as seen in this portion of the code:

// Wait for all threads to complete
for handle in handles {
    handle.join().unwrap();
}

The call to join blocks the current thread until the thread associated with the handle has completed. By doing this for each handle in handles, the main program waits for all packet capture threads to finish before it itself terminates.

thread::spawn(move || { ... })

thread::spawn(move || { ... }) spawns a new thread and move keyword allows the closure to take ownership of captured variables. This is important for safely sharing the interface across threads.

Step 3: Define Packet Capture Function

Next, we define a function that captures packets from a given network interface. This function will run in its own thread:

fn capture_packets(interface: datalink::NetworkInterface) {
    let (_, mut rx) = match datalink::channel(&interface, 
                                                                    Default::default()) {
        Ok(Ethernet(tx, rx)) => (tx, rx),
        Ok(_) => panic!("Unhandled channel type: {}",&interface),
        Err(e) => panic!(
            "An error occurred when creating the datalink channel: {}",
            e
        ),
    };

    println!("Start reading packet: ");
    loop {
        match rx.next() {
            Ok(packet) => {
                if let Some(ethernet_packet) = EthernetPacket::new(packet) {
                    println!("New packet on {}", interface.name);
                    println!("{} => {}: {}",
                        ethernet_packet.get_destination(),
                        ethernet_packet.get_source(),
                        ethernet_packet.get_ethertype());
                    let packet = ethernet_packet.packet();
                    let payload = ethernet_packet.payload();
                    let from_packet = ethernet_packet.from_packet();
                    //println!("---");
                    println!("packet: {:?}", packet);
                    // print the full packet as an array of u8
                    println!("payload: {:?}", payload);
                    // print the payload as an array of u8
                    println!("from_packet: {:?}", from_packet);
                    // print the hearder infos: mac address, ethertype, ...
                    // and the payload as an array of u8
                    println!("---");
                    
                }
            }
            Err(e)=> {
                panic!("An error occurred while reading: {}", e);
            }
        }
    }
}

The datalink::channel(&interface, Default::default()) function call is responsible for creating a channel to send and receive data packets over the network interface represented by the interface parameter. This function is a part of the pnet_datalink crate, which is a cross-platform, low-level networking library.

Here's a breakdown:

&interface: A reference to a NetworkInterface object that represents the network interface over which you wish to capture or send packets.

Default::default(): This part is calling the default method on the Default trait to produce a default configuration for the channel. The Default::default() method returns an instance of the Config struct with default values, which means that the channel will be created with default settings.

The function returns a Result which, upon success, contains a channel that can be either an Ethernet channel or another type based on the data link layer of the network interface.

Here's what the different parts of the Result mean:

Ok(Ethernet(tx, rx)): Upon success, you get two channels: tx for transmitting packets and rx for receiving packets. Both are associated with the Ethernet data link layer.

Ok(_): This would match other types of channels that are not Ethernet. The code panics in this case, stating that the channel type is unhandled.

Err(e): If there's an error while creating the channel, it is caught here.

This function is crucial for packet capturing as it sets up the fundamental communication line between your Rust program and the specified network interface.

Running the Code

To run the code, you may need administrative or root permissions since capturing packets usually requires such privileges:

sudo cargo build

Conclusion

With this setup, you can monitor multiple network interfaces in parallel using Rust. The code captures packets and prints details such as source and destination addresses and the packet type.

Remember, packet capturing may be subject to legal constraints. Always make sure to have the appropriate permissions before capturing network traffic.