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: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "flowparser-sflow"
description = "Parser for sFlow v5 datagrams"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
authors = ["michael.mileusnich@gmail.com"]
license = "MIT OR Apache-2.0"
Expand Down Expand Up @@ -40,3 +40,7 @@ harness = false
[[bench]]
name = "counter_sample_bench"
harness = false

[[bench]]
name = "throughput_bench"
harness = false
53 changes: 44 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@ An sFlow v5 parser library written in Rust. Parses sFlow v5 datagrams ([sFlow v5
## Features

- **sFlow v5** datagram parsing with IPv4 and IPv6 agent addresses
- **All four sample types**: Flow Sample, Counter Sample, Expanded Flow Sample, Expanded Counter Sample
- **38 flow record types**: Raw Packet Header, Sampled Ethernet, Sampled IPv4/IPv6, Extended Switch/Router/Gateway/User/URL, Extended MPLS (tunnel, VC, FTN, LDP FEC), Extended NAT, Extended VLAN Tunnel, Extended 802.11 (payload, Rx, Tx), Extended L2/IPv4/IPv6 Tunnel (egress/ingress), Extended Decapsulate/VNI (egress/ingress), Extended Egress Queue/ACL/Function/Transit/Queue, Extended Socket IPv4/IPv6, Extended Proxy Socket IPv4/IPv6, JVM Runtime, Memcache Operation, App Operation, HTTP Request, Extended Proxy Request
- **43 counter record types**: Generic/Ethernet/Token Ring/VG/VLAN/802.11/LAG/SFP interface counters, Slow Path Counts, InfiniBand, Processor, Queue Length, Radio Utilization, OpenFlow Port, Port Name, Host Description/Adapters/Parent/CPU/Memory/Disk IO/Net IO, Virtual Node/CPU/Memory/Disk IO/Net IO, MIB-II IP/ICMP/TCP/UDP, JMX Runtime, JVM Statistics, HTTP Counters, App Operations/Resources/Workers, Memcache Counters, OVS Datapath Stats, Energy/Temperature/Humidity/Fans, XenServer VIF (enterprise 4300)
- **5 sample types**: Flow Sample, Counter Sample, Expanded Flow Sample, Expanded Counter Sample, Discarded Packet
- **55 flow record types**: Raw Packet Header, Sampled Ethernet, Sampled IPv4/IPv6, Extended Switch/Router/Gateway/User/URL, Extended MPLS (tunnel, VC, FTN, LDP FEC), Extended NAT/NAT Port, Extended VLAN Tunnel, Extended 802.11 (payload, Rx, Tx), Extended L2/IPv4/IPv6 Tunnel (egress/ingress), Extended Decapsulate/VNI (egress/ingress), InfiniBand LRH/GRH/BRH, Extended Egress Queue/ACL/Function/Transit/Queue, Extended HW Trap/Linux Drop Reason/Timestamp, Extended Socket IPv4/IPv6, Extended Proxy Socket IPv4/IPv6, JVM Runtime, Memcache Operation, App Operation/Parent Context/Initiator/Target, HTTP Request, Extended Proxy Request, Extended TCP Info, Extended Entities
- **48 counter record types**: Generic/Ethernet/Token Ring/VG/VLAN/802.11/LAG/SFP interface counters, Slow Path Counts, InfiniBand, Processor, Queue Length, Radio Utilization, OpenFlow Port, Port Name, Host Description/Adapters/Parent/CPU/Memory/Disk IO/Net IO, Virtual Node/CPU/Memory/Disk IO/Net IO, MIB-II IP/ICMP/TCP/UDP, JMX Runtime, JVM Statistics, HTTP Counters, App Operations/Resources/Workers, Memcache Counters, OVS Datapath Stats, Energy/Temperature/Humidity/Fans, XenServer VIF (enterprise 4300), Broadcom BST Device/Port Buffers and HW Tables (enterprise 4413), NVIDIA GPU (enterprise 5703)
- **Broadcom/NVIDIA support**: Enterprise-specific counter records for switch ASIC (Broadcom 4413) and GPU monitoring (NVIDIA 5703)
- **Unknown record handling**: Unrecognized records captured as raw bytes for forward compatibility
- **Serde support**: All types serialize/deserialize to JSON and other formats
- **Builder pattern**: Optional configuration (e.g., max samples limit for DoS protection)
- **Spec validation tooling**: Included `validate_sflow` example for verifying datagrams against the sFlow v5 specification

## Usage

Add to your `Cargo.toml`:

```toml
[dependencies]
flowparser-sflow = "0.2.0"
flowparser-sflow = "0.3.0"
```

### Basic Parsing
Expand Down Expand Up @@ -126,16 +128,21 @@ Datagram
│ ├── Extended MPLS Tunnel/VC/FTN/LDP FEC (0:1008–1011)
│ ├── Extended VLAN Tunnel (0:1012)
│ ├── Extended 802.11 Payload/Rx/Tx (0:1013–1015)
│ ├── Extended NAT Port (0:1020)
│ ├── Extended L2/IPv4/IPv6 Tunnel Egress/Ingress (0:1021–1026)
│ ├── Extended Decapsulate/VNI Egress/Ingress (0:1027–1030)
│ ├── InfiniBand LRH/GRH/BRH (0:1031–1033)
│ ├── Extended Egress Queue/ACL/Function/Transit/Queue (0:1036–1040)
│ ├── Extended HW Trap/Linux Drop Reason/Timestamp (0:1041–1043)
│ ├── Extended Socket IPv4/IPv6 (0:2100–2101)
│ ├── Extended Proxy Socket IPv4/IPv6 (0:2102–2103)
│ ├── JVM Runtime (0:2105)
│ ├── Memcache Operation (0:2200)
│ ├── App Operation (0:2202)
│ ├── App Operation/Parent Context/Initiator/Target (0:2202–2205)
│ ├── HTTP Request (0:2206)
│ └── Extended Proxy Request (0:2207)
│ ├── Extended Proxy Request (0:2207)
│ ├── Extended TCP Info (0:2209)
│ └── Extended Entities (0:2210)
├── Counter Sample (enterprise=0, format=2)
│ └── Counter Records[]
│ ├── Generic Interface (0:1)
Expand Down Expand Up @@ -163,9 +170,13 @@ Datagram
│ ├── Memcache Counters (0:2204)
│ ├── OVS Datapath Stats (0:2207)
│ ├── Energy/Temperature/Humidity/Fans (0:3000–3003)
│ └── XenServer VIF (4300:2)
│ ├── XenServer VIF (4300:2)
│ ├── Broadcom BST Device/Port Buffers (4413:1–2)
│ ├── Broadcom HW Tables (4413:3)
│ └── NVIDIA GPU (5703:1)
├── Expanded Flow Sample (enterprise=0, format=3)
└── Expanded Counter Sample (enterprise=0, format=4)
├── Expanded Counter Sample (enterprise=0, format=4)
└── Discarded Packet (enterprise=0, format=5)
```

## Types
Expand All @@ -174,7 +185,7 @@ Datagram
|------|-------------|
| `SflowParser` | Main parser with optional configuration |
| `SflowDatagram` | Parsed datagram with header fields and samples |
| `SflowSample` | Enum: Flow, Counter, ExpandedFlow, ExpandedCounter, Unknown |
| `SflowSample` | Enum: Flow, Counter, ExpandedFlow, ExpandedCounter, DiscardedPacket, Unknown |
| `FlowRecord` | Enum of all flow record types |
| `CounterRecord` | Enum of all counter record types |
| `AddressType` | IPv4 or IPv6 agent address |
Expand All @@ -199,6 +210,9 @@ cargo run --example sflow_udp_listener_tokio

# Parse from pcap file
cargo run --example sflow_pcap -- <file.pcap>

# Validate sFlow datagrams against spec
echo "<hex>" | cargo run --example validate_sflow
```

## Benchmarks
Expand All @@ -207,6 +221,27 @@ cargo run --example sflow_pcap -- <file.pcap>
cargo bench
```

### Published Results

Measured on Apple Silicon (M-series), single-threaded, optimized build:

| Benchmark | Time | Throughput |
|-----------|------|------------|
| Empty datagram (28 bytes) | ~36 ns | — |
| Flow sample (1 record) | ~55 ns | — |
| Counter sample (1 record) | ~55 ns | — |
| Realistic datagram (8 samples, 4 records each, 1884 bytes) | ~919 ns | **~1.9 GiB/s** |
| Batch 100 datagrams (184 KB) | ~117 µs | **~1.5 GiB/s** |

### Spec Validation

Validate hex-encoded sFlow datagrams against the specification:

```bash
echo "00000005000000010a000001..." | cargo run --example validate_sflow
cargo run --example validate_sflow -- hexdump.txt
```

## License

Licensed under either of:
Expand Down
30 changes: 30 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Releases

## 0.3.0

### Added

- **17 new flow record types** (enterprise=0):
- NAT port translation (format 1020): `ExtendedNatPort`
- InfiniBand (formats 1031–1033): `ExtendedIbLrh`, `ExtendedIbGrh`, `ExtendedIbBrh`
- Hardware trap & drop monitoring (formats 1041–1042): `ExtendedHwTrap`, `ExtendedLinuxDropReason`
- Nanosecond timestamp (format 1043): `ExtendedTimestamp`
- Application context (formats 2203–2205): `AppParentContext`, `AppInitiator`, `AppTarget`
- TCP info (format 2209): `ExtendedTcpInfo`
- Entities (format 2210): `ExtendedEntities`
- **4 new vendor-specific counter record types**:
- Broadcom BST device buffers (enterprise=4413, format=1): `BroadcomBstDeviceBuffers`
- Broadcom BST port buffers (enterprise=4413, format=2): `BroadcomBstPortBuffers`
- Broadcom ASIC hardware tables (enterprise=4413, format=3): `BroadcomHwTables`
- NVIDIA GPU via NVML (enterprise=5703, format=1): `NvidiaGpu`
- **Discarded Packet sample type** (enterprise=0, format=5): `DiscardedPacket` — dropped packet notification with reason code and flow records
- `ParseContext::DiscardedPacket` variant for error reporting
- **Throughput benchmark** (`throughput_bench`) measuring realistic multi-sample datagram parsing (~1.9 GiB/s on Apple Silicon)
- **Spec validation tool** (`validate_sflow` example) for verifying hex-encoded sFlow datagrams against the sFlow v5 specification

### Improved

- Total flow record types: 38 → 55
- Total counter record types: 43 → 48 (including 5 vendor-specific)
- Total sample types: 4 → 5
- Published benchmark results in README
- Updated protocol structure diagram in README

## 0.2.0

### Breaking Changes
Expand Down
187 changes: 187 additions & 0 deletions benches/throughput_bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use flowparser_sflow::SflowParser;

/// Build a realistic sFlow v5 datagram with multiple flow samples,
/// each containing several record types (raw packet header, extended switch,
/// extended router, extended gateway).
fn build_realistic_datagram() -> Vec<u8> {
let mut data = Vec::new();

// Datagram header
data.extend_from_slice(&0u32.to_be_bytes()); // placeholder for later
data.extend_from_slice(&5u32.to_be_bytes()); // version 5
data.extend_from_slice(&1u32.to_be_bytes()); // IPv4
data.extend_from_slice(&[10, 0, 0, 1]); // agent
data.extend_from_slice(&0u32.to_be_bytes()); // sub_agent_id
data.extend_from_slice(&42u32.to_be_bytes()); // sequence
data.extend_from_slice(&1000u32.to_be_bytes()); // uptime

// We'll add 8 flow samples
let num_samples: u32 = 8;
data.extend_from_slice(&num_samples.to_be_bytes());

for seq in 0..num_samples {
let sample = build_flow_sample(seq + 1);
// enterprise=0, format=1
data.extend_from_slice(&1u32.to_be_bytes());
data.extend_from_slice(&(sample.len() as u32).to_be_bytes());
data.extend(sample);
}

// Fix: remove the placeholder we accidentally added
// Actually, rebuild properly from the version field
data[0..4].copy_from_slice(&5u32.to_be_bytes());
// The datagram starts at version, so shift everything
let proper = data[4..].to_vec();
let mut result = Vec::new();
result.extend_from_slice(&5u32.to_be_bytes()); // version
result.extend_from_slice(&proper[4..]); // rest after duplicate version
// Let me just rebuild cleanly:
build_clean_datagram(num_samples)
}

fn build_clean_datagram(num_samples: u32) -> Vec<u8> {
let mut data = Vec::new();

// Datagram header
data.extend_from_slice(&5u32.to_be_bytes()); // version 5
data.extend_from_slice(&1u32.to_be_bytes()); // address type IPv4
data.extend_from_slice(&[10, 0, 0, 1]); // agent address
data.extend_from_slice(&0u32.to_be_bytes()); // sub_agent_id
data.extend_from_slice(&42u32.to_be_bytes()); // sequence_number
data.extend_from_slice(&1000u32.to_be_bytes()); // uptime
data.extend_from_slice(&num_samples.to_be_bytes());

for seq in 0..num_samples {
let sample = build_flow_sample(seq + 1);
data.extend_from_slice(&1u32.to_be_bytes()); // enterprise=0, format=1
data.extend_from_slice(&(sample.len() as u32).to_be_bytes());
data.extend(&sample);
}

data
}

fn build_flow_sample(seq: u32) -> Vec<u8> {
let mut records = Vec::new();

// Record 1: Raw Packet Header (format=1)
{
let header_bytes = [0xAAu8; 64]; // fake 64-byte packet header
let mut rec = Vec::new();
rec.extend_from_slice(&1u32.to_be_bytes()); // header_protocol (ethernet)
rec.extend_from_slice(&128u32.to_be_bytes()); // frame_length
rec.extend_from_slice(&0u32.to_be_bytes()); // stripped
rec.extend_from_slice(&(header_bytes.len() as u32).to_be_bytes());
rec.extend_from_slice(&header_bytes);
// record header
records.extend_from_slice(&1u32.to_be_bytes()); // format=1
records.extend_from_slice(&(rec.len() as u32).to_be_bytes());
records.extend(rec);
}

// Record 2: Extended Switch (format=1001)
{
let mut rec = Vec::new();
rec.extend_from_slice(&100u32.to_be_bytes()); // src_vlan
rec.extend_from_slice(&0u32.to_be_bytes()); // src_priority
rec.extend_from_slice(&200u32.to_be_bytes()); // dst_vlan
rec.extend_from_slice(&0u32.to_be_bytes()); // dst_priority
records.extend_from_slice(&((0 << 12) | 1001u32).to_be_bytes());
records.extend_from_slice(&(rec.len() as u32).to_be_bytes());
records.extend(rec);
}

// Record 3: Extended Router (format=1002)
{
let mut rec = Vec::new();
rec.extend_from_slice(&1u32.to_be_bytes()); // address type IPv4
rec.extend_from_slice(&[192, 168, 1, 1]); // next_hop
rec.extend_from_slice(&24u32.to_be_bytes()); // src_mask
rec.extend_from_slice(&24u32.to_be_bytes()); // dst_mask
records.extend_from_slice(&((0 << 12) | 1002u32).to_be_bytes());
records.extend_from_slice(&(rec.len() as u32).to_be_bytes());
records.extend(rec);
}

// Record 4: Extended TCP Info (format=2209)
{
let mut rec = Vec::new();
rec.extend_from_slice(&2u32.to_be_bytes()); // direction=sent
rec.extend_from_slice(&1460u32.to_be_bytes()); // snd_mss
rec.extend_from_slice(&1460u32.to_be_bytes()); // rcv_mss
rec.extend_from_slice(&5u32.to_be_bytes()); // unacked
rec.extend_from_slice(&0u32.to_be_bytes()); // lost
rec.extend_from_slice(&0u32.to_be_bytes()); // retrans
rec.extend_from_slice(&1500u32.to_be_bytes()); // pmtu
rec.extend_from_slice(&10000u32.to_be_bytes()); // rtt
rec.extend_from_slice(&5000u32.to_be_bytes()); // rttvar
rec.extend_from_slice(&65535u32.to_be_bytes()); // snd_cwnd
rec.extend_from_slice(&3u32.to_be_bytes()); // reordering
rec.extend_from_slice(&8000u32.to_be_bytes()); // min_rtt
records.extend_from_slice(&((0 << 12) | 2209u32).to_be_bytes());
records.extend_from_slice(&(rec.len() as u32).to_be_bytes());
records.extend(rec);
}

let num_records: u32 = 4;

let mut sample = Vec::new();
sample.extend_from_slice(&seq.to_be_bytes()); // sequence_number
sample.extend_from_slice(&3u32.to_be_bytes()); // source_id
sample.extend_from_slice(&256u32.to_be_bytes()); // sampling_rate
sample.extend_from_slice(&10000u32.to_be_bytes()); // sample_pool
sample.extend_from_slice(&0u32.to_be_bytes()); // drops
sample.extend_from_slice(&1u32.to_be_bytes()); // input
sample.extend_from_slice(&2u32.to_be_bytes()); // output
sample.extend_from_slice(&num_records.to_be_bytes());
sample.extend(records);

sample
}

fn bench_throughput(c: &mut Criterion) {
let datagram = build_realistic_datagram();
let parser = SflowParser::default();

// Verify it parses correctly
let result = parser.parse_bytes(&datagram);
assert!(
result.error.is_none(),
"Test datagram should parse without error"
);
assert_eq!(result.datagrams.len(), 1);
assert_eq!(result.datagrams[0].samples.len(), 8);

let mut group = c.benchmark_group("throughput");

// Single datagram throughput
group.throughput(Throughput::Bytes(datagram.len() as u64));
group.bench_with_input(
BenchmarkId::new("single_datagram", datagram.len()),
&datagram,
|b, data| {
b.iter(|| parser.parse_bytes(data));
},
);

// Simulate batch: parse the same datagram N times in sequence
let batch_count = 100;
let batch: Vec<u8> = std::iter::repeat_n(&datagram, batch_count)
.flat_map(|d| d.iter().copied())
.collect();
// Note: parse_bytes handles multiple concatenated datagrams
group.throughput(Throughput::Bytes(batch.len() as u64));
group.bench_with_input(
BenchmarkId::new("batch_100_datagrams", batch.len()),
&batch,
|b, data| {
b.iter(|| parser.parse_bytes(data));
},
);

group.finish();
}

criterion_group!(benches, bench_throughput);
criterion_main!(benches);
18 changes: 18 additions & 0 deletions examples/dump_hex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,24 @@ fn print_sample(idx: usize, sample: &SflowSample) {
print_counter_record(ri, rec);
}
}
SflowSample::DiscardedPacket(flowparser_sflow::samples::DiscardedPacket {
sequence_number,
ds_class,
ds_index,
drops,
input,
output,
reason,
records,
}) => {
println!(
"Sample[{idx}]: DiscardedPacket {{ seq={sequence_number}, ds_class={ds_class}, ds_index={ds_index}, drops={drops}, input={input}, output={output}, reason={reason}, records={} }}",
records.len()
);
for (ri, rec) in records.iter().enumerate() {
print_flow_record(ri, rec);
}
}
SflowSample::Unknown {
enterprise,
format,
Expand Down
Loading
Loading