Network Packet Inspector
Problem Statement
Build a network packet inspector that:
- Parses binary network protocols (Ethernet, IPv4, TCP, UDP, HTTP)
- Uses pattern matching to destructure packet headers from byte arrays
- Implements a firewall rule engine with complex filtering
- Supports deep packet inspection through all protocol layers
- Detects security threats (SQL injection, XSS, suspicious patterns)
- Tracks TCP connection state using pattern matching
- Provides statistics and connection monitoring
- Demonstrates ALL binary pattern matching techniques
Network Protocol Packet Layouts
Understanding how network packets are structured is essential for building a packet inspector. Network protocols are organized in layers, with each layer wrapping the previous one. This project focuses on three key layers:
- Layer 2 (Data Link): Ethernet - handles local network delivery
- Layer 3 (Network): IPv4 - handles routing between networks
- Layer 4 (Transport): TCP/UDP - handles end-to-end communication
Layer Stacking
Packets are nested like Russian dolls. Each layer adds its own header:
[Ethernet Header (14 bytes)][IPv4 Header (20+ bytes)][TCP/UDP Header (8-20+ bytes)][Payload]
│ │ │
└─ Layer 2 └─ Layer 3 └─ Layer 4 └─ Application Data
When we receive raw bytes, we parse from outside to inside:
- First 14 bytes = Ethernet frame
- Ethernet payload = IPv4 packet
- IPv4 payload = TCP/UDP segment
- TCP/UDP payload = Application data (HTTP, etc.)
Detailed Packet Layout Diagrams
Understanding the exact byte layout of network packets is crucial for binary parsing. Each protocol defines specific fields at specific byte offsets. All multi-byte values use big-endian (network byte order).
Ethernet Frame Layout
The Ethernet frame is the outermost layer (Layer 2). Total minimum size: 14 bytes
Byte Offset
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination MAC Address |
| (6 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source MAC Address |
| (6 bytes) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| EtherType (2 bytes) | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| |
| Payload (46-1500 bytes) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Field Details:
| Field | Byte Offset | Size (bytes) | Description |
|---|---|---|---|
| Destination MAC | 0-5 | 6 | Target hardware address |
| Source MAC | 6-11 | 6 | Sender hardware address |
| EtherType | 12-13 | 2 | Protocol type (0x0800=IPv4, 0x86DD=IPv6, 0x0806=ARP) |
| Payload | 14+ | variable | Next layer data (IPv4, IPv6, ARP, etc.) |
Rust Byte Array Mapping:
#![allow(unused)]
fn main() {
// Given: data: &[u8] containing Ethernet frame
let dst_mac = &data[0..6]; // or data[0..=5]
let src_mac = &data[6..12]; // or data[6..=11]
let ethertype = u16::from_be_bytes([data[12], data[13]]);
let payload = &data[14..];
}
Example Ethernet Frame in Hex:
00 11 22 33 44 55 // Destination MAC: 00:11:22:33:44:55
AA BB CC DD EE FF // Source MAC: AA:BB:CC:DD:EE:FF
08 00 // EtherType: 0x0800 (IPv4)
45 00 00 3C ... // IPv4 packet begins
IPv4 Packet Layout
The IPv4 header is Layer 3. Minimum size: 20 bytes (can be up to 60 bytes with options)
Byte Offset
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length | 0-3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset | 4-7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum | 8-11
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source IP Address | 12-15
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination IP Address | 16-19
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (if IHL > 5) | 20+
| (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Payload |
| (TCP, UDP, ICMP, etc.) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Bit-Level Layout (First Byte):
Byte 0: [7 6 5 4][3 2 1 0]
└Version┘ └─IHL──┘
(4 bits) (4 bits)
Version = (byte >> 4) & 0x0F // Upper 4 bits
IHL = byte & 0x0F // Lower 4 bits
Header Length = IHL × 4 bytes // IHL=5 → 20 bytes
Field Details:
| Field | Byte Offset | Size | Extraction | Description |
|---|---|---|---|---|
| Version | 0 (bits 7-4) | 4 bits | (data[0] >> 4) & 0x0F | IP version (4 for IPv4) |
| IHL | 0 (bits 3-0) | 4 bits | data[0] & 0x0F | Header length in 32-bit words (min 5 = 20 bytes) |
| Type of Service | 1 | 1 byte | data[1] | QoS/DSCP field |
| Total Length | 2-3 | 2 bytes | u16::from_be_bytes([data[2], data[3]]) | Total packet size (header + payload) |
| Identification | 4-5 | 2 bytes | u16::from_be_bytes([data[4], data[5]]) | Fragment identification |
| Flags | 6 (bits 7-5) | 3 bits | (data[6] >> 5) & 0x07 | DF, MF flags |
| Fragment Offset | 6-7 | 13 bits | u16::from_be_bytes([data[6] & 0x1F, data[7]]) | Fragment position |
| TTL | 8 | 1 byte | data[8] | Time to live (hop count) |
| Protocol | 9 | 1 byte | data[9] | Next layer (6=TCP, 17=UDP, 1=ICMP) |
| Checksum | 10-11 | 2 bytes | u16::from_be_bytes([data[10], data[11]]) | Header checksum |
| Source IP | 12-15 | 4 bytes | [data[12], data[13], data[14], data[15]] | Source IPv4 address |
| Dest IP | 16-19 | 4 bytes | [data[16], data[17], data[18], data[19]] | Destination IPv4 address |
| Options | 20+ | variable | Skip to IHL * 4 | Rarely used options |
| Payload | IHL*4+ | variable | &data[header_len..] | TCP/UDP/ICMP data |
Rust Byte Array Mapping:
#![allow(unused)]
fn main() {
// Given: data: &[u8] containing IPv4 packet
let version = (data[0] >> 4) & 0x0F;
let ihl = data[0] & 0x0F;
let header_length = (ihl * 4) as usize;
let total_length = u16::from_be_bytes([data[2], data[3]]);
let ttl = data[8];
let protocol = data[9];
let src_ip = Ipv4Address([data[12], data[13], data[14], data[15]]);
let dst_ip = Ipv4Address([data[16], data[17], data[18], data[19]]);
let payload = &data[header_length..];
}
Example IPv4 Packet in Hex:
45 // Version=4, IHL=5 (20 bytes header)
00 // Type of Service
00 3C // Total Length = 60 bytes
1C 46 // Identification
40 00 // Flags=DF, Fragment Offset=0
40 // TTL = 64
06 // Protocol = 6 (TCP)
B1 E6 // Checksum
C0 A8 01 01 // Source IP: 192.168.1.1
08 08 08 08 // Dest IP: 8.8.8.8
// TCP data follows...
TCP Segment Layout
TCP header is Layer 4. Minimum size: 20 bytes (can be up to 60 bytes with options)
Byte Offset
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port | 0-3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number | 4-7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number | 8-11
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Offset| Rsvd | Flags | Window | 12-15
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer | 16-19
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (if Offset > 5) | 20+
| (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Payload (Data) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Bit-Level Layout (Bytes 12-13):
Byte 12: [7 6 5 4][3 2 1 0]
└Offset─┘ └Reserv┘
(4 bits) (4 bits)
Byte 13: [7 6 5 4 3 2 1 0]
│ │ │ │ │ │ │ └─ FIN
│ │ │ │ │ │ └─── SYN
│ │ │ │ │ └───── RST
│ │ │ │ └─────── PSH
│ │ │ └───────── ACK
│ │ └─────────── URG
└─└───────────── ECE, CWR (ECN)
Data Offset = (byte12 >> 4) × 4 bytes
Flags = byte13 & 0x3F (use 0xFF to get all)
Field Details:
| Field | Byte Offset | Size | Extraction | Description |
|---|---|---|---|---|
| Source Port | 0-1 | 2 bytes | u16::from_be_bytes([data[0], data[1]]) | Sender port |
| Dest Port | 2-3 | 2 bytes | u16::from_be_bytes([data[2], data[3]]) | Target port |
| Seq Number | 4-7 | 4 bytes | u32::from_be_bytes([data[4], data[5], data[6], data[7]]) | Sequence number |
| Ack Number | 8-11 | 4 bytes | u32::from_be_bytes([data[8], data[9], data[10], data[11]]) | Acknowledgment |
| Data Offset | 12 (bits 7-4) | 4 bits | (data[12] >> 4) * 4 | Header length in bytes |
| Reserved | 12 (bits 3-0) | 4 bits | - | Reserved (unused) |
| FIN | 13 (bit 0) | 1 bit | (data[13] & 0x01) != 0 | Final packet |
| SYN | 13 (bit 1) | 1 bit | (data[13] & 0x02) != 0 | Synchronize seq numbers |
| RST | 13 (bit 2) | 1 bit | (data[13] & 0x04) != 0 | Reset connection |
| PSH | 13 (bit 3) | 1 bit | (data[13] & 0x08) != 0 | Push data |
| ACK | 13 (bit 4) | 1 bit | (data[13] & 0x10) != 0 | Acknowledgment valid |
| URG | 13 (bit 5) | 1 bit | (data[13] & 0x20) != 0 | Urgent pointer valid |
| Window | 14-15 | 2 bytes | u16::from_be_bytes([data[14], data[15]]) | Receive window size |
| Checksum | 16-17 | 2 bytes | u16::from_be_bytes([data[16], data[17]]) | Header + payload checksum |
| Urgent Ptr | 18-19 | 2 bytes | u16::from_be_bytes([data[18], data[19]]) | Urgent data pointer |
| Options | 20+ | variable | Skip to offset | MSS, window scaling, etc. |
| Payload | offset+ | variable | &data[header_len..] | Application data |
Rust Byte Array Mapping:
#![allow(unused)]
fn main() {
// Given: data: &[u8] containing TCP segment
let src_port = u16::from_be_bytes([data[0], data[1]]);
let dst_port = u16::from_be_bytes([data[2], data[3]]);
let seq_num = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
let ack_num = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
let data_offset = (data[12] >> 4) * 4; // Header length in bytes
let flags = TcpFlags {
fin: (data[13] & 0x01) != 0,
syn: (data[13] & 0x02) != 0,
rst: (data[13] & 0x04) != 0,
psh: (data[13] & 0x08) != 0,
ack: (data[13] & 0x10) != 0,
urg: (data[13] & 0x20) != 0,
};
let window = u16::from_be_bytes([data[14], data[15]]);
let payload = &data[data_offset as usize..];
}
Example TCP Segment in Hex:
04 D2 // Source Port: 1234
00 50 // Dest Port: 80 (HTTP)
00 00 00 64 // Seq Number: 100
00 00 00 00 // Ack Number: 0
50 // Offset=5 (20 bytes), Reserved=0
02 // Flags: SYN=1, others=0
20 00 // Window: 8192
E3 E7 // Checksum
00 00 // Urgent Pointer
// HTTP request data follows...
UDP Datagram Layout
UDP header is Layer 4. Fixed size: 8 bytes (much simpler than TCP)
Byte Offset
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port | 0-3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length | Checksum | 4-7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Payload (Data) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Field Details:
| Field | Byte Offset | Size | Extraction | Description |
|---|---|---|---|---|
| Source Port | 0-1 | 2 bytes | u16::from_be_bytes([data[0], data[1]]) | Sender port (optional) |
| Dest Port | 2-3 | 2 bytes | u16::from_be_bytes([data[2], data[3]]) | Target port |
| Length | 4-5 | 2 bytes | u16::from_be_bytes([data[4], data[5]]) | Total length (header + payload) |
| Checksum | 6-7 | 2 bytes | u16::from_be_bytes([data[6], data[7]]) | Optional checksum |
| Payload | 8+ | variable | &data[8..] | Application data (DNS, DHCP, etc.) |
Rust Byte Array Mapping:
#![allow(unused)]
fn main() {
// Given: data: &[u8] containing UDP datagram
let src_port = u16::from_be_bytes([data[0], data[1]]);
let dst_port = u16::from_be_bytes([data[2], data[3]]);
let length = u16::from_be_bytes([data[4], data[5]]);
let checksum = u16::from_be_bytes([data[6], data[7]]);
let payload = &data[8..];
}
Example UDP Datagram in Hex:
04 D2 // Source Port: 1234
00 35 // Dest Port: 53 (DNS)
00 20 // Length: 32 bytes (8 header + 24 data)
A1 B2 // Checksum
// DNS query data follows (24 bytes)...
Complete Packet Stack Visualization
Here’s how all layers combine in a real network packet:
┌─────────────────────────────────────────────────────────────────┐
│ ETHERNET FRAME (14 bytes) │
├───────────────────────┬───────────────────────┬─────────────────┤
│ Dst MAC (6 bytes) │ Src MAC (6 bytes) │ EtherType (2) │
│ 00:11:22:33:44:55 │ AA:BB:CC:DD:EE:FF │ 0x0800 (IPv4) │
└───────────────────────┴───────────────────────┴─────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ IPv4 PACKET (20+ bytes) │
├─────┬─────┬──────┬─────────┬─────┬──────┬──────┬────────┤
│ Ver │ IHL │ ToS │ Length │ ID │Flags │ TTL │Proto │
│ 4 │ 5 │ 0 │ 60 │ ... │ ... │ 64 │ 6 │
├─────┴─────┴──────┴─────────┴─────┴──────┴──────┴────────┤
│ Src IP: 192.168.1.1 (4 bytes) │
├─────────────────────────────────────────────────────────┤
│ Dst IP: 8.8.8.8 (4 bytes) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ TCP SEGMENT (20+ bytes) │
├──────────────────────┬──────────────────────────┤
│ Src Port: 1234 │ Dst Port: 80 (HTTP) │
├──────────────────────┴──────────────────────────┤
│ Sequence Number: 100 │
├─────────────────────────────────────────────────┤
│ Acknowledgment: 0 │
├──────────────────┬──────────┬───────────────────┤
│ Offset: 5 │ Flags: S │ Window: 8192 │
└──────────────────┴──────────┴───────────────────┘
│
▼
┌─────────────────────────┐
│ HTTP REQUEST DATA │
│ GET / HTTP/1.1 ... │
└─────────────────────────┘
Byte Offset Summary for Full Stack:
| Layer | Start Byte | End Byte | Size | Content |
|---|---|---|---|---|
| Ethernet | 0 | 13 | 14 | MAC addresses + EtherType |
| IPv4 | 14 | 33 | 20+ | IP header (min 20 bytes) |
| TCP/UDP | 34 | 53 | 20+/8 | Transport header |
| Payload | 54+ | end | variable | Application data |
Accessing Nested Data:
#![allow(unused)]
fn main() {
// From raw byte buffer:
let ethernet = &buffer[0..14];
let ipv4 = &buffer[14..34]; // Assuming 20-byte header
let tcp = &buffer[34..54]; // Assuming 20-byte header
let http_data = &buffer[54..]; // Application payload
}
Problem Statement
Build a network packet inspector that:
- Parses binary network protocols (Ethernet, IPv4, TCP, UDP, HTTP)
- Uses pattern matching to destructure packet headers from byte arrays
- Implements a firewall rule engine with complex filtering
- Supports deep packet inspection through all protocol layers
- Detects security threats (SQL injection, XSS, suspicious patterns)
- Tracks TCP connection state using pattern matching
- Provides statistics and connection monitoring
- Demonstrates ALL binary pattern matching techniques
Key Concepts Explained
This project demonstrates advanced Rust techniques for parsing binary network protocols.
1. Binary Data Parsing
Network packets arrive as raw byte arrays at specific byte offsets.
2. Byte Order (Endianness)
Network protocols use big-endian (most significant byte first).
3. Bit Manipulation
Some fields pack multiple values into single bytes using bit shifts and masks.
4. Pattern Matching on Byte Slices
Rust’s pattern matching works directly on byte arrays for classification.
5. Zero-Copy Parsing
Parse without allocating - return references to original buffer for performance.
6. Newtype Pattern for Type Safety
Wrap primitive types to prevent mixing incompatible values.
7. Enum Dispatch for Protocol Handling
Use enums to represent protocol variants type-safely.
8. State Machines with Pattern Matching
Track TCP connection state transitions with pattern matching.
9. Bitflags Pattern
Group related boolean flags from packed bytes.
Connection to This Project
Milestone 1: Ethernet Parsing
- Binary parsing: Extract MAC addresses from bytes 0-11
- Endianness: Parse EtherType with
from_be_bytes() - Zero-copy: Return payload slice without copying
- Performance: 10x faster than allocating parser
Milestone 2: IPv4 with Bit Manipulation
- Bit manipulation: Extract version and IHL from first byte
- Pattern matching: Classify IP ranges
- Benchmarks: 3.4x faster than if-else chains
Milestone 3: TCP/UDP with Flags
- Bitflags: Extract 6 TCP flags from 1 byte
- State machines: Track connection state
- Security: Prevent SYN flood attacks
Milestone 4: Firewall Rules
- Complex patterns: Match multiple fields
- Pattern guards: Add conditions
- Performance: 100x faster with tries
Milestone 5: Deep Inspection
- Nested parsing: Ethernet → IPv4 → TCP → HTTP
- Threat detection: SQL injection, XSS patterns
- Performance: 20x faster with Aho-Corasick
Milestone 1: Ethernet packets
Step 1.1: Define Ethernet Types
#![allow(unused)]
fn main() {
// TODO: Define MAC address wrapper
// Refer to the Ethernet Frame Layout diagram above for the 6-byte MAC address structure
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MacAddress([u8; 6]);
impl MacAddress {
pub fn new(bytes: [u8; 6]) -> Self {
// TODO: Wrap the byte array in the MacAddress type
todo!()
}
// TODO: Check for broadcast address
// Hint: All bytes set to their maximum value
pub fn is_broadcast(&self) -> bool {
todo!()
}
// TODO: Check for multicast
// Hint: The least significant bit of the first byte indicates multicast
pub fn is_multicast(&self) -> bool {
todo!()
}
}
impl std::fmt::Display for MacAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// TODO: Format MAC address in colon-separated hexadecimal notation
// See the example "00:11:22:33:44:55" format in the Ethernet Frame diagram above
todo!()
}
}
// TODO: Define EtherType enum for protocol identification
// Refer to the EtherType field in the Ethernet Frame Layout (bytes 12-13)
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EtherType {
// TODO: Add variants for common protocol types
// Hint: Check the "Field Details" table in the Ethernet section for protocol values
// Hint: Include a variant to handle unrecognized protocol types
}
impl EtherType {
// TODO: Parse from 2-byte big-endian value using pattern matching
pub fn from_bytes(bytes: [u8; 2]) -> Self {
// TODO: Convert the byte array to a u16 using big-endian byte order.
// Then match on common protocol values and return the appropriate variant.
// For unknown values, wrap them in the Unknown variant.
todo!()
}
}
// TODO: Define Ethernet frame structure
// See the complete Ethernet Frame Layout diagram at the beginning
#[derive(Debug, Clone)]
pub struct EthernetFrame {
// TODO: Add fields matching the Ethernet frame structure
// Hint: Review the "Field Details" table showing all four components
}
}
Step 1.2: Implement Ethernet Parsing
#![allow(unused)]
fn main() {
// TODO: Define parse errors
#[derive(Debug, PartialEq)]
pub enum ParseError {
// TODO: Add error variants to handle parsing failures
// Hint: What can go wrong when parsing binary data?
}
impl EthernetFrame {
// TODO: Parse Ethernet frame from byte slice
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
// TODO: First check if the slice has at least 14 bytes. If not, return an error.
// Extract the destination MAC from bytes 0-5 and source MAC from bytes 6-11.
// The EtherType is in bytes 12-13 (use from_bytes method).
// Everything from byte 14 onward is the payload.
// Build and return the EthernetFrame struct.
todo!()
}
// TODO: Helper to display frame info
pub fn summary(&self) -> String {
// TODO: Format a human-readable summary showing source, destination, and protocol type.
todo!()
}
}
}
Step 1.3: Pattern Matching on EtherType
#![allow(unused)]
fn main() {
// TODO: Classify traffic based on EtherType using exhaustive matching
pub fn classify_ethernet(frame: &EthernetFrame) -> &'static str {
// TODO: Use a match expression on the frame's ethertype field.
// Return different string literals for IPv4, IPv6, ARP, and unknown protocols.
// The compiler will ensure all EtherType variants are handled.
todo!()
}
// TODO: Check if frame is interesting for analysis
pub fn is_interesting(frame: &EthernetFrame) -> bool {
// TODO: Use the matches! macro to check if the ethertype is IPv4 or IPv6.
// Also verify that the destination MAC is not a broadcast address.
// Return true only if both conditions are met.
todo!()
}
}
Checkpoint Tests
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mac_address() {
let mac = MacAddress([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
assert!(mac.is_broadcast());
let multicast = MacAddress([0x01, 0x00, 0x5E, 0x00, 0x00, 0x01]);
assert!(multicast.is_multicast());
}
#[test]
fn test_ethernet_parsing() {
let data = vec![
// Destination MAC
0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
// Source MAC
0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
// EtherType (IPv4 = 0x0800)
0x08, 0x00,
// Payload
0x45, 0x00, 0x00, 0x3C,
];
let frame = EthernetFrame::parse(&data).unwrap();
assert_eq!(frame.dst_mac, MacAddress([0x00, 0x11, 0x22, 0x33, 0x44, 0x55]));
assert_eq!(frame.src_mac, MacAddress([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]));
assert_eq!(frame.ethertype, EtherType::IPv4);
assert_eq!(frame.payload.len(), 4);
}
#[test]
fn test_too_short() {
let data = vec![0x00, 0x11, 0x22];
let result = EthernetFrame::parse(&data);
assert_eq!(result, Err(ParseError::TooShort { expected: 14, found: 3 }));
}
}
}
Check Your Understanding
- Why do we use big-endian byte order for network protocols?
- How does exhaustive matching on EtherType prevent bugs when adding new protocols?
- What would happen if we tried to parse a truncated Ethernet frame?
- How would you extend this to support VLAN tags (802.1Q)?
Milestone 2: IPv4 Parsing with Nested Destructuring
Goal: Parse IP layer and demonstrate pattern matching on IP addresses.
Concepts:
- Array pattern matching for IP addresses
- Range patterns for IP classification
- Pattern guards for validation
- Nested protocol parsing
Implementation Steps
Step 2.1: Define IPv4 Types
#![allow(unused)]
fn main() {
// TODO: Define IPv4 address wrapper
// Refer to the IPv4 Packet Layout diagram (Source/Dest IP fields at bytes 12-19)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Ipv4Address([u8; 4]);
impl Ipv4Address {
pub fn new(a: u8, b: u8, c: u8, d: u8) -> Self {
// TODO: Create Ipv4Address from four octets
todo!()
}
// TODO: Check if IP is in private range using pattern matching
// Hint: Three private ranges defined in RFC 1918
pub fn is_private(&self) -> bool {
todo!()
}
// TODO: Check for loopback addresses
// Hint: Entire /8 block starting with 127
pub fn is_loopback(&self) -> bool {
todo!()
}
// TODO: Check for multicast addresses
// Hint: Class D addresses in the 224-239 range
pub fn is_multicast(&self) -> bool {
todo!()
}
// TODO: Check for link-local addresses
// Hint: APIPA addresses when DHCP fails
pub fn is_link_local(&self) -> bool {
todo!()
}
}
impl std::fmt::Display for Ipv4Address {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// TODO: Format in dotted-decimal notation
// See example "192.168.1.1" in the IPv4 diagram above
todo!()
}
}
// TODO: Define IP protocol types
// Refer to the Protocol field in IPv4 Packet Layout (byte 9)
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum IpProtocol {
// TODO: Add variants for transport layer protocols
// Hint: Check the "Field Details" table for protocol numbers
// Hint: What protocols does this project handle at Layer 4?
}
impl IpProtocol {
pub fn from_u8(value: u8) -> Self {
// TODO: Convert protocol byte to enum variant
todo!()
}
}
// TODO: Define IPv4 packet structure
// Refer to the complete IPv4 Packet Layout and Field Details table above
#[derive(Debug, Clone)]
pub struct Ipv4Packet {
// TODO: Add fields representing the parsed IPv4 header
// Hint: Focus on the fields you'll actually use (not all 14 header fields needed)
// Hint: Review the "Rust Byte Array Mapping" example in the IPv4 section
}
}
Step 2.2: Implement IPv4 Parsing
#![allow(unused)]
fn main() {
impl Ipv4Packet {
// TODO: Parse IPv4 packet from bytes
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
// TODO: Start by checking that the data has at least 20 bytes for the minimum header.
//
// Extract version and IHL from the first byte using bit manipulation:
// The upper 4 bits contain the version, lower 4 bits contain the IHL.
// Calculate the actual header length by multiplying IHL by 4.
//
// Verify that the version field equals 4 for IPv4.
//
// Parse the remaining header fields from their byte positions:
// - Total length from bytes 2-3 (big-endian u16)
// - TTL from byte 8
// - Protocol from byte 9 (convert to IpProtocol enum)
// - Source IP from bytes 12-15
// - Destination IP from bytes 16-19
//
// Extract the payload by skipping past the header length.
// Handle the case where there might be no payload data.
//
// Construct and return the Ipv4Packet with all parsed fields.
todo!()
}
}
}
Step 2.3: Traffic Classification with Pattern Matching
#![allow(unused)]
fn main() {
// TODO: Define traffic types
#[derive(Debug, PartialEq)]
pub enum TrafficType {
// TODO: Add variants to categorize different types of network traffic
// Hint: Think about private vs public IPs, local vs external, special addresses
// Hint: At least 6-7 categories are useful for traffic analysis
}
// TODO: Classify traffic based on IP addresses
pub fn classify_traffic(packet: &Ipv4Packet) -> TrafficType {
// TODO: Create a match expression on a tuple of source and destination IP references.
// Use pattern guards to check IP properties in priority order:
// - First check if either IP is loopback
// - Then check for multicast destinations
// - Then classify based on private vs public IPs:
// * Both private = local network traffic
// * Private to public = outbound traffic
// * Public to private = inbound traffic
// * Both public = internet-routed traffic
// - Use a catch-all pattern for any other cases
todo!()
}
}
Step 2.4: Layered Packet Enum
#![allow(unused)]
fn main() {
// TODO: Define layered packet representation
// See the "Complete Packet Stack Visualization" showing how layers nest
#[derive(Debug, Clone)]
pub enum Packet {
// TODO: Add variants for each protocol layer
// Hint: Each layer should hold its parsed data and optionally the next layer
// Hint: How do you represent recursive nesting in Rust?
// Hint: What's the base case when you can't parse further?
}
impl Packet {
// TODO: Parse from Ethernet layer
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
// TODO: Begin by parsing the Ethernet frame from the input data.
//
// Then attempt to parse the inner protocol based on the EtherType:
// - If it's IPv4, try to parse an Ipv4Packet from the Ethernet payload
// - If successful, wrap it in a boxed Packet::IPv4 variant
// - If parsing fails or it's an unsupported protocol, set inner to None
//
// Return an Ethernet packet variant containing the frame and optional inner packet.
todo!()
}
// TODO: Extract IP addresses using deep destructuring
pub fn extract_ips(&self) -> Option<(Ipv4Address, Ipv4Address)> {
// TODO: Use a match expression with nested patterns to extract IPs.
// Handle multiple cases:
// - An Ethernet frame containing an IPv4 packet (nested destructuring)
// - A standalone IPv4 packet
// - Return None for packets without IP information
// Use the box pattern syntax for matching through Option<Box<...>>
todo!()
}
}
}
Checkpoint Tests
#![allow(unused)]
fn main() {
#[test]
fn test_ipv4_address_classification() {
let private = Ipv4Address::new(192, 168, 1, 1);
assert!(private.is_private());
let public = Ipv4Address::new(8, 8, 8, 8);
assert!(!public.is_private());
let loopback = Ipv4Address::new(127, 0, 0, 1);
assert!(loopback.is_loopback());
let multicast = Ipv4Address::new(224, 0, 0, 1);
assert!(multicast.is_multicast());
}
#[test]
fn test_traffic_classification() {
let local = Ipv4Packet {
version: 4,
header_length: 20,
total_length: 60,
ttl: 64,
protocol: IpProtocol::TCP,
src_ip: Ipv4Address::new(192, 168, 1, 1),
dst_ip: Ipv4Address::new(192, 168, 1, 2),
payload: vec![],
};
assert_eq!(classify_traffic(&local), TrafficType::LocalPrivate);
}
}
Check Your Understanding
- How do array patterns simplify IP address classification?
- Why is pattern matching on IP ranges safer than manual if-else chains?
- How does deep destructuring with
boxpatterns work for nested packets? - What’s the advantage of using pattern guards for subnet checking?
Milestone 3: TCP/UDP Parsing and Port Range Matching
Goal: Parse transport layer and demonstrate range patterns for port filtering
Implementation Steps
Step 3.1: Define TCP Types
#![allow(unused)]
fn main() {
// TODO: Define TCP flags structure
// Refer to the "Bit-Level Layout (Bytes 12-13)" in the TCP Segment Layout above
#[derive(Debug, Clone, Copy)]
pub struct TcpFlags {
// TODO: Add boolean fields for each TCP flag
// Hint: See the bit-by-bit breakdown in byte 13 of the TCP header diagram
}
impl TcpFlags {
// TODO: Parse from byte using bit manipulation
// Refer to the TCP flags extraction example showing bit positions and masks
pub fn from_byte(byte: u8) -> Self {
// TODO: Extract each flag bit from byte 13
// Hint: Each flag has a specific bit position (0-5) with corresponding mask values
todo!()
}
}
// TODO: Define TCP packet structure
// Refer to the TCP Segment Layout diagram and Field Details table above
#[derive(Debug, Clone)]
pub struct TcpPacket {
// TODO: Add fields for the essential TCP header information
// Hint: Review the "Rust Byte Array Mapping" example in the TCP section
// Hint: You don't need all 10+ TCP header fields, just the important ones
}
impl TcpPacket {
// TODO: Parse TCP packet
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
// TODO: Verify the data has at least 20 bytes for the TCP header.
//
// Extract the fields from their byte positions:
// - Source and destination ports (2 bytes each, big-endian)
// - Sequence and acknowledgment numbers (4 bytes each, big-endian)
// - Data offset from upper 4 bits of byte 12, multiply by 4 for actual length
// - Parse flags from byte 13 using the TcpFlags::from_byte method
// - Window size from bytes 14-15 (big-endian)
//
// Extract payload by skipping the header (data offset bytes).
// Handle the case where there may be no payload.
//
// Build and return the TcpPacket struct.
todo!()
}
}
}
Step 3.2: Define UDP Types
#![allow(unused)]
fn main() {
// TODO: Define UDP packet structure (simpler than TCP)
// Refer to the UDP Datagram Layout - only 8 bytes total!
#[derive(Debug, Clone)]
pub struct UdpPacket {
// TODO: Add fields for UDP header
// Hint: UDP is much simpler - check the Field Details table
}
impl UdpPacket {
// TODO: Parse UDP packet
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
// TODO: Check that the data has at least 8 bytes for the UDP header.
//
// Extract the four UDP header fields from their byte positions:
// - Source port from bytes 0-1 (big-endian)
// - Destination port from bytes 2-3 (big-endian)
// - Length from bytes 4-5 (big-endian)
// - Payload starts at byte 8 and continues to the end
//
// Construct and return the UdpPacket.
todo!()
}
}
}
Step 3.3: Port Classification with Range Patterns
#![allow(unused)]
fn main() {
// TODO: Define port classes
#[derive(Debug, PartialEq)]
pub enum PortClass {
// TODO: Add variants for IANA port categories
// Hint: Four standard port ranges (0, system, registered, private/ephemeral)
}
// TODO: Classify port using range patterns
pub fn classify_port(port: u16) -> PortClass {
// TODO: Use a match expression with range patterns to classify the port.
// Port 0 is reserved, ports 1-1023 are well-known, 1024-49151 are registered,
// and 49152-65535 are dynamic/private. Use inclusive range patterns (..=).
todo!()
}
// TODO: Define common services
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Service {
// TODO: Add variants for network services you want to detect
// Hint: Web, secure web, remote access, file transfer, email, name resolution, etc.
// Hint: At least 8-9 categories including databases and unknown
}
// TODO: Detect service using or-patterns
pub fn detect_service(port: u16, protocol: IpProtocol) -> Service {
// TODO: Match on a tuple of (port, protocol) to identify common services.
// Use or-patterns (|) to match multiple ports for the same service.
// Examples: HTTP runs on ports 80, 8080, or 8000 over TCP.
// HTTPS uses 443 or 8443. DNS uses port 53 for both TCP and UDP.
// Database ports include MySQL (3306), PostgreSQL (5432), SQL Server (1433), MongoDB (27017).
// Return Service::Unknown for unrecognized port/protocol combinations.
todo!()
}
}
Step 3.4: Update Packet Enum
#![allow(unused)]
fn main() {
// TODO: Add TCP and UDP to packet enum
#[derive(Debug, Clone)]
pub enum Packet {
Ethernet {
frame: EthernetFrame,
inner: Option<Box<Packet>>,
},
IPv4 {
packet: Ipv4Packet,
inner: Option<Box<Packet>>,
},
// TODO: Add transport layer variants
// Hint: TCP and UDP packet types - what structure should they have?
Raw(Vec<u8>),
}
// TODO: Helper to check TCP flags using matches! macro
pub fn is_tcp_syn(packet: &Packet) -> bool {
// TODO: Use the matches! macro to check if this is a TCP packet
// with SYN flag set and ACK flag clear (typical connection initiation).
// Use nested struct destructuring to reach the flags field.
todo!()
}
pub fn is_tcp_syn_ack(packet: &Packet) -> bool {
// TODO: Use the matches! macro to check if this is a TCP packet
// with both SYN and ACK flags set (server's response to connection request).
todo!()
}
}
Checkpoint Tests
#![allow(unused)]
fn main() {
#[test]
fn test_port_classification() {
assert_eq!(classify_port(0), PortClass::Reserved);
assert_eq!(classify_port(80), PortClass::WellKnown);
assert_eq!(classify_port(8080), PortClass::Registered);
assert_eq!(classify_port(50000), PortClass::Dynamic);
}
#[test]
fn test_service_detection() {
assert_eq!(detect_service(80, IpProtocol::TCP), Service::Http);
assert_eq!(detect_service(443, IpProtocol::TCP), Service::Https);
assert_eq!(detect_service(22, IpProtocol::TCP), Service::SSH);
assert_eq!(detect_service(53, IpProtocol::UDP), Service::DNS);
assert_eq!(detect_service(9999, IpProtocol::TCP), Service::Unknown);
}
}
Check Your Understanding
- How do or-patterns simplify service detection across multiple ports?
- Why are range patterns better than if-else for port classification?
- How does the
matches!macro make TCP flag checking more concise? - What’s the benefit of exhaustive matching when adding new services?
Milestone 4: Firewall Rule Engine with Guards and Complex Patterns
Goal: Implement a sophisticated firewall using pattern guards and complex matching.
Concepts:
- Pattern guards for multi-criteria matching
- Deep destructuring for rule evaluation
- Exhaustive action matching
- Option handling in patterns
Implementation Steps
Step 4.1: Define Firewall Rules
#![allow(unused)]
fn main() {
// TODO: Define firewall rules
#[derive(Debug, Clone)]
pub enum FirewallRule {
// TODO: Add rule variants with increasing complexity:
// - Simple blanket rules (allow/deny everything)
// - Single port rules
// - Port range rules
// - IP address rules (single and subnet)
// - Service-based rules
// - Complex multi-criteria rules
// Hint: Each rule type should hold the data it needs to match against
}
// TODO: Define actions
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Action {
// TODO: Add variants for firewall actions
// Hint: Basic allow/deny, plus logging variants
}
}
Step 4.2: Implement Firewall Engine
#![allow(unused)]
fn main() {
// TODO: Define firewall structure
#[derive(Debug)]
pub struct Firewall {
// TODO: Add fields to store rules and default behavior
// Hint: Collection of rules plus what to do when no rules match
}
impl Firewall {
pub fn new(default_action: Action) -> Self {
// TODO: Create a new Firewall with an empty rules vector and the given default action.
todo!()
}
pub fn add_rule(&mut self, rule: FirewallRule) {
// TODO: Add the given rule to the firewall's rules vector.
todo!()
}
// TODO: Check packet against all rules
pub fn check_packet(&self, packet: &Packet) -> Action {
// TODO: Iterate through all rules in order. For each rule, try to match it
// against the packet. If a rule matches (returns Some action), return that action.
// If no rules match, return the default action.
todo!()
}
// TODO: Match a single rule using exhaustive pattern matching
fn match_rule(&self, rule: &FirewallRule, packet: &Packet) -> Option<Action> {
// TODO: Match on a tuple of (rule, packet) to handle all rule types.
//
// Simple rules: AllowAll and DenyAll match any packet.
//
// Port-based rules: Match against TCP or UDP packets, checking if the
// source or destination port matches the rule's port or falls within the range.
// Use pattern guards to check port values.
//
// IP-based rules: Match against packets containing IPv4 information,
// using deep destructuring to reach nested IP packets within Ethernet frames.
// For subnet rules, use the in_subnet helper with pattern guards.
//
// Service-based rules: Detect the service from the packet and compare
// it with the rule's service. Return the action if they match.
//
// Complex rules: Extract packet information and check each optional criterion
// (src_ip, dst_ip, src_port, dst_port, protocol). Only return the action
// if all specified criteria match.
//
// Default case: Return None if no patterns match.
todo!()
}
// TODO: Helper for subnet matching
fn in_subnet(ip: &Ipv4Address, network: &Ipv4Address, mask: u8) -> bool {
// TODO: Convert both IP addresses to u32 values using big-endian byte order.
// Create a subnet mask by left-shifting all 1s by (32 - mask) bits.
// Apply the mask to both IPs and check if they're equal.
// This determines if the IP is within the subnet.
todo!()
}
// TODO: Detect service from packet
fn detect_service_from_packet(packet: &Packet) -> Service {
// TODO: Match on the packet type to extract port and protocol information.
// For TCP packets, call detect_service with the destination port and TCP protocol.
// For UDP packets, use the destination port and UDP protocol.
// Return Service::Unknown for other packet types.
todo!()
}
}
}
Step 4.3: Packet Info Extractor
#![allow(unused)]
fn main() {
// TODO: Helper to extract packet info for complex rules
#[derive(Debug)]
struct PacketInfo {
// TODO: Add optional fields for the five-tuple
// Hint: What five pieces of information identify a network flow?
}
impl PacketInfo {
// TODO: Extract info using deep destructuring
fn extract(packet: &Packet) -> Option<Self> {
// TODO: Use nested pattern matching to extract information from different packet layers.
//
// Handle the complete stack (Ethernet containing IPv4 containing TCP/UDP):
// Use triple-nested destructuring with box patterns to reach the transport layer.
// Extract all five pieces of information: src_ip, dst_ip, src_port, dst_port, protocol.
//
// Handle partial stacks (just IPv4, just TCP, etc.):
// Create PacketInfo with only the fields that are available.
// Use None for missing fields.
//
// Return None if the packet contains no useful information for rule matching.
todo!()
}
}
}
Checkpoint Tests
#![allow(unused)]
fn main() {
#[test]
fn test_firewall_allow_port() {
let mut firewall = Firewall::new(Action::Deny);
firewall.add_rule(FirewallRule::AllowPort { port: 80 });
let tcp_packet = Packet::TCP(TcpPacket {
src_port: 1234,
dst_port: 80,
seq_num: 0,
ack_num: 0,
flags: TcpFlags::from_byte(0x02),
window_size: 8192,
payload: vec![],
});
assert_eq!(firewall.check_packet(&tcp_packet), Action::Allow);
}
#[test]
fn test_subnet_matching() {
let ip = Ipv4Address::new(192, 168, 1, 100);
let network = Ipv4Address::new(192, 168, 1, 0);
assert!(Firewall::in_subnet(&ip, &network, 24));
assert!(!Firewall::in_subnet(&ip, &network, 32));
}
}
Check Your Understanding
- How do pattern guards enable multi-criteria firewall rules?
- Why is deep destructuring useful for extracting packet info through layers?
- How does exhaustive matching prevent firewall configuration bugs?
- What’s the advantage of Option handling in complex rule matching?
Milestone 5: Connection Tracking, Statistics, and Deep Packet Inspection
Goal: Add stateful inspection, HTTP parsing, and comprehensive pattern matching.
Concepts:
- State machines with pattern matching
- While-let for stream processing
- Let-else for error handling
- Complex nested matching for threat detection
Implementation Steps
Step 5.1: Connection Tracking
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::time::{Duration, Instant};
// TODO: Define connection key for tracking
// This needs to uniquely identify a bidirectional connection
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ConnectionKey {
// TODO: Add fields for the five-tuple that identifies a connection
// Hint: Same five pieces as PacketInfo, but non-optional
}
impl ConnectionKey {
// TODO: Create canonical key (bidirectional)
fn canonical(&self) -> Self {
// TODO: Create a normalized version of the connection key so that packets
// in both directions map to the same key. Compare IPs first, then ports if
// IPs are equal. If current order is already canonical, return self.
// Otherwise, return a new ConnectionKey with examples/dst swapped.
todo!()
}
}
// TODO: Define TCP connection states
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionState {
// TODO: Add variants for TCP three-way handshake states plus UDP
// Hint: Review the TCP state machine transitions described earlier
// Hint: Unknown initial state, SYN phases, established, termination, plus UDP
}
// TODO: Connection tracking structure
#[derive(Debug, Clone)]
pub struct Connection {
// TODO: Add fields to track connection metadata
// Hint: Identity (key), current state, counters, timestamps
}
// TODO: Packet analyzer with connection tracking
pub struct PacketAnalyzer {
// TODO: Add fields for tracking state
// Hint: Map from connection keys to connection info, plus aggregate statistics
}
impl PacketAnalyzer {
pub fn new() -> Self {
// TODO: Create a new PacketAnalyzer with an empty connections HashMap
// and a default Statistics struct.
todo!()
}
// TODO: Process packet and update state
pub fn process_packet(&mut self, packet: &Packet) {
// TODO: Increment the total packet counter in statistics.
// Call update_statistics to track packet types.
// Try to extract a connection key from the packet.
// If successful, normalize it and track the connection.
todo!()
}
// TODO: Track connection state using pattern matching
fn track_connection(&mut self, key: ConnectionKey, packet: &Packet) {
// TODO: Look up the connection in the HashMap, or create a new entry if needed.
// Increment the packet count for this connection.
// Update the last_seen timestamp to now.
// Update the connection's state based on the packet.
todo!()
}
// TODO: TCP state machine using exhaustive pattern matching
fn update_connection_state(&mut self, conn: &mut Connection, packet: &Packet) {
// TODO: Implement the TCP three-way handshake state machine using pattern matching.
// Match on a tuple of (packet, current_state) and look at TCP flags:
//
// - SYN without ACK in Unknown state → transition to SynSent
// - SYN+ACK in SynSent state → transition to SynReceived
// - ACK without SYN or FIN in SynReceived → transition to Established
// - FIN in Established state → transition to FinWait
// - RST flag from any state → transition to Closed
// - UDP packets → set state to UdpActive
//
// Use nested struct destructuring to access TCP flags.
// Use a catch-all pattern for invalid state transitions.
todo!()
}
// TODO: Extract connection key using let-else
fn extract_connection_key(&self, packet: &Packet) -> Option<ConnectionKey> {
// TODO: Extract PacketInfo from the packet using the ? operator.
// Then extract each required field (src_ip, dst_ip, src_port, dst_port, protocol)
// from the PacketInfo using the ? operator to handle missing fields.
// Construct and return a ConnectionKey with all five fields.
todo!()
}
// TODO: Get active connections using pattern guards
pub fn get_active_connections(&self) -> Vec<&Connection> {
// TODO: Filter the connections HashMap to return only active connections.
// Active means: seen within the last 60 seconds AND not in Closed state.
// Use the matches! macro to check for the Closed state.
// Collect and return references to the active connections.
todo!()
}
// TODO: Cleanup old connections
pub fn cleanup_old_connections(&mut self, max_age: Duration) {
// TODO: Remove connections from the HashMap that are either:
// - Older than max_age AND in the Closed state, OR
// - Just older than max_age regardless of state
// Use HashMap's retain method with a closure that checks the time since last_seen.
todo!()
}
}
// TODO: Statistics structure
#[derive(Debug, Default)]
pub struct Statistics {
// TODO: Add counter fields for different packet types
// Hint: Total, plus per-layer and per-protocol breakdowns
}
}
Step 5.2: Connection Analysis with Pattern Matching
#![allow(unused)]
fn main() {
// TODO: Analyze connection for suspicious behavior
#[derive(Debug, PartialEq)]
pub enum ConnectionAnalysis {
// TODO: Add variants for different connection patterns
// Hint: Normal traffic, attack patterns, scanning, long connections, simple queries
}
// TODO: Analyze using exhaustive pattern matching with guards
pub fn analyze_connection(conn: &Connection) -> ConnectionAnalysis {
// TODO: Match on a tuple of (state, packet_count) to detect suspicious patterns:
// - Many packets (>100) stuck in SynSent suggests a SYN flood attack
// - Few packets (<5) in SynSent suggests port scanning
// - Many packets (>10000) in Established suggests a long-lived connection
// - Few packets (<3) for UDP suggests a simple query
// - Closed connections are marked as Closed
// - Everything else is Normal
// Use pattern guards to check packet counts.
todo!()
}
}
Step 5.3: Stream Processing with While-Let
#![allow(unused)]
fn main() {
// TODO: Process packet stream using while-let
pub fn process_packet_stream<I>(analyzer: &mut PacketAnalyzer, mut packets: I)
where
I: Iterator<Item = Packet>,
{
// TODO: Use a while-let loop to process packets from the iterator one at a time.
// Process each packet through the analyzer.
// Periodically (every 1000 packets), perform cleanup of old connections
// to prevent unbounded memory growth.
todo!()
}
}
Checkpoint Tests
#![allow(unused)]
fn main() {
#[test]
fn test_connection_tracking() {
let mut analyzer = PacketAnalyzer::new();
let syn = Packet::TCP(TcpPacket {
src_port: 1234,
dst_port: 80,
seq_num: 100,
ack_num: 0,
flags: TcpFlags {
syn: true,
ack: false,
fin: false,
rst: false,
psh: false,
urg: false,
},
window_size: 8192,
payload: vec![],
});
analyzer.process_packet(&syn);
let active = analyzer.get_active_connections();
assert_eq!(active.len(), 1);
assert_eq!(active[0].state, ConnectionState::TcpSynSent);
}
#[test]
fn test_connection_analysis() {
let syn_flood = Connection {
key: ConnectionKey {
src_ip: Ipv4Address::new(1, 1, 1, 1),
dst_ip: Ipv4Address::new(2, 2, 2, 2),
src_port: 1234,
dst_port: 80,
protocol: IpProtocol::TCP,
},
state: ConnectionState::TcpSynSent,
packets: 150,
bytes: 0,
start_time: Instant::now(),
last_seen: Instant::now(),
};
assert_eq!(analyze_connection(&syn_flood), ConnectionAnalysis::SynFlood);
}
}
Check Your Understanding
- How does pattern matching simplify TCP state machine implementation?
- Why is while-let useful for stream processing?
- How do pattern guards help identify suspicious connections?
- What’s the benefit of exhaustive matching in connection analysis?
Complete Integration Tests
#![allow(unused)]
fn main() {
#[cfg(test)]
mod integration_tests {
use super::*;
#[test]
fn test_full_packet_analysis() {
let mut analyzer = PacketAnalyzer::new();
let mut firewall = Firewall::new(Action::Allow);
// Block telnet
firewall.add_rule(FirewallRule::DenyPort { port: 23 });
// Allow HTTP/HTTPS
firewall.add_rule(FirewallRule::AllowPortRange {
start: 80,
end: 443,
});
// Test with various packets...
}
#[test]
fn test_port_scan_detection() {
let mut analyzer = PacketAnalyzer::new();
// Simulate port scan - many SYN packets to different ports
for port in 1..=100 {
let syn = Packet::TCP(TcpPacket {
src_port: 54321,
dst_port: port,
seq_num: port as u32,
ack_num: 0,
flags: TcpFlags::from_byte(0x02),
window_size: 8192,
payload: vec![],
});
analyzer.process_packet(&syn);
}
// Should have many connections in SYN_SENT state
let active = analyzer.get_active_connections();
let syn_sent_count = active
.iter()
.filter(|c| c.state == ConnectionState::TcpSynSent)
.count();
assert!(syn_sent_count > 50);
}
}
}
Benchmarks
#![allow(unused)]
fn main() {
#[cfg(test)]
mod benchmarks {
use super::*;
use std::time::Instant;
#[test]
fn bench_packet_parsing() {
let ethernet_data = vec![
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, // dst
0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // examples
0x08, 0x00, // IPv4
// Minimal IPv4 header
0x45, 0x00, 0x00, 0x3C, 0x1C, 0x46, 0x40, 0x00,
0x40, 0x06, 0xB1, 0xE6, 0xC0, 0xA8, 0x01, 0x01,
0x08, 0x08, 0x08, 0x08,
];
let start = Instant::now();
for _ in 0..100_000 {
let _ = Packet::parse(ðernet_data);
}
let elapsed = start.elapsed();
println!("Packet parsing: {:?}", elapsed);
}
#[test]
fn bench_firewall_rules() {
let mut firewall = Firewall::new(Action::Deny);
// Add 100 rules
for port in 1..=100 {
firewall.add_rule(FirewallRule::AllowPort { port });
}
let packet = Packet::TCP(TcpPacket {
src_port: 1234,
dst_port: 80,
seq_num: 0,
ack_num: 0,
flags: TcpFlags::from_byte(0x02),
window_size: 8192,
payload: vec![],
});
let start = Instant::now();
for _ in 0..100_000 {
firewall.check_packet(&packet);
}
let elapsed = start.elapsed();
println!("Firewall evaluation: {:?}", elapsed);
}
}
}
Complete Working Code
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::time::{Duration, Instant};
// =============================================================================
// Milestone 1: Ethernet packets
// =============================================================================
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MacAddress([u8; 6]);
impl MacAddress {
pub fn new(bytes: [u8; 6]) -> Self {
MacAddress(bytes)
}
pub fn is_broadcast(&self) -> bool {
self.0.iter().all(|&b| b == 0xFF)
}
pub fn is_multicast(&self) -> bool {
self.0.first().map(|b| b & 1 == 1).unwrap_or(false)
}
}
impl Display for MacAddress {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5]
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EtherType {
IPv4,
IPv6,
Arp,
Unknown(u16),
}
impl EtherType {
pub fn from_bytes(bytes: [u8; 2]) -> Self {
let value = u16::from_be_bytes(bytes);
match value {
0x0800 => EtherType::IPv4,
0x86DD => EtherType::IPv6,
0x0806 => EtherType::Arp,
other => EtherType::Unknown(other),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EthernetFrame {
pub dst_mac: MacAddress,
pub src_mac: MacAddress,
pub ethertype: EtherType,
pub payload: Vec<u8>,
}
#[derive(Debug, PartialEq)]
pub enum ParseError {
TooShort { expected: usize, found: usize },
InvalidVersion,
UnsupportedProtocol,
InvalidData,
}
impl EthernetFrame {
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
if data.len() < 14 {
return Err(ParseError::TooShort {
expected: 14,
found: data.len(),
});
}
let mut dst = [0u8; 6];
dst.copy_from_slice(&data[0..6]);
let mut src = [0u8; 6];
src.copy_from_slice(&data[6..12]);
let ethertype = EtherType::from_bytes([data[12], data[13]]);
let payload = data[14..].to_vec();
Ok(EthernetFrame {
dst_mac: MacAddress::new(dst),
src_mac: MacAddress::new(src),
ethertype,
payload,
})
}
pub fn summary(&self) -> String {
format!(
"{} -> {} ({:?})",
self.src_mac, self.dst_mac, self.ethertype
)
}
}
pub fn classify_ethernet(frame: &EthernetFrame) -> &'static str {
match frame.ethertype {
EtherType::IPv4 => "IPv4",
EtherType::IPv6 => "IPv6",
EtherType::Arp => "ARP",
EtherType::Unknown(_) => "Unknown",
}
}
pub fn is_interesting(frame: &EthernetFrame) -> bool {
matches!(frame.ethertype, EtherType::IPv4 | EtherType::IPv6) && !frame.dst_mac.is_broadcast()
}
// =============================================================================
// Milestone 2: IPv4 Parsing with Nested Destructuring
// =============================================================================
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Ipv4Address([u8; 4]);
impl Ipv4Address {
pub fn new(a: u8, b: u8, c: u8, d: u8) -> Self {
Ipv4Address([a, b, c, d])
}
pub fn is_private(&self) -> bool {
matches!(self.0, [10, ..])
|| matches!(self.0, [172, 16..=31, ..])
|| matches!(self.0, [192, 168, ..])
}
pub fn is_loopback(&self) -> bool {
matches!(self.0, [127, ..])
}
pub fn is_multicast(&self) -> bool {
matches!(self.0[0], 224..=239)
}
pub fn is_link_local(&self) -> bool {
matches!(self.0, [169, 254, ..])
}
}
impl Display for Ipv4Address {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}.{}", self.0[0], self.0[1], self.0[2], self.0[3])
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IpProtocol {
ICMP,
TCP,
UDP,
Unknown(u8),
}
impl IpProtocol {
pub fn from_u8(value: u8) -> Self {
match value {
1 => IpProtocol::ICMP,
6 => IpProtocol::TCP,
17 => IpProtocol::UDP,
other => IpProtocol::Unknown(other),
}
}
}
#[derive(Debug, Clone)]
pub struct Ipv4Packet {
pub version: u8,
pub header_length: u8,
pub total_length: u16,
pub ttl: u8,
pub protocol: IpProtocol,
pub src_ip: Ipv4Address,
pub dst_ip: Ipv4Address,
pub payload: Vec<u8>,
}
impl Ipv4Packet {
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
if data.len() < 20 {
return Err(ParseError::TooShort {
expected: 20,
found: data.len(),
});
}
let version = (data[0] >> 4) & 0x0F;
if version != 4 {
return Err(ParseError::InvalidVersion);
}
let ihl = data[0] & 0x0F;
let header_length = ihl * 4;
if data.len() < header_length as usize {
return Err(ParseError::TooShort {
expected: header_length as usize,
found: data.len(),
});
}
let total_length = u16::from_be_bytes([data[2], data[3]]);
let ttl = data[8];
let protocol = IpProtocol::from_u8(data[9]);
let src_ip = Ipv4Address([data[12], data[13], data[14], data[15]]);
let dst_ip = Ipv4Address([data[16], data[17], data[18], data[19]]);
let payload = if header_length as usize <= data.len() {
data[header_length as usize..].to_vec()
} else {
Vec::new()
};
Ok(Ipv4Packet {
version,
header_length,
total_length,
ttl,
protocol,
src_ip,
dst_ip,
payload,
})
}
}
#[derive(Debug, PartialEq)]
pub enum TrafficType {
Loopback,
Multicast,
LocalPrivate,
Outbound,
Inbound,
Internet,
LinkLocal,
Unknown,
}
pub fn classify_traffic(packet: &Ipv4Packet) -> TrafficType {
match (&packet.src_ip, &packet.dst_ip) {
(src, dst) if src.is_loopback() || dst.is_loopback() => TrafficType::Loopback,
(_, dst) if dst.is_multicast() => TrafficType::Multicast,
(src, _) if src.is_link_local() => TrafficType::LinkLocal,
(_, dst) if dst.is_link_local() => TrafficType::LinkLocal,
(src, dst) if src.is_private() && dst.is_private() => TrafficType::LocalPrivate,
(src, dst) if src.is_private() && !dst.is_private() => TrafficType::Outbound,
(src, dst) if !src.is_private() && dst.is_private() => TrafficType::Inbound,
(src, dst)
if !src.is_private()
&& !dst.is_private()
&& !src.is_loopback()
&& !dst.is_loopback() =>
{
TrafficType::Internet
}
_ => TrafficType::Unknown,
}
}
#[derive(Debug, Clone)]
pub enum Packet {
Ethernet {
frame: EthernetFrame,
inner: Option<Box<Packet>>,
},
IPv4 {
packet: Ipv4Packet,
inner: Option<Box<Packet>>,
},
TCP(TcpPacket),
UDP(UdpPacket),
Raw(Vec<u8>),
}
impl Packet {
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
let frame = EthernetFrame::parse(data)?;
let inner = match frame.ethertype {
EtherType::IPv4 => match Ipv4Packet::parse(&frame.payload) {
Ok(ip) => {
let transport = match ip.protocol {
IpProtocol::TCP => Some(Box::new(Packet::TCP(TcpPacket::parse(
&ip.payload,
)?))),
IpProtocol::UDP => Some(Box::new(Packet::UDP(UdpPacket::parse(
&ip.payload,
)?))),
_ => None,
};
Some(Box::new(Packet::IPv4 {
packet: ip,
inner: transport,
}))
}
Err(_) => None,
},
_ => None,
};
Ok(Packet::Ethernet {
frame,
inner,
})
}
pub fn extract_ips(&self) -> Option<(Ipv4Address, Ipv4Address)> {
match self {
Packet::Ethernet { inner: Some(inner), .. } => inner.extract_ips(),
Packet::IPv4 { packet, .. } => Some((packet.src_ip, packet.dst_ip)),
_ => None,
}
}
}
// =============================================================================
// Milestone 3: TCP/UDP Parsing and Port Range Matching
// =============================================================================
#[derive(Debug, Clone, Copy)]
pub struct TcpFlags {
pub ns: bool,
pub cwr: bool,
pub ece: bool,
pub urg: bool,
pub ack: bool,
pub psh: bool,
pub rst: bool,
pub syn: bool,
pub fin: bool,
}
impl TcpFlags {
pub fn from_byte(byte: u8) -> Self {
TcpFlags {
ns: false,
cwr: byte & 0b1000_0000 != 0,
ece: byte & 0b0100_0000 != 0,
urg: byte & 0b0010_0000 != 0,
ack: byte & 0b0001_0000 != 0,
psh: byte & 0b0000_1000 != 0,
rst: byte & 0b0000_0100 != 0,
syn: byte & 0b0000_0010 != 0,
fin: byte & 0b0000_0001 != 0,
}
}
}
#[derive(Debug, Clone)]
pub struct TcpPacket {
pub src_port: u16,
pub dst_port: u16,
pub seq_num: u32,
pub ack_num: u32,
pub data_offset: u8,
pub flags: TcpFlags,
pub window_size: u16,
pub payload: Vec<u8>,
}
impl TcpPacket {
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
if data.len() < 20 {
return Err(ParseError::TooShort {
expected: 20,
found: data.len(),
});
}
let src_port = u16::from_be_bytes([data[0], data[1]]);
let dst_port = u16::from_be_bytes([data[2], data[3]]);
let seq_num = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
let ack_num = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
let data_offset = (data[12] >> 4) * 4;
if data.len() < data_offset as usize {
return Err(ParseError::TooShort {
expected: data_offset as usize,
found: data.len(),
});
}
let flags = TcpFlags::from_byte(data[13]);
let window_size = u16::from_be_bytes([data[14], data[15]]);
let payload = data[data_offset as usize..].to_vec();
Ok(TcpPacket {
src_port,
dst_port,
seq_num,
ack_num,
data_offset,
flags,
window_size,
payload,
})
}
}
#[derive(Debug, Clone)]
pub struct UdpPacket {
pub src_port: u16,
pub dst_port: u16,
pub length: u16,
pub payload: Vec<u8>,
}
impl UdpPacket {
pub fn parse(data: &[u8]) -> Result<Self, ParseError> {
if data.len() < 8 {
return Err(ParseError::TooShort {
expected: 8,
found: data.len(),
});
}
let src_port = u16::from_be_bytes([data[0], data[1]]);
let dst_port = u16::from_be_bytes([data[2], data[3]]);
let length = u16::from_be_bytes([data[4], data[5]]);
let payload = data[8..].to_vec();
Ok(UdpPacket {
src_port,
dst_port,
length,
payload,
})
}
}
#[derive(Debug, PartialEq)]
pub enum PortClass {
Reserved,
WellKnown,
Registered,
Dynamic,
}
pub fn classify_port(port: u16) -> PortClass {
match port {
0 => PortClass::Reserved,
1..=1023 => PortClass::WellKnown,
1024..=49151 => PortClass::Registered,
_ => PortClass::Dynamic,
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Service {
Http,
Https,
DNS,
SSH,
FTP,
SMTP,
Telnet,
Database,
Unknown,
}
pub fn detect_service(port: u16, protocol: IpProtocol) -> Service {
match (port, protocol) {
(80 | 8080 | 8000, IpProtocol::TCP) => Service::Http,
(443 | 8443, IpProtocol::TCP) => Service::Https,
(53, IpProtocol::TCP | IpProtocol::UDP) => Service::DNS,
(22, IpProtocol::TCP) => Service::SSH,
(20 | 21, IpProtocol::TCP) => Service::FTP,
(25 | 465 | 587, IpProtocol::TCP) => Service::SMTP,
(23, IpProtocol::TCP) => Service::Telnet,
(1433 | 1521 | 3306 | 5432 | 27017, IpProtocol::TCP) => Service::Database,
_ => Service::Unknown,
}
}
pub fn is_tcp_syn(packet: &Packet) -> bool {
matches!(
packet,
Packet::TCP(TcpPacket {
flags: TcpFlags {
syn: true,
ack: false,
..
},
..
})
)
}
pub fn is_tcp_syn_ack(packet: &Packet) -> bool {
matches!(
packet,
Packet::TCP(TcpPacket {
flags: TcpFlags { syn: true, ack: true, .. },
..
})
)
}
// =============================================================================
// Milestone 4: Firewall Rule Engine with Guards and Complex Patterns
// =============================================================================
#[derive(Debug, Clone)]
pub enum FirewallRule {
AllowAll,
DenyAll,
AllowPort { port: u16 },
DenyPort { port: u16 },
AllowPortRange { start: u16, end: u16 },
DenyPortRange { start: u16, end: u16 },
AllowIp { ip: Ipv4Address },
DenyIp { ip: Ipv4Address },
AllowSubnet { network: Ipv4Address, mask: u8 },
DenySubnet { network: Ipv4Address, mask: u8 },
AllowService { service: Service },
DenyService { service: Service },
Complex {
action: Action,
src_ip: Option<Ipv4Address>,
dst_ip: Option<Ipv4Address>,
src_port: Option<u16>,
dst_port: Option<u16>,
protocol: Option<IpProtocol>,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Action {
Allow,
Deny,
LogAllow,
LogDeny,
}
#[derive(Debug)]
pub struct Firewall {
rules: Vec<FirewallRule>,
default_action: Action,
}
impl Firewall {
pub fn new(default_action: Action) -> Self {
Firewall {
rules: Vec::new(),
default_action,
}
}
pub fn add_rule(&mut self, rule: FirewallRule) {
self.rules.push(rule);
}
pub fn check_packet(&self, packet: &Packet) -> Action {
for rule in &self.rules {
if let Some(action) = self.match_rule(rule, packet) {
return action;
}
}
self.default_action
}
fn match_rule(&self, rule: &FirewallRule, packet: &Packet) -> Option<Action> {
match (rule, packet) {
(FirewallRule::AllowAll, _) => Some(Action::Allow),
(FirewallRule::DenyAll, _) => Some(Action::Deny),
(FirewallRule::AllowPort { port }, Packet::TCP(tcp))
if tcp.src_port == *port || tcp.dst_port == *port =>
{
Some(Action::Allow)
}
(FirewallRule::AllowPort { port }, Packet::UDP(udp))
if udp.src_port == *port || udp.dst_port == *port =>
{
Some(Action::Allow)
}
(FirewallRule::DenyPort { port }, Packet::TCP(tcp))
if tcp.src_port == *port || tcp.dst_port == *port =>
{
Some(Action::Deny)
}
(FirewallRule::DenyPort { port }, Packet::UDP(udp))
if udp.src_port == *port || udp.dst_port == *port =>
{
Some(Action::Deny)
}
(FirewallRule::AllowPortRange { start, end }, Packet::TCP(tcp))
if (*start..=*end).contains(&tcp.src_port)
|| (*start..=*end).contains(&tcp.dst_port) =>
{
Some(Action::Allow)
}
(FirewallRule::AllowPortRange { start, end }, Packet::UDP(udp))
if (*start..=*end).contains(&udp.src_port)
|| (*start..=*end).contains(&udp.dst_port) =>
{
Some(Action::Allow)
}
(FirewallRule::DenyPortRange { start, end }, Packet::TCP(tcp))
if (*start..=*end).contains(&tcp.src_port)
|| (*start..=*end).contains(&tcp.dst_port) =>
{
Some(Action::Deny)
}
(FirewallRule::DenyPortRange { start, end }, Packet::UDP(udp))
if (*start..=*end).contains(&udp.src_port)
|| (*start..=*end).contains(&udp.dst_port) =>
{
Some(Action::Deny)
}
(FirewallRule::AllowIp { ip }, packet)
if packet.extract_ips().map(|(src, dst)| src == *ip || dst == *ip).unwrap_or(false) =>
{
Some(Action::Allow)
}
(FirewallRule::DenyIp { ip }, packet)
if packet.extract_ips().map(|(src, dst)| src == *ip || dst == *ip).unwrap_or(false) =>
{
Some(Action::Deny)
}
(FirewallRule::AllowSubnet { network, mask }, packet)
if packet
.extract_ips()
.map(|(src, dst)| {
Self::in_subnet(&src, network, *mask)
|| Self::in_subnet(&dst, network, *mask)
})
.unwrap_or(false) =>
{
Some(Action::Allow)
}
(FirewallRule::DenySubnet { network, mask }, packet)
if packet
.extract_ips()
.map(|(src, dst)| {
Self::in_subnet(&src, network, *mask)
|| Self::in_subnet(&dst, network, *mask)
})
.unwrap_or(false) =>
{
Some(Action::Deny)
}
(FirewallRule::AllowService { service }, packet)
if *service == Self::detect_service_from_packet(packet) =>
{
Some(Action::Allow)
}
(FirewallRule::DenyService { service }, packet)
if *service == Self::detect_service_from_packet(packet) =>
{
Some(Action::Deny)
}
(
FirewallRule::Complex {
action,
src_ip,
dst_ip,
src_port,
dst_port,
protocol,
},
packet,
) => {
if let Some(info) = PacketInfo::extract(packet) {
let ip_match = src_ip.map_or(true, |ip| info.src_ip == Some(ip))
&& dst_ip.map_or(true, |ip| info.dst_ip == Some(ip));
let port_match = src_port.map_or(true, |p| info.src_port == Some(p))
&& dst_port.map_or(true, |p| info.dst_port == Some(p));
let proto_match =
protocol.map_or(true, |proto| info.protocol == Some(proto));
if ip_match && port_match && proto_match {
return Some(*action);
}
}
None
}
_ => None,
}
}
fn in_subnet(ip: &Ipv4Address, network: &Ipv4Address, mask: u8) -> bool {
let ip_val = u32::from_be_bytes(ip.0);
let net_val = u32::from_be_bytes(network.0);
let mask_val = if mask == 0 {
0
} else {
u32::MAX << (32 - mask as u32)
};
(ip_val & mask_val) == (net_val & mask_val)
}
fn detect_service_from_packet(packet: &Packet) -> Service {
match packet {
Packet::TCP(tcp) => detect_service(tcp.dst_port, IpProtocol::TCP),
Packet::UDP(udp) => detect_service(udp.dst_port, IpProtocol::UDP),
Packet::Ethernet { inner: Some(inner), .. }
| Packet::IPv4 { inner: Some(inner), .. } => Self::detect_service_from_packet(inner),
_ => Service::Unknown,
}
}
}
#[derive(Debug)]
struct PacketInfo {
src_ip: Option<Ipv4Address>,
dst_ip: Option<Ipv4Address>,
src_port: Option<u16>,
dst_port: Option<u16>,
protocol: Option<IpProtocol>,
}
impl PacketInfo {
fn extract(packet: &Packet) -> Option<Self> {
match packet {
Packet::Ethernet { inner: Some(inner), .. } => PacketInfo::extract(inner),
Packet::IPv4 { packet, inner } => {
let mut info = PacketInfo {
src_ip: Some(packet.src_ip),
dst_ip: Some(packet.dst_ip),
src_port: None,
dst_port: None,
protocol: Some(packet.protocol),
};
if let Some(inner) = inner {
if let Some(mut inner_info) = PacketInfo::extract(inner) {
if info.src_port.is_none() {
info.src_port = inner_info.src_port.take();
}
if info.dst_port.is_none() {
info.dst_port = inner_info.dst_port.take();
}
}
}
Some(info)
}
Packet::TCP(tcp) => Some(PacketInfo {
src_ip: Some(Ipv4Address::new(0, 0, 0, 0)),
dst_ip: Some(Ipv4Address::new(0, 0, 0, 0)),
src_port: Some(tcp.src_port),
dst_port: Some(tcp.dst_port),
protocol: Some(IpProtocol::TCP),
}),
Packet::UDP(udp) => Some(PacketInfo {
src_ip: Some(Ipv4Address::new(0, 0, 0, 0)),
dst_ip: Some(Ipv4Address::new(0, 0, 0, 0)),
src_port: Some(udp.src_port),
dst_port: Some(udp.dst_port),
protocol: Some(IpProtocol::UDP),
}),
_ => None,
}
}
}
// =============================================================================
// Milestone 5: Connection Tracking, Statistics, and Deep Packet Inspection
// =============================================================================
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ConnectionKey {
pub src_ip: Ipv4Address,
pub dst_ip: Ipv4Address,
pub src_port: u16,
pub dst_port: u16,
pub protocol: IpProtocol,
}
impl ConnectionKey {
fn canonical(&self) -> Self {
if (self.src_ip.0, self.src_port) <= (self.dst_ip.0, self.dst_port) {
*self
} else {
ConnectionKey {
src_ip: self.dst_ip,
dst_ip: self.src_ip,
src_port: self.dst_port,
dst_port: self.src_port,
protocol: self.protocol,
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConnectionState {
Unknown,
TcpSynSent,
TcpSynReceived,
TcpEstablished,
TcpFinWait,
TcpClosed,
TcpReset,
UdpActive,
}
#[derive(Debug, Clone)]
pub struct Connection {
pub key: ConnectionKey,
pub state: ConnectionState,
pub packets: usize,
pub bytes: usize,
pub start_time: Instant,
pub last_seen: Instant,
}
pub struct PacketAnalyzer {
connections: HashMap<ConnectionKey, Connection>,
stats: Statistics,
}
impl PacketAnalyzer {
pub fn new() -> Self {
PacketAnalyzer {
connections: HashMap::new(),
stats: Statistics::default(),
}
}
pub fn process_packet(&mut self, packet: &Packet) {
self.stats.total_packets += 1;
match packet {
Packet::Ethernet { .. } => self.stats.ethernet_frames += 1,
Packet::IPv4 { packet, .. } => {
self.stats.ipv4_packets += 1;
match packet.protocol {
IpProtocol::TCP => self.stats.tcp_packets += 1,
IpProtocol::UDP => self.stats.udp_packets += 1,
_ => {}
}
}
Packet::TCP(_) => self.stats.tcp_packets += 1,
Packet::UDP(_) => self.stats.udp_packets += 1,
Packet::Raw(_) => {}
}
if let Some(key) = self.extract_connection_key(packet) {
let canonical = key.canonical();
self.track_connection(canonical, packet);
}
}
fn track_connection(&mut self, key: ConnectionKey, packet: &Packet) {
let now = Instant::now();
let entry = self.connections.entry(key).or_insert(Connection {
key,
state: ConnectionState::Unknown,
packets: 0,
bytes: 0,
start_time: now,
last_seen: now,
});
entry.packets += 1;
entry.last_seen = now;
entry.bytes += match packet {
Packet::TCP(tcp) => tcp.payload.len(),
Packet::UDP(udp) => udp.payload.len(),
Packet::IPv4 { packet, .. } => packet.payload.len(),
Packet::Ethernet { frame, .. } => frame.payload.len(),
Packet::Raw(data) => data.len(),
};
Self::update_connection_state(entry, packet);
}
fn update_connection_state(conn: &mut Connection, packet: &Packet) {
match (packet, conn.state.clone()) {
(
Packet::TCP(TcpPacket {
flags: TcpFlags {
syn: true,
ack: false,
rst: false,
fin: false,
..
},
..
}),
ConnectionState::Unknown,
) => {
conn.state = ConnectionState::TcpSynSent;
}
(
Packet::TCP(TcpPacket {
flags: TcpFlags {
syn: true,
ack: true,
rst: false,
..
},
..
}),
ConnectionState::TcpSynSent,
) => {
conn.state = ConnectionState::TcpSynReceived;
}
(
Packet::TCP(TcpPacket {
flags: TcpFlags {
ack: true,
syn: false,
fin: false,
rst: false,
..
},
..
}),
ConnectionState::TcpSynReceived,
) => {
conn.state = ConnectionState::TcpEstablished;
}
(
Packet::TCP(TcpPacket {
flags: TcpFlags {
fin: true,
rst: false,
..
},
..
}),
ConnectionState::TcpEstablished,
) => {
conn.state = ConnectionState::TcpFinWait;
}
(
Packet::TCP(TcpPacket {
flags: TcpFlags { rst: true, .. },
..
}),
_,
) => {
conn.state = ConnectionState::TcpReset;
}
(
Packet::TCP(TcpPacket {
flags: TcpFlags { fin: true, .. },
..
}),
ConnectionState::TcpFinWait,
) => {
conn.state = ConnectionState::TcpClosed;
}
(Packet::UDP(_), _) => {
conn.state = ConnectionState::UdpActive;
}
_ => {}
}
}
fn extract_connection_key(&self, packet: &Packet) -> Option<ConnectionKey> {
let info = PacketInfo::extract(packet)?;
Some(ConnectionKey {
src_ip: info.src_ip?,
dst_ip: info.dst_ip?,
src_port: info.src_port?,
dst_port: info.dst_port?,
protocol: info.protocol?,
})
}
pub fn get_active_connections(&self) -> Vec<&Connection> {
let now = Instant::now();
self.connections
.values()
.filter(|conn| {
now.duration_since(conn.last_seen) <= Duration::from_secs(60)
&& !matches!(conn.state, ConnectionState::TcpClosed)
})
.collect()
}
pub fn cleanup_old_connections(&mut self, max_age: Duration) {
let now = Instant::now();
self.connections.retain(|_, conn| {
let age = now.duration_since(conn.last_seen);
if age > max_age && matches!(conn.state, ConnectionState::TcpClosed) {
return false;
}
age <= max_age
});
}
}
#[derive(Debug, Default)]
pub struct Statistics {
pub total_packets: usize,
pub ethernet_frames: usize,
pub ipv4_packets: usize,
pub tcp_packets: usize,
pub udp_packets: usize,
}
#[derive(Debug, PartialEq)]
pub enum ConnectionAnalysis {
Normal,
SynFlood,
PortScan,
LongLived,
UdpQuery,
Closed,
}
pub fn analyze_connection(conn: &Connection) -> ConnectionAnalysis {
match (conn.state.clone(), conn.packets) {
(ConnectionState::TcpSynSent, count) if count > 100 => ConnectionAnalysis::SynFlood,
(ConnectionState::TcpSynSent, count) if count < 5 => ConnectionAnalysis::PortScan,
(ConnectionState::TcpEstablished, count) if count > 10_000 => {
ConnectionAnalysis::LongLived
}
(ConnectionState::UdpActive, count) if count < 3 => ConnectionAnalysis::UdpQuery,
(ConnectionState::TcpClosed | ConnectionState::TcpReset, _) => ConnectionAnalysis::Closed,
_ => ConnectionAnalysis::Normal,
}
}
pub fn process_packet_stream<I>(analyzer: &mut PacketAnalyzer, mut packets: I)
where
I: Iterator<Item = Packet>,
{
let mut processed = 0usize;
while let Some(packet) = packets.next() {
analyzer.process_packet(&packet);
processed += 1;
if processed % 1000 == 0 {
analyzer.cleanup_old_connections(Duration::from_secs(60));
}
}
}
// =============================================================================
// Tests
// =============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mac_address() {
let mac = MacAddress([0xFF; 6]);
assert!(mac.is_broadcast());
let multicast = MacAddress([0x01, 0x00, 0x5E, 0x00, 0x00, 0x01]);
assert!(multicast.is_multicast());
}
#[test]
fn test_ethernet_parsing() {
let data = vec![
0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
0x08, 0x00,
0x45, 0x00, 0x00, 0x3C,
];
let frame = EthernetFrame::parse(&data).unwrap();
assert_eq!(frame.dst_mac, MacAddress([0x00, 0x11, 0x22, 0x33, 0x44, 0x55]));
assert_eq!(frame.src_mac, MacAddress([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]));
assert_eq!(frame.ethertype, EtherType::IPv4);
assert_eq!(frame.payload.len(), 4);
}
#[test]
fn test_too_short() {
let data = vec![0x00, 0x11, 0x22];
let result = EthernetFrame::parse(&data);
assert_eq!(result, Err(ParseError::TooShort { expected: 14, found: 3 }));
}
#[test]
fn test_ipv4_address_classification() {
let private = Ipv4Address::new(192, 168, 1, 1);
assert!(private.is_private());
let public = Ipv4Address::new(8, 8, 8, 8);
assert!(!public.is_private());
let loopback = Ipv4Address::new(127, 0, 0, 1);
assert!(loopback.is_loopback());
let multicast = Ipv4Address::new(224, 0, 0, 1);
assert!(multicast.is_multicast());
}
#[test]
fn test_traffic_classification() {
let local = Ipv4Packet {
version: 4,
header_length: 20,
total_length: 60,
ttl: 64,
protocol: IpProtocol::TCP,
src_ip: Ipv4Address::new(192, 168, 1, 1),
dst_ip: Ipv4Address::new(192, 168, 1, 2),
payload: vec![],
};
assert_eq!(classify_traffic(&local), TrafficType::LocalPrivate);
}
#[test]
fn test_port_classification() {
assert_eq!(classify_port(0), PortClass::Reserved);
assert_eq!(classify_port(80), PortClass::WellKnown);
assert_eq!(classify_port(8080), PortClass::Registered);
assert_eq!(classify_port(50000), PortClass::Dynamic);
}
#[test]
fn test_service_detection() {
assert_eq!(detect_service(80, IpProtocol::TCP), Service::Http);
assert_eq!(detect_service(443, IpProtocol::TCP), Service::Https);
assert_eq!(detect_service(22, IpProtocol::TCP), Service::SSH);
assert_eq!(detect_service(53, IpProtocol::UDP), Service::DNS);
assert_eq!(detect_service(9999, IpProtocol::TCP), Service::Unknown);
}
#[test]
fn test_firewall_allow_port() {
let mut firewall = Firewall::new(Action::Deny);
firewall.add_rule(FirewallRule::AllowPort { port: 80 });
let tcp_packet = Packet::TCP(TcpPacket {
src_port: 1234,
dst_port: 80,
seq_num: 0,
ack_num: 0,
data_offset: 20,
flags: TcpFlags::from_byte(0x02),
window_size: 8192,
payload: vec![],
});
assert_eq!(firewall.check_packet(&tcp_packet), Action::Allow);
}
#[test]
fn test_subnet_matching() {
let ip = Ipv4Address::new(192, 168, 1, 100);
let network = Ipv4Address::new(192, 168, 1, 0);
assert!(Firewall::in_subnet(&ip, &network, 24));
assert!(!Firewall::in_subnet(&ip, &network, 32));
}
#[test]
fn test_connection_tracking() {
let mut analyzer = PacketAnalyzer::new();
let syn = Packet::TCP(TcpPacket {
src_port: 1234,
dst_port: 80,
seq_num: 100,
ack_num: 0,
data_offset: 20,
flags: TcpFlags {
syn: true,
ack: false,
fin: false,
rst: false,
psh: false,
urg: false,
cwr: false,
ece: false,
ns: false,
},
window_size: 8192,
payload: vec![],
});
analyzer.process_packet(&syn);
let active = analyzer.get_active_connections();
assert_eq!(active.len(), 1);
assert_eq!(active[0].state, ConnectionState::TcpSynSent);
}
#[test]
fn test_connection_analysis() {
let syn_flood = Connection {
key: ConnectionKey {
src_ip: Ipv4Address::new(1, 1, 1, 1),
dst_ip: Ipv4Address::new(2, 2, 2, 2),
src_port: 1234,
dst_port: 80,
protocol: IpProtocol::TCP,
},
state: ConnectionState::TcpSynSent,
packets: 150,
bytes: 0,
start_time: Instant::now(),
last_seen: Instant::now(),
};
assert_eq!(analyze_connection(&syn_flood), ConnectionAnalysis::SynFlood);
}
}
}