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
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "netflow_parser"
description = "Parser for Netflow Cisco V5, V7, V9, IPFIX"
version = "0.6.7"
version = "0.6.8"
edition = "2024"
authors = ["michael.mileusnich@gmail.com"]
license = "MIT OR Apache-2.0"
Expand All @@ -15,6 +15,7 @@ nom = "7.1.3"
nom-derive = "0.10.1"
mac_address = "1.1.5"
serde = { version = "1.0.166", features = ["derive"] }
lru = "0.12"

[features]
default = ["parse_unknown_fields"]
Expand Down
56 changes: 52 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,44 @@ let parsed = parser.parse_bytes(&v5_packet);

This code will return an empty Vec as version 5 is not allowed.

## Template Cache Configuration

V9 and IPFIX parsers use LRU (Least Recently Used) caching to store templates with a configurable size limit. This prevents memory exhaustion from template flooding attacks while maintaining good performance for legitimate traffic.

### Default Behavior

By default, parsers cache up to 1000 templates:

```rust
use netflow_parser::NetflowParser;

// Uses default cache size of 1000 templates per parser
let parser = NetflowParser::default();
```

### Custom Cache Size

You can configure the template cache size when creating parsers:

```rust
use netflow_parser::{NetflowParser, variable_versions::v9::V9Parser, variable_versions::ipfix::IPFixParser};

// Create V9 parser with custom cache size
let v9_parser = V9Parser::try_new(5000)?; // Cache up to 5000 templates

// Create IPFix parser with custom cache size
let ipfix_parser = IPFixParser::try_new(2000)?; // Cache up to 2000 templates

// Note: try_new() returns Result<Parser, Error> and will fail if cache_size is 0
```

### Cache Behavior

- When the cache is full, the least recently used template is evicted
- Templates are keyed by template ID (per source)
- Each parser instance maintains its own template cache
- For multi-source deployments, create separate parser instances per source

## Error Handling Configuration

To prevent memory exhaustion from malformed packets, the parser limits the size of error buffer samples. By default, only the first 256 bytes of unparseable data are stored in error messages. You can customize this limit for all parsers:
Expand Down Expand Up @@ -415,17 +453,27 @@ if let NetflowPacket::V5(v5) = NetflowParser::default()

## V9/IPFIX Notes

Parse the data (`&[u8]`) like any other version. The parser (`NetflowParser`) caches parsed templates, so you can send header/data flowset combos and it will use the cached templates. To see cached templates, use the parser for the correct version (`v9_parser` for V9, `ipfix_parser` for IPFIX).
Parse the data (`&[u8]`) like any other version. The parser (`NetflowParser`) caches parsed templates using LRU eviction, so you can send header/data flowset combos and it will use the cached templates. Templates are automatically cached and evicted when the cache limit is reached.

**IPFIX Note:** We only parse sequence number and domain id, it is up to you if you wish to validate it.
**Template Cache Access:**
Template caches use `LruCache` internally. You can inspect cached templates, but note that accessing them may affect LRU ordering:

```rust
use netflow_parser::NetflowParser;
let parser = NetflowParser::default();
dbg!(parser.v9_parser.templates);
dbg!(parser.v9_parser.options_templates);

// Check if a template exists
if parser.v9_parser.templates.contains(&template_id) {
// Template is cached
}

// Get cache stats
println!("V9 template cache size: {}", parser.v9_parser.templates.len());
println!("V9 max cache size: {}", parser.v9_parser.max_template_cache_size);
```

**IPFIX Note:** We only parse sequence number and domain id, it is up to you if you wish to validate it.

To access templates flowset of a processed V9/IPFix flowset you can find the `flowsets` attribute on the Parsed Record. In there you can find `Templates`, `Option Templates`, and `Data` Flowsets.

## Performance & Thread Safety
Expand Down
9 changes: 9 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# 0.6.8
* Added LRU-based template caching for V9Parser and IPFixParser to prevent memory exhaustion
* Default template cache size: 1000 templates per parser (configurable)
* New `V9Parser::try_new(cache_size)` and `IPFixParser::try_new(cache_size)` constructors for custom cache sizes
* Added `V9ParserError` and `IPFixParserError` error types for proper error handling
* Template cache is automatically evicted using LRU policy when limit is reached
* Provides protection against DoS attacks via template flooding
* Removed `PartialEq`, `Clone`, and `Serialize` derives from parser structs (due to LruCache)

