In this article, we’ll delve into the intricacies of working with network traffic using Rust. We’ll explore capturing packets, parsing them, setting alerts, and even some flow analysis. By the end, you’ll have a foundational understanding of networking in Rust and a stepping stone to craft your own network monitoring solutions.
Let’s get started!
A bit about the Crates we will use
pcap: Thepcapcrate offers bindings to thelibpcaplibrary, granting the ability to capture live network packets. Through it, users can set filters to capture specific traffic, read from saved pcap files, and directly interact with live packet data.pnet: Thepnetcrate is a comprehensive library for packet parsing and crafting in Rust. It provides decoding and encoding for a variety of network protocols, including Ethernet, IP, TCP, and UDP. Additionally, it offers utilities for creating, sending custom packets, and working with network devices.notify-rust: Withnotify-rust, users can send desktop notifications from their Rust applications. This crate supports platform-independent notifications, different levels of urgency, and customizable notification timeouts.serde:serdeis a powerful serialization and deserialization framework in Rust. It supports a range of data formats, such as JSON and TOML. The crate is known for its efficiency, customization capabilities, and the provided macros that ease defining serialization behavior for custom structures.toml: Designed to work seamlessly withserde, thetomlcrate specializes in parsing and generating TOML-formatted strings and files. Whether deserializing TOML into Rust structures or encoding Rust data into TOML, this crate makes TOML handling straightforward.
Setting Up
First, add pcap to your Cargo.toml:
[dependencies]
pcap = "0.8"
Install libpcap for your system if you haven’t already. E.g., for Ubuntu:
$ sudo apt-get install libpcap-dev
Working Example
Let’s write a simple program that captures packets on a given network interface and prints basic information about them:
// Import necessary dependencies
extern crate pcap;
fn main() {
// Choose the network interface for capturing. E.g., "eth0"
let interface = "eth0";
// Open the capture for the given interface
let mut cap = pcap::Capture::from_device(interface).unwrap()
.promisc(true) // Set the capture mode to promiscuous
.snaplen(5000) // Set the maximum bytes to capture per packet
.open().unwrap();
// Start capturing packets
while let Ok(packet) = cap.next() {
println!("Received packet with length: {}", packet.header.len);
// Here, you can add more processing or filtering logic if needed
}
}
- We first set up a capture for a specific device (e.g.,
eth0which is a common interface name for wired connections on Linux systems). The specific name can vary depending on your system configuration. - The
promisc(true)call sets the interface to promiscuous mode, which allows it to capture all packets, not just those destined for the machine. - The
snaplen(5000)call sets the maximum byte length of packets that the capture will obtain. You can adjust this as needed. - In the loop,
cap.next()captures the next packet, returning it if available. We then print the length of the packet, but you can extend this logic to parse the packet, analyze its content, and more.
Enhancing Your Monitor with Packet Parsing
To further extend our monitoring tool, we can dive deeper into the packet content to identify patterns, protocols, or specific packet information. For this, the pnet library in Rust provides an extensive framework for packet crafting and parsing.
Setting Up with pnet
Firstly, add pnet to your Cargo.toml:
[dependencies]
pnet = "0.27"
Extending the Example
extern crate pcap;
extern crate pnet;
use pnet::packet::ethernet::EthernetPacket;
use pnet::packet::ip::IpNextHeaderProtocols;
use pnet::packet::tcp::TcpPacket;
use pnet::packet::udp::UdpPacket;
use pnet::packet::Packet;
fn main() {
let interface = "eth0";
let mut cap = pcap::Capture::from_device(interface).unwrap()
.promisc(true)
.snaplen(5000)
.open().unwrap();
while let Ok(packet) = cap.next() {
// Parse the Ethernet frame from the captured packet data
if let Some(ethernet_packet) = EthernetPacket::new(&packet.data) {
match ethernet_packet.get_ethertype() {
IpNextHeaderProtocols::Tcp => {
// Handle TCP packets
let tcp_packet = TcpPacket::new(ethernet_packet.payload());
if let Some(tcp_packet) = tcp_packet {
println!(
"TCP Packet: {}:{} > {}:{}; Seq: {}, Ack: {}",
ethernet_packet.get_source(),
tcp_packet.get_source(),
ethernet_packet.get_destination(),
tcp_packet.get_destination(),
tcp_packet.get_sequence(),
tcp_packet.get_acknowledgment()
);
}
},
IpNextHeaderProtocols::Udp => {
// Handle UDP packets
let udp_packet = UdpPacket::new(ethernet_packet.payload());
if let Some(udp_packet) = udp_packet {
println!(
"UDP Packet: {}:{} > {}:{}; Len: {}",
ethernet_packet.get_source(),
udp_packet.get_source(),
ethernet_packet.get_destination(),
udp_packet.get_destination(),
udp_packet.get_length()
);
}
},
_ => {}
}
}
}
}

