0% found this document useful (0 votes)
11 views

Implementing A Network Traffic Analyzer in Rust - by Luis Soares - Dev Genius

Uploaded by

alistdktrue2
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
11 views

Implementing A Network Traffic Analyzer in Rust - by Luis Soares - Dev Genius

Uploaded by

alistdktrue2
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 23

This member-only story is on us. Upgrade to access all of Medium.

Member-only story

Implementing a Network Traffic


Analyzer in Rust
Luis Soares · Follow
Published in Dev Genius · 11 min read · Oct 8, 2023

69 3
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


1. pcap : The pcap crate offers bindings to the libpcap library, granting the
ability to capture live network packets. Through it, users can set filters to
capture specific traffic, read from saved
Open in app pcap files, and directly interact
with live packet data.
Search Write

2. pnet : The pnet crate 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.

3. notify-rust : With notify-rust , users can send desktop notifications


from their Rust applications. This crate supports platform-independent
notifications, different levels of urgency, and customizable notification
timeouts.

4. serde : serde is 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.
5. toml : Designed to work seamlessly with serde , the toml crate 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., eth0 which 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()
);
}
},
_ => {}
}
}
}
}

Download Now!

We introduced the pnet library to parse the captured packets.

The EthernetPacket::new method 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 && etherne
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_IP and ALERT_PORT to 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_alert function.

send_alert function prints a message to the console and also sends a


system notification using the notify-rust library.

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_
send_alert(&config.alert.ip, config.alert.port);
}
}
// ... rest of the code ...

In this version:
1. We load the configuration from config.toml at the beginning of the main

function.

2. We define multiple structures ( Config , GeneralConfig , and AlertConfig )

to represent the sections and items in our TOML file.

3. In the packet processing loop, we check the mode from the configuration
to decide the logging behavior.

4. We also use the alert configuration to check against packet attributes and
determine if an alert should be triggered.

Implementing a Summary mode


By default, our application will to the console every packet received. It’s very
helpful for a close look at the traffic, but sometimes we might be interested
in the bigger picture. Let’s them implement a summary mode in the app.

For simplicity, we’ll use a text-based chart display, although more advanced
charting solutions can be integrated.

Let’s use the terminal crate to help with console rendering. This allows us to
refresh the display smoothly.

Setting Up:
First, add terminal to your Cargo.toml :

[dependencies]
terminal = "0.4"
Implementation:

extern crate pcap;


extern crate pnet;
extern crate terminal;