# 0.6.7
* Optimized NetflowCommon conversion with single-pass field lookups (reduced O(n*m) to O(n))
* Added V5/V7/DataNumber capacity pre-allocation
Expand Down
58 changes: 54 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,18 +368,68 @@
//! }
//! ```
//!
//! ## Template Cache Configuration
//!
//! V9 and IPFIX parsers use LRU (Least Recently Used) caching to store templates with a configurable size limit. This prevents memory exhaustion from template flooding attacks while maintaining good performance for legitimate traffic.
//!
//! ### Default Behavior
//!
//! By default, parsers cache up to 1000 templates:
//!
//! ```rust
//! use netflow_parser::NetflowParser;
//!
//! // Uses default cache size of 1000 templates per parser
//! let parser = NetflowParser::default();
//! ```
//!
//! ### Custom Cache Size
//!
//! You can configure the template cache size when creating parsers:
//!
//! ```rust,ignore
//! use netflow_parser::{NetflowParser, variable_versions::v9::V9Parser, variable_versions::ipfix::IPFixParser};
//!
//! // Create V9 parser with custom cache size
//! let v9_parser = V9Parser::try_new(5000)?; // Cache up to 5000 templates
//!
//! // Create IPFix parser with custom cache size
//! let ipfix_parser = IPFixParser::try_new(2000)?; // Cache up to 2000 templates
//!
//! // Note: try_new() returns Result<Parser, Error> and will fail if cache_size is 0
//! ```
//!
//! ### Cache Behavior
//!
//! - When the cache is full, the least recently used template is evicted
//! - Templates are keyed by template ID (per source)
//! - Each parser instance maintains its own template cache
//! - For multi-source deployments, create separate parser instances per source
//!
//! ## V9/IPFIX Notes
//!
//! Parse the data (`&[u8]`) like any other version. The parser (`NetflowParser`) caches parsed templates, so you can send header/data flowset combos and it will use the cached templates. To see cached templates, use the parser for the correct version (`v9_parser` for V9, `ipfix_parser` for IPFIX).
//! Parse the data (`&[u8]`) like any other version. The parser (`NetflowParser`) caches parsed templates using LRU eviction, so you can send header/data flowset combos and it will use the cached templates. Templates are automatically cached and evicted when the cache limit is reached.
//!
//! **IPFIX Note:** We only parse sequence number and domain id, it is up to you if you wish to validate it.
//! **Template Cache Access:**
//! Template caches use `LruCache` internally. You can inspect cached templates, but note that accessing them may affect LRU ordering:
//!
//! ```rust
//! use netflow_parser::NetflowParser;
//! let parser = NetflowParser::default();
//! dbg!(parser.v9_parser.templates);
//! dbg!(parser.v9_parser.options_templates);
//! # let template_id = 256;
//!
//! // Check if a template exists
//! if parser.v9_parser.templates.contains(&template_id) {
//! // Template is cached
//! }
//!
//! // Get cache stats
//! println!("V9 template cache size: {}", parser.v9_parser.templates.len());
//! println!("V9 max cache size: {}", parser.v9_parser.max_template_cache_size);
//! ```
//!
//! **IPFIX Note:** We only parse sequence number and domain id, it is up to you if you wish to validate it.
//!
//! To access templates flowset of a processed V9/IPFix flowset you can find the `flowsets` attribute on the Parsed Record. In there you can find `Templates`, `Option Templates`, and `Data` Flowsets.
//!
//! ## Performance & Thread Safety
Expand Down
8 changes: 4 additions & 4 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ mod base_tests {
fields,
};
let mut parser = NetflowParser::default();
parser.v9_parser.templates.insert(258, template);
parser.v9_parser.templates.put(258, template);
assert_yaml_snapshot!(parser.parse_bytes(&packet));
}

Expand Down Expand Up @@ -352,7 +352,7 @@ mod base_tests {
..Default::default()
};
let mut parser = NetflowParser::default();
parser.ipfix_parser.templates.insert(258, template);
parser.ipfix_parser.templates.put(258, template);
assert_yaml_snapshot!(parser.parse_bytes(&packet));
}

Expand All @@ -367,7 +367,7 @@ mod base_tests {
..Default::default()
};
let mut parser = NetflowParser::default();
parser.ipfix_parser.templates.insert(258, template);
parser.ipfix_parser.templates.put(258, template);
assert_yaml_snapshot!(parser.parse_bytes(&packet));
}

Expand All @@ -382,7 +382,7 @@ mod base_tests {
fields: vec![],
};
let mut parser = NetflowParser::default();
parser.v9_parser.templates.insert(258, template);
parser.v9_parser.templates.put(258, template);
assert_yaml_snapshot!(parser.parse_bytes(&packet));
}

Expand Down
76 changes: 58 additions & 18 deletions src/variable_versions/ipfix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,34 @@ use crate::variable_versions::v9::{
Template as V9Template,
};

