Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ shared-version = true
tag-name = "v{{version}}"

[workspace.package]
version = "0.5.0"
version = "0.6.0"
edition = "2024"
license = "GPL-3.0-only"
authors = ["Stackforge Contributors"]
Expand Down
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

- **Scapy-style API** — Stack layers with `Ether() / IP() / TCP()`, set fields with keyword arguments
- **High Performance** — Core logic in Rust, zero-copy parsing, copy-on-write mutation
- **Broad Protocol Support** — Ethernet, ARP, IPv4/IPv6, TCP, UDP, ICMP, ICMPv6, DNS, HTTP/1.x, HTTP/2, QUIC, L2TP, 802.11 (Wi-Fi), 802.15.4 (Zigbee), and custom protocols
- **Stateful Flow Extraction** — Extract bidirectional conversations from PCAP files with TCP state tracking, stream reassembly, and UDP timeout handling
- **Broad Protocol Support** — Ethernet, ARP, IPv4/IPv6, TCP, UDP, ICMP/ICMPv6 (with echo correlation), DNS, HTTP/1.x, HTTP/2, QUIC, L2TP, 802.11 (Wi-Fi), 802.15.4 (Zigbee), and custom protocols
- **Stateful Flow Extraction** — Extract bidirectional conversations from PCAP files with TCP state tracking, stream reassembly, UDP timeout handling, and optional max packet/flow length tracking
- **PCAP I/O** — Read and write pcap files with `rdpcap()` / `wrpcap()`
- **Python Bindings** — Seamless integration via PyO3/maturin
- **Custom Protocols** — Define runtime protocols with `CustomLayer` and typed fields
Expand Down Expand Up @@ -303,6 +303,45 @@ config = FlowConfig(
conversations = extract_flows("capture.pcap", config=config)
```

Optional: Track maximum packet sizes during flow extraction:

```python
config = FlowConfig(
track_max_packet_len=True, # Track max per-direction (forward_max_packet_len, reverse_max_packet_len)
track_max_flow_len=True, # Track overall max (max_flow_len)
)
conversations = extract_flows("capture.pcap", config=config)

for conv in conversations:
print(f"Max fwd packet: {conv.forward_max_packet_len} bytes")
print(f"Max rev packet: {conv.reverse_max_packet_len} bytes")
print(f"Max overall: {conv.max_flow_len} bytes")
```

Disabled by default (zero overhead). Enable only when needed for flow analysis.

#### ICMP and ICMPv6 Flow Tracking

Automatically correlate ICMP echo request/reply pairs and track other ICMP message types:

```python
conversations = extract_flows("capture.pcap")

for conv in conversations:
if conv.protocol == "ICMP" or conv.protocol == "ICMPv6":
print(f"ICMP Echo: {conv.src_addr} <-> {conv.dst_addr}")
print(f" Type: {conv.icmp_type}, Code: {conv.icmp_code}")
print(f" Identifier: {conv.icmp_identifier}")
print(f" Requests: {conv.icmp_request_count}, Replies: {conv.icmp_reply_count}")
print(f" Last seq: {conv.icmp_last_seq}")
```

Features:
- Echo request/reply pairs correlated via identifier (symmetric src/dst ports)
- Non-echo message types tracked via (type, code) substitution
- Properties: `icmp_type`, `icmp_code`, `icmp_identifier`, `icmp_request_count`, `icmp_reply_count`, `icmp_last_seq`
- Returns `None` for non-ICMP flows

## Rust Crate

The core library is available as a standalone Rust crate:
Expand Down
8 changes: 8 additions & 0 deletions crates/stackforge-core/src/flow/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ pub struct FlowConfig {
pub max_ooo_fragments: usize,
/// Interval between idle conversation eviction sweeps (default: 30s).
pub eviction_interval: Duration,
/// Track maximum packet length per direction (default: false).
pub track_max_packet_len: bool,
/// Track maximum flow length per direction (default: false).
pub track_max_flow_len: bool,
}

impl Default for FlowConfig {
Expand All @@ -32,6 +36,8 @@ impl Default for FlowConfig {
max_reassembly_buffer: 16 * 1024 * 1024, // 16 MB
max_ooo_fragments: 100,
eviction_interval: Duration::from_secs(30),
track_max_packet_len: false,
track_max_flow_len: false,
}
}
}
Expand All @@ -50,5 +56,7 @@ mod tests {
assert_eq!(config.max_reassembly_buffer, 16 * 1024 * 1024);
assert_eq!(config.max_ooo_fragments, 100);
assert_eq!(config.eviction_interval, Duration::from_secs(30));
assert!(!config.track_max_packet_len);
assert!(!config.track_max_flow_len);
}
}
2 changes: 1 addition & 1 deletion crates/stackforge-core/src/flow/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ pub fn extract_zwave_flows(
state
});

conv.record_packet(direction, byte_count, timestamp, index);
conv.record_packet(direction, byte_count, timestamp, index, false, false);

// Track ACK vs command frames
if let ProtocolState::ZWave(ref mut zw) = conv.protocol_state
Expand Down
29 changes: 26 additions & 3 deletions crates/stackforge-core/src/flow/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub struct DirectionStats {
pub first_seen: Duration,
/// Timestamp of the most recent packet in this direction.
pub last_seen: Duration,
/// Maximum packet length in this direction (if tracking enabled).
pub max_packet_len: Option<u64>,
}

impl DirectionStats {
Expand All @@ -58,14 +60,18 @@ impl DirectionStats {
bytes: 0,
first_seen: timestamp,
last_seen: timestamp,
max_packet_len: None,
}
}

/// Record a new packet in this direction.
pub fn record_packet(&mut self, byte_count: u64, timestamp: Duration) {
pub fn record_packet(&mut self, byte_count: u64, timestamp: Duration, track_max_len: bool) {
self.packets += 1;
self.bytes += byte_count;
self.last_seen = timestamp;
if track_max_len {
self.max_packet_len = Some(self.max_packet_len.unwrap_or(0).max(byte_count));
}
}
}

Expand Down Expand Up @@ -120,6 +126,8 @@ pub struct ConversationState {
pub packet_indices: Vec<usize>,
/// Protocol-specific state.
pub protocol_state: ProtocolState,
/// Maximum packet length across both directions (if tracking enabled).
pub max_flow_len: Option<u64>,
}

impl ConversationState {
Expand All @@ -143,6 +151,7 @@ impl ConversationState {
reverse: DirectionStats::new(timestamp),
packet_indices: Vec::new(),
protocol_state,
max_flow_len: None,
}
}

Expand Down Expand Up @@ -179,6 +188,7 @@ impl ConversationState {
command_count: 0,
ack_count: 0,
}),
max_flow_len: None,
}
}

Expand Down Expand Up @@ -207,13 +217,26 @@ impl ConversationState {
byte_count: u64,
timestamp: Duration,
packet_index: usize,
track_max_packet_len: bool,
track_max_flow_len: bool,
) {
self.last_seen = timestamp;
self.packet_indices.push(packet_index);

match direction {
FlowDirection::Forward => self.forward.record_packet(byte_count, timestamp),
FlowDirection::Reverse => self.reverse.record_packet(byte_count, timestamp),
FlowDirection::Forward => {
self.forward
.record_packet(byte_count, timestamp, track_max_packet_len);
},
FlowDirection::Reverse => {
self.reverse
.record_packet(byte_count, timestamp, track_max_packet_len);
},
}

// Update max flow length if tracking is enabled
if track_max_flow_len {
self.max_flow_len = Some(self.max_flow_len.unwrap_or(0).max(byte_count));
}
}

Expand Down
9 changes: 8 additions & 1 deletion crates/stackforge-core/src/flow/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,14 @@ impl ConversationTable {
let conv = entry.value_mut();

// Record packet stats
conv.record_packet(direction, byte_count, timestamp, packet_index);
conv.record_packet(
direction,
byte_count,
timestamp,
packet_index,
self.config.track_max_packet_len,
self.config.track_max_flow_len,
);

// Process protocol-specific state
let buf = packet.as_bytes();
Expand Down
28 changes: 28 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3861,6 +3861,10 @@ impl PyPcapPacket {
hexdump_bytes(self.inner.packet.as_bytes())
}

fn bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
PyBytes::new(py, self.inner.packet.as_bytes())
}

fn __repr__(&self) -> String {
format!(
"<PcapPacket time={:.6} len={}>",
Expand Down Expand Up @@ -4011,6 +4015,8 @@ impl PyFlowConfig {
udp_timeout=120.0,
max_reassembly_buffer=16777216,
max_ooo_fragments=100,
track_max_packet_len=false,
track_max_flow_len=false,
))]
fn new(
tcp_established_timeout: f64,
Expand All @@ -4019,6 +4025,8 @@ impl PyFlowConfig {
udp_timeout: f64,
max_reassembly_buffer: usize,
max_ooo_fragments: usize,
track_max_packet_len: bool,
track_max_flow_len: bool,
) -> Self {
Self {
inner: stackforge_core::FlowConfig {
Expand All @@ -4030,6 +4038,8 @@ impl PyFlowConfig {
udp_timeout: std::time::Duration::from_secs_f64(udp_timeout),
max_reassembly_buffer,
max_ooo_fragments,
track_max_packet_len,
track_max_flow_len,
..stackforge_core::FlowConfig::default()
},
}
Expand Down Expand Up @@ -4137,6 +4147,24 @@ impl PyConversation {
self.inner.total_bytes()
}

/// Maximum packet length in forward direction.
#[getter]
fn forward_max_packet_len(&self) -> Option<u64> {
self.inner.forward.max_packet_len
}

/// Maximum packet length in reverse direction.
#[getter]
fn reverse_max_packet_len(&self) -> Option<u64> {
self.inner.reverse.max_packet_len
}

/// Maximum packet length across both directions.
#[getter]
fn max_flow_len(&self) -> Option<u64> {
self.inner.max_flow_len
}

/// Indices of packets belonging to this conversation.
#[getter]
fn packet_indices(&self) -> Vec<usize> {
Expand Down
Loading