- We introduced the
pnetlibrary to parse the captured packets. - The
EthernetPacket::newmethod creates an Ethernet packet structure from raw bytes. - Depending on the ethertype, we check if it’s a TCP or UDP packet and handle them accordingly.
- For each TCP packet, we print out source and destination IPs, ports, and sequence and acknowledgment numbers.
- For UDP packets, we display source and destination IPs, ports, and the packet length.
Implementing Automatic Alerts
Implementing alerts in our network monitoring tool allows us to be promptly notified when specific network conditions are met. We can use various mechanisms to issue alerts, such as console messages, system notifications, or even integrating with external messaging platforms like Slack or email.
In this example, we’ll implement a basic alert mechanism using console messages and system notifications (using the notify-rust crate). If you wish to expand the system to use other alerting mechanisms, you can further build upon this foundation.
Setting Up
First, add notify-rust to your Cargo.toml:
[dependencies]
notify-rust = "4.0"
Let’s say we want to trigger an alert when a specific IP address sends traffic on a particular port. Here’s how you can achieve that:
extern crate pcap;
extern crate pnet;
extern crate notify_rust;
use pnet::packet::ethernet::EthernetPacket;
use pnet::packet::ip::IpNextHeaderProtocols;
use pnet::packet::tcp::TcpPacket;
use pnet::packet::Packet;
use notify_rust::Notification;
const ALERT_IP: &str = "192.168.1.10";
const ALERT_PORT: u16 = 80;
fn main() {
let interface = "eth0";
let mut cap = pcap::Capture::from_device(interface).unwrap()
.promisc(true)
.snaplen(5000)
.open().unwrap();
while let Ok(packet) = cap.next() {
if let Some(ethernet_packet) = EthernetPacket::new(&packet.data) {
match ethernet_packet.get_ethertype() {
IpNextHeaderProtocols::Tcp => {
let tcp_packet = TcpPacket::new(ethernet_packet.payload());
if let Some(tcp_packet) = tcp_packet {
if tcp_packet.get_destination() == ALERT_PORT && ethernet_packet.get_source().to_string() == ALERT_IP {
send_alert(ALERT_IP, ALERT_PORT);
}
}
},
_ => {}
}
}
}
}
fn send_alert(ip: &str, port: u16) {
println!("ALERT! Traffic from IP {} on port {}", ip, port);
Notification::new()
.summary("Network Monitoring Alert")
.body(&format!("Traffic from IP {} on port {}", ip, port))
.show().unwrap();
}
- We’ve defined constants
ALERT_IPandALERT_PORTto specify which IP and port should trigger the alert. - Inside our packet processing loop, we check if the current packet’s source IP and destination port match our alert criteria.
- If the criteria match, we call the
send_alertfunction. send_alertfunction prints a message to the console and also sends a system notification using thenotify-rustlibrary.
Using Dynamic Configuration
To have a single configuration file that governs both alerts and the application mode (detailed or summary), we’ll define a unified structure in our config.toml file and then adjust our Rust application to read from it.
Configuration File Structure
Here’s a sample config.toml:
[general]
mode = "detailed" # or "summary"
[alert]
ip = "192.168.1.10"
port = 80
This configuration file defines two sections:
general: Holds general configuration items, like the mode of operation.alert: Defines the criteria that should trigger an alert.
Implementing the Configuration
Here’s the adjusted Rust code:
// ... imports ...
#[derive(Deserialize)]
struct Config {
general: GeneralConfig,
alert: AlertConfig,
}
#[derive(Deserialize)]
struct GeneralConfig {
mode: String,
}
#[derive(Deserialize)]
struct AlertConfig {
ip: String,
port: u16,
}
fn main() {
// Load and parse the config
let config_content = fs::read_to_string("config.toml").unwrap();
let config: Config = toml::from_str(&config_content).unwrap();
// ... rest of the main ...
// Inside packet processing loop:
if config.general.mode == "detailed" {
// Detailed logging logic
} else if config.general.mode == "summary" {
// Summary logging logic
}
// For alerts:
if tcp_packet.get_destination() == config.alert.port && ethernet_packet.get_source().to_string() == config.alert.ip {
send_alert(&config.alert.ip, config.alert.port);
}
}
// ... rest of the code ...
In this version:
- We load the configuration from
config.tomlat the beginning of themainfunction. - We define multiple structures (
Config,GeneralConfig, andAlertConfig) to represent the sections and items in our TOML file. - In the packet processing loop, we check the mode from the configuration to decide the logging behavior.
- We also use the alert configuration to check against packet attributes and determine if an alert should be triggered.


