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
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,25 @@ Each field mapping has a `primary` field (always checked first) and an optional

Parsed V5, V7, V9, and IPFIX packets can be re-exported back into bytes.

**Note:** For V9/IPFIX, we only export the original padding we dissected and do not calculate/align the flowset padding ourselves. If you modify an existing V9/IPFIX flow or create your own, you must manually adjust the padding.
**V9/IPFIX Padding Behavior:**
- For **parsed packets**: Original padding is preserved exactly for byte-perfect round-trips
- For **manually created packets**: Padding is automatically calculated to align FlowSets to 4-byte boundaries

**Creating Data Structs:**
For convenience, use `Data::new()` and `OptionsData::new()` to create data structures without manually specifying padding:

```rust
use netflow_parser::variable_versions::ipfix::Data;

// Padding is automatically set to empty vec and calculated during export
let data = Data::new(vec![vec![
(field1, value1),
(field2, value2),
]]);
```

See `examples/manual_ipfix_creation.rs` for a complete example of creating IPFIX packets from scratch.

```rust
// 0000 00 05 00 01 03 00 04 00 05 00 06 07 08 09 00 01 ................
// 0010 02 03 04 05 06 07 08 09 00 01 02 03 04 05 06 07 ................
Expand Down Expand Up @@ -450,18 +468,14 @@ To run:

```cargo run --example netflow_udp_listener_multi_threaded```

or

```cargo run --example netflow_udp_listener_single_threaded```

or

```cargo run --example netflow_udp_listener_tokio```

or

```cargo run --example netflow_pcap```

```cargo run --example manual_ipfix_creation```

The pcap example also shows how to cache flows that have not yet discovered a template.

## Support My Work
Expand Down
6 changes: 6 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
* Enhanced validation for malformed packets
* Improved IPFIX error handling - parse errors now properly propagate
* Added thread safety documentation and performance tuning guide
* **Fixed V9/IPFIX padding handling:**
* Fixed missing padding export for V9 Data FlowSets
* Added padding fields to IPFIX Data and OptionsData structures
* Auto-calculate padding for manually created packets (when padding field is empty)
* Preserve original padding for parsed packets (byte-perfect round-trips)
* Added `examples/manual_ipfix_creation.rs` demonstrating manual packet creation

# 0.6.6
* Added configurable field mappings for V9 and IPFIX in NetflowCommon.
Expand Down
117 changes: 117 additions & 0 deletions examples/manual_ipfix_creation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! Example demonstrating manual creation of IPFIX packets.
//!
//! This example shows how to create IPFIX packets from scratch. The library provides
//! the `calculate_padding` function to help calculate the correct padding bytes needed
//! to align FlowSets to 4-byte boundaries.

use netflow_parser::variable_versions::data_number::FieldValue;
use netflow_parser::variable_versions::ipfix::{
Data, FlowSet, FlowSetBody, FlowSetHeader, Header, IPFix, Template, TemplateField,
};
use netflow_parser::variable_versions::ipfix_lookup::{IANAIPFixField, IPFixField};
use std::net::Ipv4Addr;

fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a simple IPFIX header
let header = Header {
version: 10,
length: 0, // Will be calculated based on actual size
export_time: 1234567890,
sequence_number: 1,
observation_domain_id: 0,
};

// Create a template with 3 fields (which will need 1 byte of padding)
// Template structure: template_id (2) + field_count (2) + 3 fields * 4 bytes = 16 bytes
// 16 bytes is already aligned to 4, but let's create one that needs padding

// Actually, let's create a template with 1 field to demonstrate padding
// Template: template_id (2) + field_count (2) + 1 field * 4 bytes = 8 bytes (no padding needed)
// Let's add a field with enterprise bit set: + 4 bytes enterprise = 12 bytes (no padding)

// For demonstration, let's create a simple template that shows the concept
let template = Template {
template_id: 256,
field_count: 2,
fields: vec![
TemplateField {
field_type_number: 8, // sourceIPv4Address
field_length: 4,
enterprise_number: None,
field_type: IPFixField::IANA(IANAIPFixField::SourceIpv4address),
},
TemplateField {
field_type_number: 12, // destinationIPv4Address
field_length: 4,
enterprise_number: None,
field_type: IPFixField::IANA(IANAIPFixField::DestinationIpv4address),
},
],
};

// Calculate the template flowset length
// Header: 4 bytes (flowset_id + length)
// Template: 2 (template_id) + 2 (field_count) + 2 fields * 4 bytes = 12 bytes
// Total: 16 bytes (perfectly aligned, no padding needed)
let template_flowset = FlowSet {
header: FlowSetHeader {
header_id: 2, // Template FlowSet ID
length: 16,
},
body: FlowSetBody::Template(template.clone()),
};

// Create data flowset with actual flow records
let data = Data::new(vec![vec![
(
IPFixField::IANA(IANAIPFixField::SourceIpv4address),
FieldValue::Ip4Addr(Ipv4Addr::new(192, 168, 1, 1)),
),
(
IPFixField::IANA(IANAIPFixField::DestinationIpv4address),
FieldValue::Ip4Addr(Ipv4Addr::new(10, 0, 0, 1)),
),
]]);

// Data content: 2 fields * 4 bytes = 8 bytes (already aligned, no padding needed in this case)
// If we had unaligned data, we could use calculate_padding() to determine padding bytes needed
let data_flowset = FlowSet {
header: FlowSetHeader {
header_id: 256, // Matches template_id
length: 12, // Header (4) + Data (8) = 12 bytes
},
body: FlowSetBody::Data(data),
};

// Create the IPFIX packet
let mut ipfix = IPFix {
header,
flowsets: vec![template_flowset, data_flowset],
};

// Update the total length in the header
ipfix.header.length = 16 + 16 + 12; // Header + Template FlowSet + Data FlowSet = 44 bytes

// Export to bytes
let exported_bytes = ipfix.to_be_bytes()?;

println!("Created IPFIX packet with {} bytes", exported_bytes.len());
println!("Header length field: {}", ipfix.header.length);
println!(
"Actual exported length: {} (should match header.length)",
exported_bytes.len()
);

// Verify the packet structure
println!("\nPacket structure:");
println!("- IPFIX Header: 16 bytes");
println!(
"- Template FlowSet: {} bytes",
ipfix.flowsets[0].header.length
);
println!("- Data FlowSet: {} bytes", ipfix.flowsets[1].header.length);

println!("\n✓ Use calculate_padding() to calculate alignment when needed!");

Ok(())
}
15 changes: 8 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,12 @@
//!
//! Parsed V5, V7, V9, and IPFIX packets can be re-exported back into bytes.
//!
//! **Note:** For V9/IPFIX, we only export the original padding we dissected and do not calculate/align the flowset padding ourselves. If you modify an existing V9/IPFIX flow or create your own, you must manually adjust the padding.
//! **V9/IPFIX Padding Behavior:**
//! - For **parsed packets**: Original padding is preserved exactly for byte-perfect round-trips
//! - For **manually created packets**: Padding is automatically calculated to align FlowSets to 4-byte boundaries - simply leave the `padding` field empty (`vec![]`)
//!
//! See `examples/manual_ipfix_creation.rs` for a complete example of creating IPFIX packets from scratch.
//!
//! ```rust
//! use netflow_parser::{NetflowParser, NetflowPacket};
//!
Expand Down Expand Up @@ -417,18 +422,14 @@
//!
//! ```cargo run --example netflow_udp_listener_multi_threaded```
//!
//! or
//!
//! ```cargo run --example netflow_udp_listener_single_threaded```
//!
//! or
//!
//! ```cargo run --example netflow_udp_listener_tokio```
//!
//! or
//!
//! ```cargo run --example netflow_pcap```
//!
//! ```cargo run --example manual_ipfix_creation```
//!
//! The pcap example also shows how to cache flows that have not yet discovered a template.
//!
//! ## Support My Work
Expand Down
60 changes: 58 additions & 2 deletions src/variable_versions/ipfix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ type TemplateId = u16;
pub type IPFixFieldPair = (IPFixField, FieldValue);
pub type IpFixFlowRecord = Vec<IPFixFieldPair>;

