Add TUN/TAP interface support (#43)

* feat: add TUN/TAP interface support

Add comprehensive support for TUN/TAP virtual network interfaces by
refactoring link layer parsing into modular components.

New modules:
- link_layer: Modular packet parsing (ethernet, raw_ip, linux_sll, tun_tap)
- protocol: Dedicated TCP/UDP/ICMP parsers

Changes:
- Remove TUN/TAP interface exclusions in capture.rs
- Add TUN/TAP detection and parsing support
- macOS PKTAP support with conditional compilation

Platform compatibility:
- Linux: Full TUN/TAP support
- macOS: TUN (utun*) and TAP support
- Windows: No breaking changes

Fixes #39
This commit is contained in:
Marco Cadetg
2025-10-11 14:10:50 +02:00
committed by GitHub
parent 5ad0095b91
commit 0d55a86605
17 changed files with 1753 additions and 499 deletions
+43 -2
View File
@@ -31,13 +31,18 @@ rustnet
**Basic usage examples:**
```bash
# Run with default settings (monitors default interface)
# Run with default settings
# macOS: Uses PKTAP for process metadata
# Linux/Other: Auto-detects active interface
rustnet
# Specify network interface
rustnet -i eth0
rustnet --interface wlan0
# Linux: Monitor all interfaces simultaneously
rustnet -i any
# Filter out localhost connections (already filtered by default)
rustnet --no-localhost
@@ -79,15 +84,51 @@ Options:
#### `-i, --interface <INTERFACE>`
Specify which network interface to monitor. If not provided, RustNet will use the first available non-loopback interface.
Specify which network interface to monitor.
**Default behavior (no `-i` flag):**
- **macOS**: Automatically uses PKTAP for enhanced process metadata (requires sudo)
- **Linux/Other**: Auto-detects the first available non-loopback interface
**Examples:**
```bash
# Default: Auto-detect interface (PKTAP on macOS)
rustnet
# Linux: Monitor all interfaces using the special "any" pseudo-interface
rustnet -i any
# Monitor specific interfaces
rustnet -i eth0 # Monitor Ethernet interface
rustnet -i wlan0 # Monitor WiFi interface
rustnet -i en0 # Monitor macOS primary interface
# Monitor VPN and tunnel interfaces (TUN/TAP support)
rustnet -i utun0 # macOS VPN tunnel (TUN, Layer 3)
rustnet -i tun0 # Linux/BSD VPN tunnel (TUN, Layer 3)
rustnet -i tap0 # TAP interface (Layer 2, includes Ethernet)
```
**TUN/TAP Interface Support:**
RustNet fully supports monitoring VPN and virtual network interfaces:
- **TUN interfaces** (Layer 3): Carry IP packets directly without Ethernet headers
- Common on VPNs: WireGuard, OpenVPN (tun mode), Tailscale
- Examples: `utun0-utun9` (macOS), `tun0-tun9` (Linux/BSD)
- **TAP interfaces** (Layer 2): Include full Ethernet frames
- Used by: OpenVPN (tap mode), QEMU/KVM virtual networks, Docker
- Examples: `tap0-tap9` (Linux/BSD)
RustNet automatically detects TUN/TAP interfaces and adjusts packet parsing accordingly. The interface type is displayed in the UI status area.
**Platform-specific notes:**
- **macOS**: Without `-i`, PKTAP is used automatically for better process detection. Use `-i <interface>` to monitor a specific interface instead
- **Linux**: Use `-i any` to capture on all interfaces simultaneously (not available on other platforms)
- **TUN/TAP**: Fully supported on all platforms - RustNet detects interface type by name and adjusts parsing
- **All platforms**: If you specify a non-existent interface, an error will show available interfaces
**Finding your interfaces:**
- Linux: `ip link show` or `ifconfig`
- macOS: `ifconfig` or `networksetup -listallhardwareports`
+27 -1
View File
@@ -188,6 +188,9 @@ impl App {
/// Start packet capture thread
fn start_capture_thread(&self, packet_tx: Sender<Vec<u8>>) -> Result<()> {
// Validate interface exists before spawning thread (fail fast)
crate::network::capture::validate_interface(&self.config.interface)?;
let capture_config = CaptureConfig {
interface: self.config.interface.clone(),
filter: self.config.bpf_filter.clone(),
@@ -210,7 +213,7 @@ impl App {
// Check if PKTAP is active (linktype 149 or 258)
#[cfg(target_os = "macos")]
{
use crate::network::pktap;
use crate::network::link_layer::pktap;
if pktap::is_pktap_linktype(linktype) {
_pktap_active.store(true, Ordering::Relaxed);
info!("✓ PKTAP is active - process metadata will be provided directly");
@@ -791,6 +794,29 @@ impl App {
.unwrap_or_else(|_| String::from("unknown"))
}
/// Get link layer information for the current interface
/// Returns (link_layer_type_name, is_tunnel)
pub fn get_link_layer_info(&self) -> (String, bool) {
use crate::network::link_layer::LinkLayerType;
if let Ok(linktype_opt) = self.linktype.read()
&& let Some(dlt) = *linktype_opt
{
// Get interface name to detect TUN/TAP more accurately
let interface_name = self.current_interface
.read()
.ok()
.and_then(|opt| opt.clone())
.unwrap_or_default();
let link_type = LinkLayerType::from_dlt_and_name(dlt, &interface_name);
let type_name = format!("{:?}", link_type);
let is_tunnel = link_type.is_tunnel();
return (type_name, is_tunnel);
}
(String::from("Unknown"), false)
}
/// Stop all threads gracefully
pub fn stop(&self) {
info!("Stopping application");
+74 -20
View File
@@ -69,12 +69,19 @@ fn find_best_device() -> Result<Device> {
// Find the best active device
let suitable_device = devices
.iter()
// First priority: up, running, and has a valid IP address
// First priority: up, running, has a valid IP address, and NOT virtual
.find(|d| {
// Check if it's a virtual/problematic interface
let desc_lower = d.desc.as_ref().map(|s| s.to_lowercase()).unwrap_or_default();
let is_virtual = desc_lower.contains("hyper-v")
|| desc_lower.contains("vmware")
|| desc_lower.contains("virtualbox");
!d.name.starts_with("lo")
// Note: 'any' is excluded here because it's not a real interface
// Users can still specify '-i any' explicitly on Linux
&& d.name != "any"
&& !is_virtual // Skip virtual adapters in first priority too
&& d.flags.is_up()
&& d.flags.is_running()
&& d.addresses.iter().any(|addr| {
@@ -97,16 +104,25 @@ fn find_best_device() -> Result<Device> {
// Third priority: any up interface with valid addresses (excluding problematic ones)
.or_else(|| {
devices.iter().find(|d| {
// Check if it's a virtual/problematic interface
let desc_lower = d.desc.as_ref().map(|s| s.to_lowercase()).unwrap_or_default();
let is_virtual = desc_lower.contains("hyper-v")
|| desc_lower.contains("virtual")
|| desc_lower.contains("vmware")
|| desc_lower.contains("virtualbox")
|| desc_lower.contains("loopback");
!d.name.starts_with("lo") &&
!d.name.starts_with("ap") && // Skip Apple's ap interfaces
!d.name.starts_with("awdl") && // Skip Apple Wireless Direct
!d.name.starts_with("llw") && // Skip Low latency WLAN
!d.name.starts_with("bridge") && // Skip bridges
!d.name.starts_with("utun") && // Skip tunnels
// TUN/TAP interfaces now supported - removed utun/tun/tap exclusion
!d.name.starts_with("vmnet") && // Skip VM interfaces
// Note: 'any' is excluded here because it's not a real interface
// Users can still specify '-i any' explicitly on Linux
d.name != "any" &&
!is_virtual && // Skip virtual adapters
d.flags.is_up() &&
!d.addresses.is_empty()
})
@@ -137,9 +153,9 @@ fn find_best_device() -> Result<Device> {
/// Setup packet capture with the given configuration
pub fn setup_packet_capture(config: CaptureConfig) -> Result<(Capture<Active>, String, i32)> {
// Try PKTAP first on macOS for process metadata
// Try PKTAP first on macOS for process metadata, but only when no interface is explicitly specified
#[cfg(target_os = "macos")]
{
if config.interface.is_none() {
log::info!("Attempting to use PKTAP for process metadata on macOS");
match Capture::from_device("pktap") {
@@ -199,10 +215,26 @@ pub fn setup_packet_capture(config: CaptureConfig) -> Result<(Capture<Active>, S
log::info!("Setting up regular packet capture");
let device = find_capture_device(&config.interface)?;
// Check if this is a TUN/TAP interface
use crate::network::link_layer::tun_tap;
let is_tunnel = tun_tap::is_tunnel_interface(&device.name);
let tunnel_type = if tun_tap::is_tun_interface(&device.name) {
"TUN (Layer 3)"
} else if tun_tap::is_tap_interface(&device.name) {
"TAP (Layer 2)"
} else {
"N/A"
};
log::info!(
"Setting up capture on device: {} ({})",
"Setting up capture on device: {} ({}){}",
device.name,
device.desc.as_deref().unwrap_or("no description")
device.desc.as_deref().unwrap_or("no description"),
if is_tunnel {
format!(" [Tunnel: {}]", tunnel_type)
} else {
String::new()
}
);
let device_name = device.name.clone();
@@ -239,6 +271,16 @@ pub fn setup_packet_capture(config: CaptureConfig) -> Result<(Capture<Active>, S
Ok((cap, device_name, linktype.0))
}
/// Validate that the specified interface exists (if provided)
/// This is useful for failing fast before starting capture threads
pub fn validate_interface(interface_name: &Option<String>) -> Result<()> {
if let Some(name) = interface_name {
// This will return an error if the interface doesn't exist
find_capture_device(&Some(name.clone()))?;
}
Ok(())
}
/// Find a capture device by name or return the default
fn find_capture_device(interface_name: &Option<String>) -> Result<Device> {
match interface_name {
@@ -279,19 +321,13 @@ fn find_capture_device(interface_name: &Option<String>) -> Result<Device> {
// List available interfaces for error message
let available: Vec<String> = devices
.iter()
.map(|d| {
format!(
"{} ({})",
d.name,
d.desc.as_deref().unwrap_or("no description")
)
})
.map(|d| d.name.clone())
.collect();
Err(anyhow!(
"Interface '{}' not found. Available interfaces:\n{}",
"Interface '{}' not found. Available interfaces: {}",
name,
available.join("\n")
available.join(", ")
))
}
None => {
@@ -322,7 +358,7 @@ fn find_capture_device(interface_name: &Option<String>) -> Result<Device> {
|| device.name.starts_with("awdl")
|| device.name.starts_with("llw")
|| device.name.starts_with("bridge")
|| device.name.starts_with("utun")
// TUN/TAP interfaces now supported - removed utun/tun/tap check
|| device.name.starts_with("vmnet")
|| (device.name == "any" && !cfg!(target_os = "linux"))
|| device.flags.is_loopback();
@@ -381,11 +417,23 @@ impl PacketReader {
/// Get capture statistics
pub fn stats(&mut self) -> Result<CaptureStats> {
let stats = self.capture.stats()?;
Ok(CaptureStats {
let capture_stats = CaptureStats {
received: stats.received,
dropped: stats.dropped,
if_dropped: stats.if_dropped,
})
};
// Log dropped packets if any occurred
if capture_stats.total_dropped() > 0 {
log::debug!(
"Total {} packets dropped (kernel: {}, interface: {})",
capture_stats.total_dropped(),
capture_stats.dropped,
capture_stats.if_dropped
);
}
Ok(capture_stats)
}
}
@@ -394,11 +442,17 @@ impl PacketReader {
pub struct CaptureStats {
pub received: u32,
pub dropped: u32,
#[allow(dead_code)]
// TODO: implement interface-specific dropped packets
/// Interface-level dropped packets (platform-specific)
pub if_dropped: u32,
}
impl CaptureStats {
/// Get total packets dropped (both kernel and interface level)
pub fn total_dropped(&self) -> u32 {
self.dropped.saturating_add(self.if_dropped)
}
}
#[cfg(test)]
mod tests {
use super::*;
+63
View File
@@ -0,0 +1,63 @@
//! Ethernet (IEEE 802.3) frame parsing
//!
//! Handles DLT_EN10MB (Ethernet) frames with 14-byte header
use crate::network::parser::{PacketParser, ParsedPacket};
/// Parse an Ethernet frame and extract the network layer packet
///
/// Ethernet frame format (14 bytes):
/// - Destination MAC (6 bytes)
/// - Source MAC (6 bytes)
/// - EtherType (2 bytes)
///
/// Returns the parsed packet if successful
pub fn parse(
data: &[u8],
parser: &PacketParser,
process_name: Option<String>,
process_id: Option<u32>,
) -> Option<ParsedPacket> {
if data.len() < 14 {
log::debug!("Ethernet frame too small: {} bytes", data.len());
return None;
}
// Extract EtherType from bytes 12-13
let ethertype = u16::from_be_bytes([data[12], data[13]]);
match ethertype {
0x0800 => {
// IPv4
log::trace!("Ethernet: IPv4 packet detected");
parser.parse_ipv4_packet_inner(data, process_name, process_id)
}
0x86dd => {
// IPv6
log::trace!("Ethernet: IPv6 packet detected");
parser.parse_ipv6_packet_inner(data, process_name, process_id)
}
0x0806 => {
// ARP
log::trace!("Ethernet: ARP packet detected");
parser.parse_arp_packet_inner(data, process_name, process_id)
}
_ => {
log::debug!("Ethernet: Unknown EtherType: 0x{:04x}", ethertype);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ethernet_frame_too_small() {
// Ethernet frames must be at least 14 bytes
let small_frame = vec![0x00, 0x11, 0x22];
let parser = PacketParser::new();
assert!(parse(&small_frame, &parser, None, None).is_none());
}
}
+115
View File
@@ -0,0 +1,115 @@
//! Linux "cooked" capture parsing
//!
//! Handles DLT_LINUX_SLL (113) and DLT_LINUX_SLL2 (276)
//! Used by the Linux "any" pseudo-interface
use crate::network::parser::{PacketParser, ParsedPacket};
/// Parse Linux Cooked Capture v1 packet (DLT_LINUX_SLL)
///
/// Header format (16 bytes):
/// - Packet type (2 bytes)
/// - ARPHRD type (2 bytes)
/// - Link-layer address length (2 bytes)
/// - Link-layer address (8 bytes)
/// - Protocol type (2 bytes) - EtherType
///
/// IP payload starts at byte 16
pub fn parse_sll(
data: &[u8],
parser: &PacketParser,
process_name: Option<String>,
process_id: Option<u32>,
) -> Option<ParsedPacket> {
if data.len() < 16 {
log::debug!("Linux SLL packet too small: {} bytes", data.len());
return None;
}
// Protocol type is at bytes 14-15 (EtherType)
let protocol = u16::from_be_bytes([data[14], data[15]]);
match protocol {
0x0800 => {
// IPv4 - payload starts at byte 16
log::trace!("Linux SLL: IPv4 packet detected");
let ip_data = &data[16..];
parser.parse_raw_ipv4_packet(ip_data, process_name, process_id)
}
0x86dd => {
// IPv6 - payload starts at byte 16
log::trace!("Linux SLL: IPv6 packet detected");
let ip_data = &data[16..];
parser.parse_raw_ipv6_packet(ip_data, process_name, process_id)
}
_ => {
log::debug!("Linux SLL: Unknown protocol: 0x{:04x}", protocol);
None
}
}
}
/// Parse Linux Cooked Capture v2 packet (DLT_LINUX_SLL2)
///
/// Header format (20 bytes):
/// - Protocol type (2 bytes) - EtherType
/// - Reserved (2 bytes)
/// - Interface index (4 bytes)
/// - ARPHRD type (2 bytes)
/// - Packet type (1 byte)
/// - Link-layer address length (1 byte)
/// - Link-layer address (8 bytes)
///
/// IP payload starts at byte 20
pub fn parse_sll2(
data: &[u8],
parser: &PacketParser,
process_name: Option<String>,
process_id: Option<u32>,
) -> Option<ParsedPacket> {
if data.len() < 20 {
log::debug!("Linux SLL2 packet too small: {} bytes", data.len());
return None;
}
// Protocol type is at bytes 0-1 (EtherType)
let protocol = u16::from_be_bytes([data[0], data[1]]);
match protocol {
0x0800 => {
// IPv4 - payload starts at byte 20
log::trace!("Linux SLL2: IPv4 packet detected");
let ip_data = &data[20..];
parser.parse_raw_ipv4_packet(ip_data, process_name, process_id)
}
0x86dd => {
// IPv6 - payload starts at byte 20
log::trace!("Linux SLL2: IPv6 packet detected");
let ip_data = &data[20..];
parser.parse_raw_ipv6_packet(ip_data, process_name, process_id)
}
_ => {
log::debug!("Linux SLL2: Unknown protocol: 0x{:04x}", protocol);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sll_packet_too_small() {
let small_packet = vec![0x00; 10];
let parser = PacketParser::new();
assert!(parse_sll(&small_packet, &parser, None, None).is_none());
}
#[test]
fn test_sll2_packet_too_small() {
let small_packet = vec![0x00; 15];
let parser = PacketParser::new();
assert!(parse_sll2(&small_packet, &parser, None, None).is_none());
}
}
+152
View File
@@ -0,0 +1,152 @@
//! Link layer (Layer 2) packet parsing
//!
//! This module handles data-link layer protocols and extracts network-layer packets:
//! - Ethernet (DLT_EN10MB)
//! - Linux Cooked Capture v1 and v2 (DLT_LINUX_SLL, DLT_LINUX_SLL2)
//! - Raw IP packets (DLT_RAW, LINKTYPE_IPV4, LINKTYPE_IPV6)
//! - TUN/TAP interfaces
//! - PKTAP (macOS process metadata)
pub mod ethernet;
pub mod linux_sll;
#[cfg(target_os = "macos")]
pub mod pktap;
pub mod raw_ip;
pub mod tun_tap;
/// Data Link Type (DLT) constants
/// These match the values from libpcap
pub mod dlt {
pub const EN10MB: i32 = 1; // Ethernet
pub const RAW: i32 = 12; // Raw IP (no link layer)
pub const NULL: i32 = 0; // BSD loopback (sometimes used by TUN)
pub const LINUX_SLL: i32 = 113; // Linux "cooked" capture v1
pub const PKTAP: i32 = 149; // Apple PKTAP (DLT_USER2)
pub const PKTAP_STANDARD: i32 = 258; // Standard PKTAP
pub const LINUX_SLL2: i32 = 276; // Linux "cooked" capture v2
// Link type values for raw IP packets
pub const LINKTYPE_RAW: i32 = 101; // Raw IPv4/IPv6
pub const LINKTYPE_IPV4: i32 = 228; // Raw IPv4 only
pub const LINKTYPE_IPV6: i32 = 229; // Raw IPv6 only
}
/// Link layer type enum for identifying interface types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkLayerType {
Ethernet,
RawIP,
LinuxSLL,
LinuxSLL2,
Pktap,
Tun,
Tap,
Unknown,
}
impl LinkLayerType {
/// Determine link layer type from DLT value
pub fn from_dlt(dlt: i32) -> Self {
match dlt {
dlt::EN10MB => LinkLayerType::Ethernet,
dlt::RAW | dlt::NULL | dlt::LINKTYPE_RAW | dlt::LINKTYPE_IPV4 | dlt::LINKTYPE_IPV6 => {
LinkLayerType::RawIP
}
dlt::LINUX_SLL => LinkLayerType::LinuxSLL,
dlt::LINUX_SLL2 => LinkLayerType::LinuxSLL2,
dlt::PKTAP | dlt::PKTAP_STANDARD => LinkLayerType::Pktap,
_ => LinkLayerType::Unknown,
}
}
/// Determine link layer type from both DLT value and interface name
///
/// This is more accurate than `from_dlt()` alone because it can distinguish
/// TUN/TAP interfaces from regular interfaces that use the same DLT codes.
///
/// # Example
///
/// ```rust,ignore
/// let link_type = LinkLayerType::from_dlt_and_name(12, "tun0");
/// assert!(matches!(link_type, LinkLayerType::Tun));
///
/// let link_type = LinkLayerType::from_dlt_and_name(1, "tap0");
/// assert!(matches!(link_type, LinkLayerType::Tap));
/// ```
pub fn from_dlt_and_name(dlt: i32, interface_name: &str) -> Self {
// Check if this is a TUN/TAP interface by name
if tun_tap::is_tun_interface(interface_name) {
return LinkLayerType::Tun;
}
if tun_tap::is_tap_interface(interface_name) {
return LinkLayerType::Tap;
}
// Otherwise, use DLT-based detection
Self::from_dlt(dlt)
}
/// Check if this link type represents a TUN/TAP interface
pub fn is_tunnel(&self) -> bool {
matches!(self, LinkLayerType::Tun | LinkLayerType::Tap)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linktype_from_dlt() {
assert_eq!(LinkLayerType::from_dlt(dlt::EN10MB), LinkLayerType::Ethernet);
assert_eq!(LinkLayerType::from_dlt(dlt::RAW), LinkLayerType::RawIP);
assert_eq!(LinkLayerType::from_dlt(dlt::NULL), LinkLayerType::RawIP);
assert_eq!(LinkLayerType::from_dlt(dlt::LINUX_SLL), LinkLayerType::LinuxSLL);
assert_eq!(LinkLayerType::from_dlt(dlt::LINUX_SLL2), LinkLayerType::LinuxSLL2);
assert_eq!(LinkLayerType::from_dlt(dlt::PKTAP), LinkLayerType::Pktap);
assert_eq!(LinkLayerType::from_dlt(dlt::PKTAP_STANDARD), LinkLayerType::Pktap);
assert_eq!(LinkLayerType::from_dlt(dlt::LINKTYPE_RAW), LinkLayerType::RawIP);
assert_eq!(LinkLayerType::from_dlt(dlt::LINKTYPE_IPV4), LinkLayerType::RawIP);
assert_eq!(LinkLayerType::from_dlt(dlt::LINKTYPE_IPV6), LinkLayerType::RawIP);
assert_eq!(LinkLayerType::from_dlt(999), LinkLayerType::Unknown);
}
#[test]
fn test_is_tunnel() {
assert!(LinkLayerType::Tun.is_tunnel());
assert!(LinkLayerType::Tap.is_tunnel());
assert!(!LinkLayerType::Ethernet.is_tunnel());
assert!(!LinkLayerType::RawIP.is_tunnel());
assert!(!LinkLayerType::LinuxSLL.is_tunnel());
assert!(!LinkLayerType::Pktap.is_tunnel());
}
#[test]
fn test_from_dlt_and_name() {
// Test TUN interface detection
assert_eq!(
LinkLayerType::from_dlt_and_name(dlt::RAW, "tun0"),
LinkLayerType::Tun
);
assert_eq!(
LinkLayerType::from_dlt_and_name(dlt::RAW, "utun0"),
LinkLayerType::Tun
);
// Test TAP interface detection
assert_eq!(
LinkLayerType::from_dlt_and_name(dlt::EN10MB, "tap0"),
LinkLayerType::Tap
);
// Test regular interface (not TUN/TAP)
assert_eq!(
LinkLayerType::from_dlt_and_name(dlt::EN10MB, "eth0"),
LinkLayerType::Ethernet
);
assert_eq!(
LinkLayerType::from_dlt_and_name(dlt::RAW, "wlan0"),
LinkLayerType::RawIP
);
}
}
+117
View File
@@ -0,0 +1,117 @@
//! Raw IP packet parsing (no link-layer header)
//!
//! Handles DLT_RAW (12), DLT_NULL (0), LINKTYPE_RAW (101),
//! LINKTYPE_IPV4 (228), and LINKTYPE_IPV6 (229)
//!
//! Used by TUN devices which operate at Layer 3 (network layer)
use crate::network::parser::{PacketParser, ParsedPacket};
/// Parse a raw IP packet (auto-detect IPv4 or IPv6)
///
/// TUN devices typically send raw IP packets without any link-layer header.
/// The first nibble of the packet indicates the IP version.
pub fn parse(
data: &[u8],
parser: &PacketParser,
process_name: Option<String>,
process_id: Option<u32>,
) -> Option<ParsedPacket> {
if data.is_empty() {
log::debug!("Raw IP: Empty packet");
return None;
}
// Check IP version from first nibble
let version = data[0] >> 4;
match version {
4 => {
log::trace!("Raw IP: IPv4 packet detected");
parser.parse_raw_ipv4_packet(data, process_name, process_id)
}
6 => {
log::trace!("Raw IP: IPv6 packet detected");
parser.parse_raw_ipv6_packet(data, process_name, process_id)
}
_ => {
log::debug!("Raw IP: Unknown IP version: {}", version);
None
}
}
}
/// Parse a raw IPv4 packet only
///
/// Used when the link type explicitly indicates IPv4 (LINKTYPE_IPV4 = 228)
pub fn parse_ipv4(
data: &[u8],
parser: &PacketParser,
process_name: Option<String>,
process_id: Option<u32>,
) -> Option<ParsedPacket> {
if data.is_empty() || data.len() < 20 {
log::debug!("Raw IPv4: Packet too small");
return None;
}
let version = data[0] >> 4;
if version != 4 {
log::warn!("Raw IPv4: Expected IPv4 but got version {}", version);
return None;
}
log::trace!("Raw IPv4: Parsing IPv4 packet");
parser.parse_raw_ipv4_packet(data, process_name, process_id)
}
/// Parse a raw IPv6 packet only
///
/// Used when the link type explicitly indicates IPv6 (LINKTYPE_IPV6 = 229)
pub fn parse_ipv6(
data: &[u8],
parser: &PacketParser,
process_name: Option<String>,
process_id: Option<u32>,
) -> Option<ParsedPacket> {
if data.is_empty() || data.len() < 40 {
log::debug!("Raw IPv6: Packet too small");
return None;
}
let version = data[0] >> 4;
if version != 6 {
log::warn!("Raw IPv6: Expected IPv6 but got version {}", version);
return None;
}
log::trace!("Raw IPv6: Parsing IPv6 packet");
parser.parse_raw_ipv6_packet(data, process_name, process_id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_raw_ip_empty_packet() {
let empty = vec![];
let parser = PacketParser::new();
assert!(parse(&empty, &parser, None, None).is_none());
}
#[test]
fn test_raw_ipv4_version_check() {
// Create a minimal IPv6 packet but try to parse as IPv4
let ipv6_packet = vec![0x60, 0x00, 0x00, 0x00]; // Version 6
let parser = PacketParser::new();
assert!(parse_ipv4(&ipv6_packet, &parser, None, None).is_none());
}
#[test]
fn test_raw_ipv6_version_check() {
// Create a minimal IPv4 packet but try to parse as IPv6
let ipv4_packet = vec![0x45, 0x00, 0x00, 0x00]; // Version 4
let parser = PacketParser::new();
assert!(parse_ipv6(&ipv4_packet, &parser, None, None).is_none());
}
}
+168
View File
@@ -0,0 +1,168 @@
//! TUN/TAP interface support
//!
//! TUN (Layer 3) and TAP (Layer 2) virtual network interfaces
//!
//! - TUN: Operates at IP layer, carries raw IP packets (DLT_RAW or DLT_NULL)
//! - TAP: Operates at Ethernet layer, carries Ethernet frames (DLT_EN10MB)
use crate::network::link_layer::{ethernet, raw_ip};
use crate::network::parser::{PacketParser, ParsedPacket};
/// Detect if an interface name is a TUN interface
///
/// TUN interface naming conventions:
/// - Linux: `tun0`, `tun1`, etc.
/// - macOS: `utun0`, `utun1`, etc.
/// - BSD: `tun0`, `tun1`, etc.
pub fn is_tun_interface(name: &str) -> bool {
name.starts_with("tun") || name.starts_with("utun")
}
/// Detect if an interface name is a TAP interface
///
/// TAP interface naming conventions:
/// - Linux: `tap0`, `tap1`, etc.
/// - macOS: `tap0`, `tap1`, etc. (requires third-party drivers)
/// - BSD: `tap0`, `tap1`, etc.
pub fn is_tap_interface(name: &str) -> bool {
name.starts_with("tap")
}
/// Detect if an interface name is any tunnel interface (TUN or TAP)
pub fn is_tunnel_interface(name: &str) -> bool {
is_tun_interface(name) || is_tap_interface(name)
}
/// Parse a TUN packet (raw IP, no link-layer header)
///
/// TUN devices operate at the network layer (Layer 3) and carry raw IP packets.
/// This is a convenience wrapper around raw_ip::parse()
pub fn parse_tun(
data: &[u8],
parser: &PacketParser,
process_name: Option<String>,
process_id: Option<u32>,
) -> Option<ParsedPacket> {
log::trace!("TUN: Parsing raw IP packet ({} bytes)", data.len());
raw_ip::parse(data, parser, process_name, process_id)
}
/// Parse a TAP packet (Ethernet frame with full header)
///
/// TAP devices operate at the data-link layer (Layer 2) and carry Ethernet frames.
/// This is a convenience wrapper around ethernet::parse()
pub fn parse_tap(
data: &[u8],
parser: &PacketParser,
process_name: Option<String>,
process_id: Option<u32>,
) -> Option<ParsedPacket> {
log::trace!("TAP: Parsing Ethernet frame ({} bytes)", data.len());
ethernet::parse(data, parser, process_name, process_id)
}
/// Determine the appropriate parser based on DLT type
///
/// This is the main entry point for parsing TUN/TAP packets when you know the DLT type.
pub fn parse_by_dlt(
data: &[u8],
dlt: i32,
parser: &PacketParser,
process_name: Option<String>,
process_id: Option<u32>,
) -> Option<ParsedPacket> {
match dlt {
// DLT_NULL - BSD/macOS loopback with 4-byte header
0 => {
log::trace!("TUN/TAP: DLT_NULL (0)");
parse_tun(data, parser, process_name, process_id)
}
// TAP devices use Ethernet (Layer 2)
1 => {
log::trace!("TUN/TAP: DLT_EN10MB (1) - TAP");
parse_tap(data, parser, process_name, process_id)
}
// DLT_RAW - Raw IP packets (no link layer)
12 | 101 => {
log::trace!("TUN/TAP: DLT_RAW ({}) - TUN", dlt);
parse_tun(data, parser, process_name, process_id)
}
// IPv4-only packets
228 => {
log::trace!("TUN/TAP: LINKTYPE_IPV4 (228) - TUN");
raw_ip::parse_ipv4(data, parser, process_name, process_id)
}
// IPv6-only packets
229 => {
log::trace!("TUN/TAP: LINKTYPE_IPV6 (229) - TUN");
raw_ip::parse_ipv6(data, parser, process_name, process_id)
}
_ => {
log::warn!("TUN/TAP: Unsupported DLT type: {}", dlt);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tun_interface_detection() {
assert!(is_tun_interface("tun0"));
assert!(is_tun_interface("tun1"));
assert!(is_tun_interface("tun10"));
assert!(is_tun_interface("utun0")); // macOS
assert!(is_tun_interface("utun1"));
assert!(!is_tun_interface("eth0"));
assert!(!is_tun_interface("tap0"));
assert!(!is_tun_interface("wlan0"));
}
#[test]
fn test_tap_interface_detection() {
assert!(is_tap_interface("tap0"));
assert!(is_tap_interface("tap1"));
assert!(is_tap_interface("tap10"));
assert!(!is_tap_interface("tun0"));
assert!(!is_tap_interface("eth0"));
assert!(!is_tap_interface("wlan0"));
}
#[test]
fn test_tunnel_interface_detection() {
assert!(is_tunnel_interface("tun0"));
assert!(is_tunnel_interface("tap0"));
assert!(is_tunnel_interface("utun0"));
assert!(!is_tunnel_interface("eth0"));
assert!(!is_tunnel_interface("wlan0"));
assert!(!is_tunnel_interface("lo"));
}
#[test]
fn test_parse_by_dlt() {
use crate::network::parser::PacketParser;
let parser = PacketParser::new();
// Test with empty packet - should return None
let empty = vec![];
assert!(parse_by_dlt(&empty, 12, &parser, None, None).is_none());
assert!(parse_by_dlt(&empty, 1, &parser, None, None).is_none());
}
#[test]
fn test_parse_tun_tap() {
use crate::network::parser::PacketParser;
let parser = PacketParser::new();
let empty = vec![];
// TUN parsing should fail on empty packet
assert!(parse_tun(&empty, &parser, None, None).is_none());
// TAP parsing should fail on empty packet
assert!(parse_tap(&empty, &parser, None, None).is_none());
}
}
+2 -2
View File
@@ -1,10 +1,10 @@
pub mod capture;
pub mod dpi;
pub mod link_layer;
pub mod merge;
pub mod parser;
#[cfg(target_os = "macos")]
pub mod pktap;
pub mod platform;
pub mod privileges;
pub mod protocol;
pub mod services;
pub mod types;
+471 -462
View File
File diff suppressed because it is too large Load Diff
+166 -12
View File
@@ -29,10 +29,17 @@ struct ProcessCache {
impl WindowsProcessLookup {
pub fn new() -> Result<Self> {
// Use a very old timestamp that's guaranteed to be before now
// by using checked_sub and falling back to epoch
let now = Instant::now();
let initial_refresh = now
.checked_sub(Duration::from_secs(3600))
.unwrap_or_else(|| now.checked_sub(Duration::from_secs(60)).unwrap_or(now));
Ok(Self {
cache: RwLock::new(ProcessCache {
lookup: HashMap::new(),
last_refresh: Instant::now() - Duration::from_secs(3600), // Force initial refresh
last_refresh: initial_refresh,
}),
})
}
@@ -64,9 +71,16 @@ impl WindowsProcessLookup {
);
if WIN32_ERROR(result) != ERROR_INSUFFICIENT_BUFFER {
log::debug!("GetExtendedTcpTable (IPv4) returned no data or error: {}", result);
return Ok(()); // No connections or error
}
if size == 0 || size > 100_000_000 {
// Sanity check: reject unreasonably large sizes (100MB limit)
log::warn!("GetExtendedTcpTable (IPv4) returned invalid size: {}", size);
return Ok(());
}
// Allocate buffer and get actual data
table = vec![0u8; size as usize];
let result = GetExtendedTcpTable(
@@ -79,13 +93,35 @@ impl WindowsProcessLookup {
);
if result != 0 {
log::debug!("GetExtendedTcpTable (IPv4) second call failed: {}", result);
return Ok(()); // Error getting table
}
// Verify we have enough data for the header
if table.len() < std::mem::size_of::<u32>() {
log::warn!("TCP table buffer too small for header");
return Ok(());
}
// Parse the table
let tcp_table = &*(table.as_ptr() as *const MIB_TCPTABLE_OWNER_PID);
let num_entries = tcp_table.dwNumEntries as usize;
// Bounds check: ensure we have enough space for all entries
let required_size = std::mem::size_of::<u32>()
+ num_entries * std::mem::size_of::<MIB_TCPROW_OWNER_PID>();
if table.len() < required_size {
log::warn!(
"TCP table buffer too small: got {} bytes, need {} for {} entries",
table.len(),
required_size,
num_entries
);
return Ok(());
}
log::debug!("Processing {} TCP IPv4 connections", num_entries);
// Get pointer to the first entry
let rows_ptr = &tcp_table.table[0] as *const MIB_TCPROW_OWNER_PID;
@@ -94,12 +130,12 @@ impl WindowsProcessLookup {
let local_addr = SocketAddr::new(
IpAddr::V4(Ipv4Addr::from(row.dwLocalAddr.to_ne_bytes())),
u16::from_be((row.dwLocalPort as u16).to_be()),
u16::from_be(row.dwLocalPort as u16),
);
let remote_addr = SocketAddr::new(
IpAddr::V4(Ipv4Addr::from(row.dwRemoteAddr.to_ne_bytes())),
u16::from_be((row.dwRemotePort as u16).to_be()),
u16::from_be(row.dwRemotePort as u16),
);
let key = ConnectionKey {
@@ -109,6 +145,14 @@ impl WindowsProcessLookup {
};
if let Some(process_name) = get_process_name_from_pid(row.dwOwningPid) {
log::trace!(
"Cached: {:?} {} -> {} (PID: {}, {})",
key.protocol,
local_addr,
remote_addr,
row.dwOwningPid,
process_name
);
cache.insert(key, (row.dwOwningPid, process_name));
}
}
@@ -133,9 +177,16 @@ impl WindowsProcessLookup {
);
if WIN32_ERROR(result) != ERROR_INSUFFICIENT_BUFFER {
log::debug!("GetExtendedTcpTable (IPv6) returned no data or error: {}", result);
return Ok(()); // No connections or error
}
if size == 0 || size > 100_000_000 {
// Sanity check: reject unreasonably large sizes (100MB limit)
log::warn!("GetExtendedTcpTable (IPv6) returned invalid size: {}", size);
return Ok(());
}
// Allocate buffer and get actual data
table = vec![0u8; size as usize];
let result = GetExtendedTcpTable(
@@ -148,13 +199,35 @@ impl WindowsProcessLookup {
);
if result != 0 {
log::debug!("GetExtendedTcpTable (IPv6) second call failed: {}", result);
return Ok(()); // Error getting table
}
// Verify we have enough data for the header
if table.len() < std::mem::size_of::<u32>() {
log::warn!("TCP IPv6 table buffer too small for header");
return Ok(());
}
// Parse the table
let tcp_table = &*(table.as_ptr() as *const MIB_TCP6TABLE_OWNER_PID);
let num_entries = tcp_table.dwNumEntries as usize;
// Bounds check: ensure we have enough space for all entries
let required_size = std::mem::size_of::<u32>()
+ num_entries * std::mem::size_of::<MIB_TCP6ROW_OWNER_PID>();
if table.len() < required_size {
log::warn!(
"TCP IPv6 table buffer too small: got {} bytes, need {} for {} entries",
table.len(),
required_size,
num_entries
);
return Ok(());
}
log::debug!("Processing {} TCP IPv6 connections", num_entries);
// Get pointer to the first entry
let rows_ptr = &tcp_table.table[0] as *const MIB_TCP6ROW_OWNER_PID;
@@ -163,12 +236,12 @@ impl WindowsProcessLookup {
let local_addr = SocketAddr::new(
IpAddr::V6(Ipv6Addr::from(row.ucLocalAddr)),
u16::from_be((row.dwLocalPort as u16).to_be()),
u16::from_be(row.dwLocalPort as u16),
);
let remote_addr = SocketAddr::new(
IpAddr::V6(Ipv6Addr::from(row.ucRemoteAddr)),
u16::from_be((row.dwRemotePort as u16).to_be()),
u16::from_be(row.dwRemotePort as u16),
);
let key = ConnectionKey {
@@ -178,6 +251,14 @@ impl WindowsProcessLookup {
};
if let Some(process_name) = get_process_name_from_pid(row.dwOwningPid) {
log::trace!(
"Cached: {:?} {} -> {} (PID: {}, {})",
key.protocol,
local_addr,
remote_addr,
row.dwOwningPid,
process_name
);
cache.insert(key, (row.dwOwningPid, process_name));
}
}
@@ -213,9 +294,16 @@ impl WindowsProcessLookup {
);
if WIN32_ERROR(result) != ERROR_INSUFFICIENT_BUFFER {
log::debug!("GetExtendedUdpTable (IPv4) returned no data or error: {}", result);
return Ok(()); // No connections or error
}
if size == 0 || size > 100_000_000 {
// Sanity check: reject unreasonably large sizes (100MB limit)
log::warn!("GetExtendedUdpTable (IPv4) returned invalid size: {}", size);
return Ok(());
}
// Allocate buffer and get actual data
table = vec![0u8; size as usize];
let result = GetExtendedUdpTable(
@@ -228,13 +316,35 @@ impl WindowsProcessLookup {
);
if result != 0 {
log::debug!("GetExtendedUdpTable (IPv4) second call failed: {}", result);
return Ok(()); // Error getting table
}
// Verify we have enough data for the header
if table.len() < std::mem::size_of::<u32>() {
log::warn!("UDP table buffer too small for header");
return Ok(());
}
// Parse the table
let udp_table = &*(table.as_ptr() as *const MIB_UDPTABLE_OWNER_PID);
let num_entries = udp_table.dwNumEntries as usize;
// Bounds check: ensure we have enough space for all entries
let required_size = std::mem::size_of::<u32>()
+ num_entries * std::mem::size_of::<MIB_UDPROW_OWNER_PID>();
if table.len() < required_size {
log::warn!(
"UDP table buffer too small: got {} bytes, need {} for {} entries",
table.len(),
required_size,
num_entries
);
return Ok(());
}
log::debug!("Processing {} UDP IPv4 connections", num_entries);
// Get pointer to the first entry
let rows_ptr = &udp_table.table[0] as *const MIB_UDPROW_OWNER_PID;
@@ -243,7 +353,7 @@ impl WindowsProcessLookup {
let local_addr = SocketAddr::new(
IpAddr::V4(Ipv4Addr::from(row.dwLocalAddr.to_ne_bytes())),
u16::from_be((row.dwLocalPort as u16).to_be()),
u16::from_be(row.dwLocalPort as u16),
);
// UDP doesn't have remote address in the table
@@ -256,6 +366,14 @@ impl WindowsProcessLookup {
};
if let Some(process_name) = get_process_name_from_pid(row.dwOwningPid) {
log::trace!(
"Cached: {:?} {} -> {} (PID: {}, {})",
key.protocol,
local_addr,
remote_addr,
row.dwOwningPid,
process_name
);
cache.insert(key, (row.dwOwningPid, process_name));
}
}
@@ -275,20 +393,46 @@ impl ProcessLookup for WindowsProcessLookup {
fn get_process_for_connection(&self, conn: &Connection) -> Option<(u32, String)> {
let key = ConnectionKey::from_connection(conn);
// Try cache first
// Try cache first - handle poisoned lock gracefully
{
let cache = self.cache.read().unwrap();
let cache = match self.cache.read() {
Ok(cache) => cache,
Err(poisoned) => {
log::warn!("Process cache lock was poisoned, recovering data");
poisoned.into_inner()
}
};
if cache.last_refresh.elapsed() < Duration::from_secs(2)
&& let Some(process_info) = cache.lookup.get(&key)
{
log::trace!("✓ Cache hit: {:?} {} -> {} => {:?}",
key.protocol, key.local_addr, key.remote_addr, process_info);
return Some(process_info.clone());
} else {
log::trace!("✗ Cache miss: {:?} {} -> {} (cache: {} entries, age: {}s)",
key.protocol, key.local_addr, key.remote_addr,
cache.lookup.len(), cache.last_refresh.elapsed().as_secs());
}
}
// Cache is stale or miss, refresh
if self.refresh().is_ok() {
let cache = self.cache.read().unwrap();
cache.lookup.get(&key).cloned()
let cache = match self.cache.read() {
Ok(cache) => cache,
Err(poisoned) => {
log::warn!("Process cache lock was poisoned after refresh, recovering data");
poisoned.into_inner()
}
};
let result = cache.lookup.get(&key).cloned();
if result.is_some() {
log::trace!("✓ Found after refresh: {:?} => {:?}", key, result);
} else {
log::trace!("✗ Still no match after refresh for: {:?} {} -> {}",
key.protocol, key.local_addr, key.remote_addr);
}
result
} else {
None
}
@@ -300,15 +444,25 @@ impl ProcessLookup for WindowsProcessLookup {
self.refresh_tcp_processes(&mut new_cache)?;
self.refresh_udp_processes(&mut new_cache)?;
let mut cache = self.cache.write().unwrap();
let mut cache = match self.cache.write() {
Ok(cache) => cache,
Err(poisoned) => {
log::warn!("Process cache write lock was poisoned, recovering and replacing cache");
poisoned.into_inner()
}
};
let total_entries = new_cache.len();
cache.lookup = new_cache;
cache.last_refresh = Instant::now();
log::debug!("Windows process lookup refresh complete: {} entries cached", total_entries);
Ok(())
}
fn get_detection_method(&self) -> &str {
"N/A"
"windows-iphlpapi"
}
}
+107
View File
@@ -0,0 +1,107 @@
//! ICMP (Internet Control Message Protocol) parsing
//! Handles both ICMPv4 and ICMPv6
use crate::network::parser::ParsedPacket;
use crate::network::protocol::TransportParams;
use crate::network::types::{Protocol, ProtocolState};
use std::net::SocketAddr;
/// Parse an ICMP (IPv4) packet
pub fn parse(
transport_data: &[u8],
params: TransportParams,
local_ips: &std::collections::HashSet<std::net::IpAddr>,
) -> Option<ParsedPacket> {
if transport_data.is_empty() {
return None;
}
let icmp_type = transport_data[0];
let icmp_code = if transport_data.len() > 1 {
transport_data[1]
} else {
0
};
// Determine direction based on local IPs
let is_outgoing = local_ips.contains(&params.src_ip);
let (local_addr, remote_addr) = if is_outgoing {
(
SocketAddr::new(params.src_ip, 0),
SocketAddr::new(params.dst_ip, 0),
)
} else {
(
SocketAddr::new(params.dst_ip, 0),
SocketAddr::new(params.src_ip, 0),
)
};
Some(ParsedPacket {
connection_key: format!("ICMP:{}-ICMP:{}", local_addr, remote_addr),
protocol: Protocol::ICMP,
local_addr,
remote_addr,
tcp_flags: None,
protocol_state: ProtocolState::Icmp {
icmp_type,
icmp_code,
},
is_outgoing,
packet_len: params.packet_len,
dpi_result: None,
process_name: params.process_name,
process_id: params.process_id,
})
}
/// Parse an ICMPv6 packet
pub fn parse_v6(
transport_data: &[u8],
params: TransportParams,
local_ips: &std::collections::HashSet<std::net::IpAddr>,
) -> Option<ParsedPacket> {
if transport_data.is_empty() {
return None;
}
let icmp_type = transport_data[0];
let icmp_code = if transport_data.len() > 1 {
transport_data[1]
} else {
0
};
// Determine direction based on local IPs
let is_outgoing = local_ips.contains(&params.src_ip);
let (local_addr, remote_addr) = if is_outgoing {
(
SocketAddr::new(params.src_ip, 0),
SocketAddr::new(params.dst_ip, 0),
)
} else {
(
SocketAddr::new(params.dst_ip, 0),
SocketAddr::new(params.src_ip, 0),
)
};
Some(ParsedPacket {
connection_key: format!("ICMP:{}-ICMP:{}", local_addr, remote_addr),
protocol: Protocol::ICMP,
local_addr,
remote_addr,
tcp_flags: None,
protocol_state: ProtocolState::Icmp {
icmp_type,
icmp_code,
},
is_outgoing,
packet_len: params.packet_len,
dpi_result: None, // No DPI for ICMPv6
process_name: params.process_name,
process_id: params.process_id,
})
}
+43
View File
@@ -0,0 +1,43 @@
//! Transport layer protocol parsing
//!
//! This module handles transport layer protocols (Layer 4 of the OSI model):
//! - TCP (Transmission Control Protocol)
//! - UDP (User Datagram Protocol)
//! - ICMP (Internet Control Message Protocol)
//! - ICMPv6 (Internet Control Message Protocol for IPv6)
pub mod icmp;
pub mod tcp;
pub mod udp;
use std::net::IpAddr;
/// Common parameters for transport layer parsing
/// Note: Direction (is_outgoing) is determined by the protocol parsers
/// based on local_ips, not passed as a parameter
#[derive(Clone)]
pub struct TransportParams {
pub src_ip: IpAddr,
pub dst_ip: IpAddr,
pub packet_len: usize,
pub process_name: Option<String>,
pub process_id: Option<u32>,
}
impl TransportParams {
pub fn new(
src_ip: IpAddr,
dst_ip: IpAddr,
packet_len: usize,
process_name: Option<String>,
process_id: Option<u32>,
) -> Self {
Self {
src_ip,
dst_ip,
packet_len,
process_name,
process_id,
}
}
}
+136
View File
@@ -0,0 +1,136 @@
//! TCP (Transmission Control Protocol) parsing
use crate::network::dpi;
use crate::network::parser::{ParsedPacket, ParserConfig};
use crate::network::protocol::TransportParams;
use crate::network::types::{Protocol, ProtocolState, TcpState};
use std::net::SocketAddr;
// Define TCP flags as bit masks
const TCP_FIN: u8 = 0x01;
const TCP_SYN: u8 = 0x02;
const TCP_RST: u8 = 0x04;
const TCP_PSH: u8 = 0x08;
const TCP_ACK: u8 = 0x10;
const TCP_URG: u8 = 0x20;
/// TCP flags from the TCP header
/// All flags are public fields as they represent the actual TCP flags
#[derive(Debug, Clone, Copy)]
pub struct TcpFlags {
pub fin: bool,
pub syn: bool,
pub rst: bool,
pub psh: bool,
pub ack: bool,
pub urg: bool,
}
/// Parse TCP flags from the flags byte
pub fn parse_tcp_flags(flags: u8) -> TcpFlags {
TcpFlags {
fin: (flags & TCP_FIN) != 0,
syn: (flags & TCP_SYN) != 0,
rst: (flags & TCP_RST) != 0,
psh: (flags & TCP_PSH) != 0,
ack: (flags & TCP_ACK) != 0,
urg: (flags & TCP_URG) != 0,
}
}
/// Parse a TCP packet
pub fn parse(
transport_data: &[u8],
params: TransportParams,
config: &ParserConfig,
local_ips: &std::collections::HashSet<std::net::IpAddr>,
) -> Option<ParsedPacket> {
if transport_data.len() < 20 {
return None;
}
let src_port = u16::from_be_bytes([transport_data[0], transport_data[1]]);
let dst_port = u16::from_be_bytes([transport_data[2], transport_data[3]]);
let flags = transport_data[13];
let tcp_flags = parse_tcp_flags(flags);
// Log TCP flags for debugging
log::trace!(
"TCP flags: FIN={} SYN={} RST={} PSH={} ACK={} URG={}",
tcp_flags.fin,
tcp_flags.syn,
tcp_flags.rst,
tcp_flags.psh,
tcp_flags.ack,
tcp_flags.urg
);
// Determine direction based on local IPs
let is_outgoing = local_ips.contains(&params.src_ip);
let (local_addr, remote_addr) = if is_outgoing {
(
SocketAddr::new(params.src_ip, src_port),
SocketAddr::new(params.dst_ip, dst_port),
)
} else {
(
SocketAddr::new(params.dst_ip, dst_port),
SocketAddr::new(params.src_ip, src_port),
)
};
// Perform DPI if enabled and there's payload
let dpi_result = if config.enable_dpi {
let tcp_header_len = ((transport_data[12] >> 4) as usize) * 4;
if transport_data.len() > tcp_header_len {
let payload = &transport_data[tcp_header_len..];
dpi::analyze_tcp_packet(
payload,
local_addr.port(),
remote_addr.port(),
is_outgoing,
)
} else {
None
}
} else {
None
};
Some(ParsedPacket {
connection_key: format!("TCP:{}-TCP:{}", local_addr, remote_addr),
protocol: Protocol::TCP,
local_addr,
remote_addr,
tcp_flags: Some(tcp_flags),
protocol_state: ProtocolState::Tcp(TcpState::Unknown),
is_outgoing,
packet_len: params.packet_len,
dpi_result,
process_name: params.process_name,
process_id: params.process_id,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tcp_flags_parsing() {
let flags = parse_tcp_flags(0x02); // SYN
assert!(flags.syn);
assert!(!flags.ack);
assert!(!flags.fin);
let flags = parse_tcp_flags(0x12); // SYN + ACK
assert!(flags.syn);
assert!(flags.ack);
let flags = parse_tcp_flags(0x11); // FIN + ACK
assert!(flags.fin);
assert!(flags.ack);
}
}
+64
View File
@@ -0,0 +1,64 @@
//! UDP (User Datagram Protocol) parsing
use crate::network::dpi;
use crate::network::parser::{ParsedPacket, ParserConfig};
use crate::network::protocol::TransportParams;
use crate::network::types::{Protocol, ProtocolState};
use std::net::SocketAddr;
/// Parse a UDP packet
pub fn parse(
transport_data: &[u8],
params: TransportParams,
config: &ParserConfig,
local_ips: &std::collections::HashSet<std::net::IpAddr>,
) -> Option<ParsedPacket> {
if transport_data.len() < 8 {
return None;
}
let src_port = u16::from_be_bytes([transport_data[0], transport_data[1]]);
let dst_port = u16::from_be_bytes([transport_data[2], transport_data[3]]);
// Determine direction based on local IPs
let is_outgoing = local_ips.contains(&params.src_ip);
let (local_addr, remote_addr) = if is_outgoing {
(
SocketAddr::new(params.src_ip, src_port),
SocketAddr::new(params.dst_ip, dst_port),
)
} else {
(
SocketAddr::new(params.dst_ip, dst_port),
SocketAddr::new(params.src_ip, src_port),
)
};
// Perform DPI if enabled and there's payload
let dpi_result = if config.enable_dpi && transport_data.len() > 8 {
let payload = &transport_data[8..];
dpi::analyze_udp_packet(
payload,
local_addr.port(),
remote_addr.port(),
is_outgoing,
)
} else {
None
};
Some(ParsedPacket {
connection_key: format!("UDP:{}-UDP:{}", local_addr, remote_addr),
protocol: Protocol::UDP,
local_addr,
remote_addr,
tcp_flags: None,
protocol_state: ProtocolState::Udp,
is_outgoing,
packet_len: params.packet_len,
dpi_result,
process_name: params.process_name,
process_id: params.process_id,
})
}
+5
View File
@@ -735,9 +735,14 @@ fn draw_stats_panel(
.unwrap_or_else(|| "Unknown".to_string());
let process_detection_method = app.get_process_detection_method();
let (link_layer_type, is_tunnel) = app.get_link_layer_info();
let conn_stats_text: Vec<Line> = vec![
Line::from(format!("Interface: {}", interface_name)),
Line::from(format!("Link Layer: {}{}",
link_layer_type,
if is_tunnel { " (Tunnel)" } else { "" }
)),
Line::from(format!("Process Detection: {}", process_detection_method)),
Line::from(""),
Line::from(format!("TCP Connections: {}", tcp_count)),