use std::collections::HashMap;
use std::thread::sleep;
use std::time::Duration;
use pnet::packet::ethernet::EthernetPacket;
use terminal::{Clear,ClearType};
struct IpStats {
sent: u64,
received: u64,
}
fn main() {
let interface = "eth0";

let mut cap = pcap::Capture::from_device(interface).unwrap()


.promisc(true)
.snaplen(5000)
.open().unwrap();
let mut ip_map: HashMap<String, IpStats> = HashMap::new();
loop {
for _ in 0..10 { // Collect data from 10 packets at a time
if let Ok(packet) = cap.next() {
if let Some(ethernet_packet) = EthernetPacket::new(&packet.data)
let src_ip = ethernet_packet.get_source().to_string();
let dst_ip = ethernet_packet.get_destination().to_string();
update_ip_stats(&mut ip_map, src_ip, true, packet.header.len
update_ip_stats(&mut ip_map, dst_ip, false, packet.header.le
}
}
}
display_summary(&ip_map);
sleep(Duration::from_millis(500));
}
}
fn update_ip_stats(ip_map: &mut HashMap<String, IpStats>, ip: String, is_source:
let stats = ip_map.entry(ip).or_insert(IpStats { sent: 0, received: 0 });
if is_source {
stats.sent += packet_size as u64;
} else {
stats.received += packet_size as u64;
}
}
fn display_summary(ip_map: &HashMap<String, IpStats>) {
terminal::clear(ClearType::All);
println!("IP Address | Packets Sent | Packets Received");
println!("------------------+--------------+-----------------");
for (ip, stats) in ip_map {
println!("{:<18} | {:<12} | {}", ip, stats.sent, stats.received);
}
}

1. We maintain a HashMap ( ip_map ) where the key is an IP address and the


value is its associated IpStats .

2. For every packet processed, we update the sent or received packet count
based on whether the IP was a source or destination.

3. In our main loop, we gather data from 10 packets and then update the
display.

4. The display_summary function prints a table of IP addresses and their


associated sent and received packet counts.

5. Using terminal::clear(ClearType::All) , we clear the console each time


before rendering the new summary.

To spawn the summary display as a separate thread, we can use Rust’s


standard library threading capabilities. This way, the packet capturing loop
and the summary display loop run concurrently without blocking each
other.

Let’s integrate this threading mechanism into our code:

extern crate pcap;


extern crate pnet;
extern crate terminal;

use std::collections::HashMap;
use std::thread;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use pnet::packet::ethernet::EthernetPacket;
use terminal::{Clear, ClearType};
struct IpStats {
sent: u64,
received: u64,
}
fn main() {
let interface = "eth0";

let mut cap = pcap::Capture::from_device(interface).unwrap()


.promisc(true)
.snaplen(5000)
.open().unwrap();
let shared_ip_map = Arc::new(Mutex::new(HashMap::<String, IpStats>::new()));
let ip_map_for_thread = Arc::clone(&shared_ip_map);
// Spawn a thread to handle the display
thread::spawn(move || {
loop {
display_summary(&ip_map_for_thread.lock().unwrap());
thread::sleep(Duration::from_millis(500));
}
});
loop {
if let Ok(packet) = cap.next() {
if let Some(ethernet_packet) = EthernetPacket::new(&packet.data) {
let src_ip = ethernet_packet.get_source().to_string();
let dst_ip = ethernet_packet.get_destination().to_string();
update_ip_stats(&mut shared_ip_map.lock().unwrap(), src_ip, true
update_ip_stats(&mut shared_ip_map.lock().unwrap(), dst_ip, fals
}
}
}
}
fn update_ip_stats(ip_map: &mut HashMap<String, IpStats>, ip: String, is_source:
let stats = ip_map.entry(ip).or_insert(IpStats { sent: 0, received: 0 });
if is_source {
stats.sent += packet_size as u64;
} else {
stats.received += packet_size as u64;
}
}
fn display_summary(ip_map: &HashMap<String, IpStats>) {
terminal::clear(ClearType::All);
println!("IP Address | Packets Sent | Packets Received");
println!("------------------+--------------+-----------------");
for (ip, stats) in ip_map {
println!("{:<18} | {:<12} | {}", ip, stats.sent, stats.received);
}
}

Note: You might need root access to run the application properly. If
permission-denied errors occur, grant access by doing:

sudo setcap cap_net_raw=eip ./target/debug/{your_project_name}

Putting it all together


Let’s have a recap of our journey:

1. Network Monitoring in Rust:

We began by diving into the world of network monitoring using Rust. We


explored how to capture packets using the pcap library, ensuring that we
could listen to traffic and interpret it.

2. Packet Parsing with pnet :

To make sense of the packets, we incorporated the pnet library. This allowed
us to dissect the packet data, identifying details such as source and
destination IP addresses, ports, and more.

3. Applying Filters:
A crucial addition was the ability to set filters, letting our tool focus only on
specific types of traffic. This made the tool more flexible, ensuring that we
could narrow our observations to just the traffic patterns of interest.

4. Alerting Mechanism:

We then introduced alerting capabilities. Whenever traffic from a specific IP


address was detected on a certain port, our tool could trigger an alert. This
was done using both console messages and system notifications via the
notify-rust crate.

5. Flow Analysis:

To provide a more holistic view of network traffic, we ventured into flow


analysis. By analyzing sequences of packets, our tool could infer broader
patterns and behaviors in the network, grouping traffic based on
source/destination pairs and tracking stats.

6. Live-updating Summary Mode:

To enhance user experience, we implemented a live-updating summary


mode. Every 500 milliseconds, a refreshed chart displayed the total packets
sent and received by each IP address. For this live update, we employed the
terminal crate (and later transitioned to the crossterm crate for better
terminal interactions).

7. Multi-threading:

Understanding the importance of real-time performance, we made the


summary display independent of the packet-capturing process. By spawning
it as a separate thread, we ensured that the packet capture loop wasn’t
disrupted by display updates.

Download Now!

🌟 Developing a Fully Functional API Gateway in Rust — Discover how to


set up a robust and scalable gateway that stands as the frontline for your
microservices.

🌟 Implementing a Network Traffic Analyzer — Ever wondered about the


data packets zooming through your network? Unravel their mysteries with
this deep dive into network analysis.

🌟 Building an Application Container in Rust — Join us in creating a


lightweight, performant, and secure container from scratch! Docker’s got
nothing on this. 😉

🌟 Crafting a Secure Server-to-Server Handshake with Rust & OpenSSL —


If you’ve been itching to give your servers a unique secret handshake that
only they understand, you’ve come to the right place. Today, we’re venturing
into the world of secure server-to-server handshakes, using the powerful
combo of Rust and OpenSSL.

🌟Building a Function-as-a-Service (FaaS) in Rust: If you’ve been exploring


cloud computing, you’ve likely come across FaaS platforms like AWS Lambda
or Google Cloud Functions. In this article, we’ll be creating our own simple
FaaS platform using Rust.

🌟 Rusting Up Your Own Self-Signed Certificate Generator — Let’s walk


through the process of crafting your very own self-signed certificate
generator, all using the versatile Rust programming language and the rust-
openssl crate.

If you’ve been itching to give your servers a unique secret handshake that
only they understand, you’ve come to the right place. Today, we’re venturing
into the world of secure server-to-server handshakes, using the powerful
combo of Rust and OpenSSL.

🌟 Rusting Up Your Own Self-Signed Certificate Generator — Let’s walk


through the process of crafting your very own self-signed certificate
generator, all using the versatile Rust programming language and the rust-
openssl crate.

Dive into the code, play around with it, and perhaps even contribute!

Stay tuned, and we’ll catch you in the next part of our Rust-powered journey.

Thanks for sticking around, and happy coding!

There we have it, folks!


You can check out the full (basic version) implementation in my GitHub
repository at https://github.com/luishsr/rust-network-monitor.

Dive into the code, play around with it, and perhaps even contribute!

Stay tuned, and we’ll catch you in the next part of our Rust-powered journey.

Thanks for sticking around, and happy coding!

Read more articles about Rust in my Rust Programming Library!

Visit my Blog for more articles, news, and software engineering stuff!

Follow me on Medium, LinkedIn, and Twitter.

Leave a comment, and drop me a message!

All the best,

Luis Soares

CTO | Tech Lead | Senior Software Engineer | Cloud Solutions Architect | Rust
🦀 | Golang | Java | ML AI & Statistics | Web3 & Blockchain
Rust Rust Programming Language Rustlang Programming

Written by Luis Soares Follow

1.6K Followers · Writer for Dev Genius

Senior Software Engineer | Rust 🦀 | Golang | Java | Cloud Engineer | Web3 & Blockchain |
ML AI & Statistics | Author

More from Luis Soares and Dev Genius

Luis Soares in Dev Genius Anusha SP in Dev Genius

Implementing Zero Knowledge Java 8 Coding and Programming


Multi-Party Computation in Rust Interview Questions and Answers
In this tutorial, we explore the foundational It has been 8 years since Java 8 was released.
concepts and practical implementation of a… I have already shared the Java 8 Interview…
· 10 min read · Apr 10, 2024 6 min read · Jan 31, 2023

26 833 10

Wasp in Dev Genius Luis Soares

You don’t need Devin. Use these The Change Data Capture (CDC)
free AI-powered tools instead. Design Pattern
TL;DR Change Data Capture (CDC) is a design
pattern that identifies and tracks changes in…

8 min read · Mar 21, 2024 · 6 min read · Jan 16, 2024

1K 6 430 3

See all from Luis Soares See all from Dev Genius

Recommended from Medium


Mike Code Adrian Macal in Level Up Coding

Rust, Build a Basic Blockchain Learning Rust: I/O Ring


Blockchain is a decentralized and distributed Are you disappointed with select, poll, epoll or
ledger technology that records transactions… AIO? Try out the best I/O promise in the Linu…

7 min read · Apr 19, 2024 17 min read · Apr 7, 2024

18 154 3

Lists

General Coding Knowledge Coding & Development


20 stories · 1156 saves 11 stories · 581 saves

Stories to Help You Grow as a ChatGPT


Software Developer 21 stories · 598 saves
19 stories · 1015 saves

Atharva Pandey in Rustaceans Luis Soares in Dev Genius


Part 1: Understanding Rust Traits — Rust Traits: from Zero to Hero
The Foundation Who’s never stumbled upon the tricky “the (…)
Welcome, fellow Rustaceans and curious trait bound is not satisfied” when coding in…
minds! Today, we embark on the first…

· 3 min read · Feb 27, 2024 · 7 min read · Feb 20, 2024

65 250 4

Waqar Amin in Stackademic James Lloyd

Why Developers Are Turning to Learning Rust by Project


Golang and Rust Learning by doing is always the most effective
In recent years, there has been a noticeable way! And I think I got a useful tool in the…
shift in the developer community, with many…

3 min read · Apr 14, 2024 8 min read · Jan 8, 2024

20 91

See more recommendations

You might also like