/// Calculate padding needed to align to 4-byte boundary.
/// Returns a Vec of zero bytes with the appropriate length.
fn calculate_padding(content_size: usize) -> Vec<u8> {
const PADDING_SIZES: [usize; 4] = [0, 3, 2, 1];
let padding_len = PADDING_SIZES[content_size % 4];
vec![0u8; padding_len]
}

#[derive(Debug, PartialEq, Clone, Serialize)]
pub struct IPFixParser {
pub templates: HashMap<TemplateId, Template>,
Expand Down Expand Up @@ -358,6 +366,20 @@ pub struct Data {
Parse = "{ |i| FieldParser::parse::<Template>(i, template) }"
)]
pub fields: Vec<IpFixFlowRecord>,
#[serde(skip_serializing)]
pub padding: Vec<u8>,
}

impl Data {
/// Creates a new Data instance with the given fields.
/// The padding field is automatically set to an empty vector and will be
/// calculated during export for manually created packets.
pub fn new(fields: Vec<IpFixFlowRecord>) -> Self {
Self {
fields,
padding: vec![],
}
}
}

#[derive(Debug, PartialEq, Clone, Serialize, Nom)]
Expand All @@ -368,6 +390,20 @@ pub struct OptionsData {
Parse = "{ |i| FieldParser::parse::<OptionsTemplate>(i, template) }"
)]
pub fields: Vec<Vec<IPFixFieldPair>>,
#[serde(skip_serializing)]
pub padding: Vec<u8>,
}

impl OptionsData {
/// Creates a new OptionsData instance with the given fields.
/// The padding field is automatically set to an empty vector and will be
/// calculated during export for manually created packets.
pub fn new(fields: Vec<Vec<IPFixFieldPair>>) -> Self {
Self {
fields,
padding: vec![],
}
}
}

#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Nom)]
Expand Down Expand Up @@ -598,19 +634,39 @@ impl IPFix {
}

if let FlowSetBody::Data(data) = &flow.body {
let mut data_content = Vec::new();
for item in data.fields.iter() {
for (_, v) in item.iter() {
result_flowset.extend_from_slice(&v.to_be_bytes()?);
data_content.extend_from_slice(&v.to_be_bytes()?);
}
}
result_flowset.extend_from_slice(&data_content);

// Auto-calculate padding if not provided (for manually created packets)
let padding = if data.padding.is_empty() {
calculate_padding(data_content.len())
} else {
data.padding.clone()
};
result_flowset.extend_from_slice(&padding);
}

if let FlowSetBody::OptionsData(data) = &flow.body {
let mut options_data_content = Vec::new();
for item in data.fields.iter() {
for (_, v) in item.iter() {
result_flowset.extend_from_slice(&v.to_be_bytes()?);
options_data_content.extend_from_slice(&v.to_be_bytes()?);
}
}
result_flowset.extend_from_slice(&options_data_content);

// Auto-calculate padding if not provided (for manually created packets)
let padding = if data.padding.is_empty() {
calculate_padding(options_data_content.len())
} else {
data.padding.clone()
};
result_flowset.extend_from_slice(&padding);
}

if let FlowSetBody::V9Data(data) = &flow.body {
Expand Down
Loading