use std::collections::HashMap;
use lru::LruCache;
use std::num::NonZeroUsize;

const DATA_TEMPLATE_IPFIX_ID: u16 = 2;
const OPTIONS_TEMPLATE_IPFIX_ID: u16 = 3;

/// Default maximum number of templates to cache per parser
pub const DEFAULT_MAX_TEMPLATE_CACHE_SIZE: usize = 1000;

/// Error type for IPFixParser creation
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IPFixParserError {
/// Template cache size must be greater than 0
InvalidCacheSize,
}

impl std::fmt::Display for IPFixParserError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IPFixParserError::InvalidCacheSize => {
write!(f, "max_template_cache_size must be greater than 0")
}
}
}
}

impl std::error::Error for IPFixParserError {}

type TemplateId = u16;
pub type IPFixFieldPair = (IPFixField, FieldValue);
pub type IpFixFlowRecord = Vec<IPFixFieldPair>;
Expand All @@ -42,30 +65,48 @@ fn calculate_padding(content_size: usize) -> Vec<u8> {
vec![0u8; padding_len]
}

#[derive(Debug, PartialEq, Clone, Serialize)]
#[derive(Debug)]
pub struct IPFixParser {
pub templates: HashMap<TemplateId, Template>,
pub v9_templates: HashMap<TemplateId, V9Template>,
pub ipfix_options_templates: HashMap<TemplateId, OptionsTemplate>,
pub v9_options_templates: HashMap<TemplateId, V9OptionsTemplate>,
pub templates: LruCache<TemplateId, Template>,
pub v9_templates: LruCache<TemplateId, V9Template>,
pub ipfix_options_templates: LruCache<TemplateId, OptionsTemplate>,
pub v9_options_templates: LruCache<TemplateId, V9OptionsTemplate>,
/// Maximum number of templates to cache. Defaults to 1000.
pub max_template_cache_size: usize,
/// Maximum number of bytes to include in error samples to prevent memory exhaustion.
/// Defaults to 256 bytes.
pub max_error_sample_size: usize,
}

impl Default for IPFixParser {
fn default() -> Self {
Self {
templates: HashMap::default(),
v9_templates: HashMap::default(),
ipfix_options_templates: HashMap::default(),
v9_options_templates: HashMap::default(),
max_error_sample_size: 256,
}
// Safe to unwrap because DEFAULT_MAX_TEMPLATE_CACHE_SIZE is non-zero
Self::try_new(DEFAULT_MAX_TEMPLATE_CACHE_SIZE).unwrap()
}
}

impl IPFixParser {
/// Create a new IPFixParser with a custom template cache size.
///
/// # Arguments
/// * `max_template_cache_size` - Maximum number of templates to cache (must be > 0)
///
/// # Errors
/// Returns `IPFixParserError::InvalidCacheSize` if `max_template_cache_size` is 0
pub fn try_new(max_template_cache_size: usize) -> Result<Self, IPFixParserError> {
let cache_size = NonZeroUsize::new(max_template_cache_size)
.ok_or(IPFixParserError::InvalidCacheSize)?;

Ok(Self {
templates: LruCache::new(cache_size),
v9_templates: LruCache::new(cache_size),
ipfix_options_templates: LruCache::new(cache_size),
v9_options_templates: LruCache::new(cache_size),
max_template_cache_size,
max_error_sample_size: 256,
})
}

pub fn parse<'a>(&mut self, packet: &'a [u8]) -> ParsedNetflow<'a> {
match IPFix::parse(packet, self) {
Ok((remaining, ipfix)) => ParsedNetflow::Success {
Expand Down Expand Up @@ -93,26 +134,25 @@ impl IPFixParser {
/// Add templates to the parser by cloning from slice.
fn add_ipfix_templates(&mut self, templates: &[Template]) {
for t in templates {
self.templates.insert(t.template_id, t.clone());
self.templates.put(t.template_id, t.clone());
}
}

fn add_ipfix_options_templates(&mut self, templates: &[OptionsTemplate]) {
for t in templates {
self.ipfix_options_templates
.insert(t.template_id, t.clone());
self.ipfix_options_templates.put(t.template_id, t.clone());
}
}

fn add_v9_templates(&mut self, templates: &[V9Template]) {
for t in templates {
self.v9_templates.insert(t.template_id, t.clone());
self.v9_templates.put(t.template_id, t.clone());
}
}

fn add_v9_options_templates(&mut self, templates: &[V9OptionsTemplate]) {
for t in templates {
self.v9_options_templates.insert(t.template_id, t.clone());
self.v9_options_templates.put(t.template_id, t.clone());
}
}
}
Expand Down
Loading