use netflow_parser::{NetflowError, NetflowPacket, NetflowParser};
use std::time::Duration;
#[test]
fn test_v9_max_field_count_exceeded() {
let mut parser = NetflowParser::builder()
.with_v9_max_field_count(100) .build()
.expect("Failed to build parser");
let mut packet = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xC8, ];
for i in 0..200u16 {
packet.extend_from_slice(&(i + 1).to_be_bytes()); packet.extend_from_slice(&4u16.to_be_bytes()); }
let flowset_length = (4 + 4 + 200 * 4) as u16;
packet[22..24].copy_from_slice(&flowset_length.to_be_bytes());
let _result = parser.parse_bytes(&packet);
assert!(
!parser.has_v9_template(256),
"Template with excessive field count should not be cached"
);
}
#[test]
fn test_ipfix_max_field_count_handling() {
let mut parser = NetflowParser::builder()
.with_ipfix_max_field_count(50) .build()
.expect("Failed to build parser");
let mut packet = vec![
0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x64, ];
for i in 0..100u16 {
packet.extend_from_slice(&(i + 1).to_be_bytes()); packet.extend_from_slice(&4u16.to_be_bytes()); }
let set_length = (4 + 4 + 100 * 4) as u16;
packet[18..20].copy_from_slice(&set_length.to_be_bytes());
let total_length = 16 + set_length; packet[2..4].copy_from_slice(&total_length.to_be_bytes());
let result = parser.parse_bytes(&packet);
assert!(
!parser.has_ipfix_template(256),
"IPFIX template with excessive field count should not be cached"
);
assert!(
result.error.is_none(),
"Parser should handle oversized template gracefully without error"
);
}
#[test]
fn test_template_cache_eviction() {
let mut parser = NetflowParser::builder()
.with_v9_cache_size(5) .build()
.expect("Failed to build parser");
for template_id in 256..266u16 {
let mut packet = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0C, ];
packet.extend_from_slice(&template_id.to_be_bytes()); packet.extend_from_slice(&[0x00, 0x01]); packet.extend_from_slice(&[0x00, 0x01]); packet.extend_from_slice(&[0x00, 0x04]);
let result = parser.parse_bytes(&packet);
assert!(
result.is_ok(),
"Template {} should parse successfully",
template_id
);
}
let stats = parser.v9_cache_info();
assert_eq!(stats.current_size, 5, "Cache should be at max size");
assert_eq!(stats.max_size_per_cache, 5, "Max size should be 5");
let data_packet = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, ];
let result = parser.parse_bytes(&data_packet);
assert!(
result.error.is_none(),
"Evicted-template data packet should parse without error"
);
let v9 = match result.packets.first() {
Some(NetflowPacket::V9(v9)) => v9,
_ => panic!("Expected a V9 packet"),
};
assert!(
v9.flowsets.iter().any(|fs| matches!(
&fs.body,
netflow_parser::variable_versions::v9::FlowSetBody::NoTemplate(_)
)),
"Should have NoTemplate flowset for evicted template"
);
}
#[test]
fn test_error_buffer_size_configuration() {
let mut parser = NetflowParser::builder()
.with_max_error_sample_size(32) .build()
.expect("Failed to build parser");
let mut packet = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, ];
packet.extend(vec![0xFF; 1000]);
let result = parser.parse_bytes(&packet);
assert!(result.error.is_some(), "Should error on malformed packet");
match &result.error {
Some(NetflowError::ParseError { remaining, .. }) => {
assert!(
remaining.len() <= 32,
"Error sample should be limited to 32 bytes, got {}",
remaining.len()
);
}
Some(NetflowError::Partial { .. }) => {
println!("Got Partial error (acceptable)");
}
Some(other) => {
panic!("Unexpected error type for malformed V9 packet: {:?}", other);
}
None => panic!("Expected error on malformed packet"),
}
}
#[test]
fn test_rapid_template_collisions() {
let mut parser = NetflowParser::default();
let template1 = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0C, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x04, ];
let result1 = parser.parse_bytes(&template1);
assert!(result1.is_ok(), "First template should succeed");
let template2 = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x01, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x04, 0x00, 0x02, 0x00, 0x04, ];
let result2 = parser.parse_bytes(&template2);
assert!(result2.is_ok(), "Second template should succeed (override)");
let data_packet = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, ];
let result3 = parser.parse_bytes(&data_packet);
assert!(
result3.is_ok(),
"Data packet should parse with new template"
);
assert_eq!(result3.packets.len(), 1, "Should have parsed 1 packet");
if let NetflowPacket::V9(v9) = &result3.packets[0] {
let data_flowsets: Vec<_> = v9
.flowsets
.iter()
.filter_map(|fs| {
if let netflow_parser::variable_versions::v9::FlowSetBody::Data(data) = &fs.body
{
Some(data)
} else {
None
}
})
.collect();
assert_eq!(data_flowsets.len(), 1, "Should have 1 data flowset");
assert_eq!(
data_flowsets[0].fields.len(),
1,
"Should have 1 data record"
);
assert_eq!(
data_flowsets[0].fields[0].len(),
2,
"Record should have 2 fields (from the overridden template)"
);
} else {
panic!("Expected V9 packet");
}
}
#[test]
fn test_cache_metrics_accuracy() {
let mut parser = NetflowParser::builder()
.with_v9_cache_size(10)
.build()
.expect("Failed to build parser");
let template = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0C, 0x01, 0x00, 0x00, 0x01,
0x00, 0x01, 0x00, 0x04,
];
let _ = parser.parse_bytes(&template);
let initial_stats = parser.v9_cache_info();
let initial_hits = initial_stats.metrics.hits;
for _ in 0..5 {
let data = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01,
];
let _ = parser.parse_bytes(&data);
}
let final_stats = parser.v9_cache_info();
let final_hits = final_stats.metrics.hits;
assert_eq!(
final_hits - initial_hits,
5,
"Should have 5 cache hits from data packets"
);
}
#[test]
fn test_template_ttl_expiration() {
use netflow_parser::variable_versions::ttl::TtlConfig;
let mut parser = NetflowParser::builder()
.with_v9_ttl(TtlConfig::new(Duration::from_millis(100))) .build()
.expect("Failed to build parser");
let template = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0C, 0x01, 0x00, 0x00, 0x01,
0x00, 0x01, 0x00, 0x04,
];
let result1 = parser.parse_bytes(&template);
assert!(result1.is_ok(), "Template should be added");
let data = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01,
];
let result2 = parser.parse_bytes(&data);
assert!(
result2.is_ok(),
"Data packet should parse with fresh template"
);
std::thread::sleep(Duration::from_millis(500));
let result3 = parser.parse_bytes(&data);
assert!(
result3.error.is_none(),
"Expired-template data packet should parse without error"
);
let v9 = match result3.packets.first() {
Some(NetflowPacket::V9(v9)) => v9,
_ => panic!("Expected a V9 packet"),
};
assert!(
v9.flowsets.iter().any(|fs| matches!(
&fs.body,
netflow_parser::variable_versions::v9::FlowSetBody::NoTemplate(_)
)),
"Should have NoTemplate flowset for expired template"
);
}
#[test]
fn test_zero_cache_size_rejected() {
let result = NetflowParser::builder().with_v9_cache_size(0).build();
assert!(result.is_err(), "Should reject zero cache size");
let error = result.unwrap_err();
let error_msg = error.to_string();
assert!(
error_msg.contains("cache") || error_msg.contains("size"),
"Error should mention cache/size issue, got: {}",
error_msg
);
}
#[test]
fn test_malformed_flowset_length() {
let mut parser = NetflowParser::default();
let packet = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x01, ];
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_some(),
"Malformed flowset length must produce an error"
);
}
#[test]
fn test_v9_max_records_per_flowset() {
let template_packet = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0C, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x04, ];
let mut parser = NetflowParser::builder()
.with_max_records_per_flowset(2) .build()
.expect("Failed to build parser");
let result = parser.parse_bytes(&template_packet);
assert!(result.error.is_none(), "Template parse failed");
let mut data_packet = vec![
0x00, 0x09, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x18, ];
for i in 0u32..5 {
data_packet.extend_from_slice(&(i + 1).to_be_bytes());
}
let result = parser.parse_bytes(&data_packet);
assert!(
result.error.is_none(),
"Data parse failed: {:?}",
result.error
);
if let Some(NetflowPacket::V9(v9)) = result.packets.first() {
let data_flowset = v9.flowsets.iter().find(|fs| {
matches!(
fs.body,
netflow_parser::variable_versions::v9::FlowSetBody::Data(_)
)
});
assert!(data_flowset.is_some(), "Expected data flowset");
if let netflow_parser::variable_versions::v9::FlowSetBody::Data(data) =
&data_flowset.unwrap().body
{
assert_eq!(
data.fields.len(),
2,
"Expected 2 records (limited by max_records_per_flowset), got {}",
data.fields.len()
);
}
} else {
panic!("Expected V9 packet");
}
}
#[test]
fn test_ipfix_max_records_per_flowset() {
let mut template_packet = vec![
0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x0C, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x04, ];
let len = template_packet.len() as u16;
template_packet[2..4].copy_from_slice(&len.to_be_bytes());
let mut parser = NetflowParser::builder()
.with_max_records_per_flowset(3) .build()
.expect("Failed to build parser");
let result = parser.parse_bytes(&template_packet);
assert!(result.error.is_none(), "Template parse failed");
let mut data_packet = vec![
0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x1C, ];
for i in 0u32..6 {
data_packet.extend_from_slice(&(i + 1).to_be_bytes());
}
let len = data_packet.len() as u16;
data_packet[2..4].copy_from_slice(&len.to_be_bytes());
let result = parser.parse_bytes(&data_packet);
assert!(
result.error.is_none(),
"Data parse failed: {:?}",
result.error
);
if let Some(NetflowPacket::IPFix(ipfix)) = result.packets.first() {
let data_flowset = ipfix.flowsets.iter().find(|fs| {
matches!(
fs.body,
netflow_parser::variable_versions::ipfix::FlowSetBody::Data(_)
)
});
assert!(data_flowset.is_some(), "Expected data flowset");
if let netflow_parser::variable_versions::ipfix::FlowSetBody::Data(data) =
&data_flowset.unwrap().body
{
assert_eq!(
data.fields.len(),
3,
"Expected 3 records (limited by max_records_per_flowset), got {}",
data.fields.len()
);
}
} else {
panic!("Expected IPFIX packet");
}
}
#[test]
fn test_zero_max_records_per_flowset_rejected() {
let result = NetflowParser::builder()
.with_max_records_per_flowset(0)
.build();
assert!(result.is_err(), "Should reject max_records_per_flowset = 0");
}
fn build_v5_packet(count: u16, actual_flows: u16) -> Vec<u8> {
let mut packet = vec![
0x00, 0x05, ];
packet.extend_from_slice(&count.to_be_bytes()); packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); packet.push(0x00); packet.push(0x00); packet.extend_from_slice(&[0x00, 0x00]); for i in 0..actual_flows {
let mut flow = [0u8; 48];
flow[0] = 10;
flow[3] = (i & 0xFF) as u8;
flow[38] = 6;
packet.extend_from_slice(&flow);
}
packet
}
#[test]
fn test_v5_count_zero_rejected() {
let mut parser = NetflowParser::default();
let packet = build_v5_packet(0, 0);
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_some(),
"V5 with count=0 must produce an error"
);
}
#[test]
fn test_v5_count_exceeds_max_flows() {
let mut parser = NetflowParser::default();
let packet = build_v5_packet(31, 31);
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_some(),
"V5 with count=31 (>30) must produce an error"
);
}
#[test]
fn test_v5_max_flow_count_succeeds() {
let mut parser = NetflowParser::default();
let packet = build_v5_packet(30, 30);
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_none(),
"V5 with count=30 should parse successfully: {:?}",
result.error
);
assert_eq!(result.packets.len(), 1);
if let NetflowPacket::V5(v5) = &result.packets[0] {
assert_eq!(v5.flowsets.len(), 30, "Should have 30 flowsets");
} else {
panic!("Expected V5 packet");
}
}
#[test]
fn test_v5_truncated_flow_data() {
let mut parser = NetflowParser::default();
let packet = build_v5_packet(2, 1); let result = parser.parse_bytes(&packet);
assert!(
result.error.is_some(),
"V5 with truncated flow data must produce an error"
);
}
#[test]
fn test_v5_single_flow() {
let mut parser = NetflowParser::default();
let packet = build_v5_packet(1, 1);
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_none(),
"V5 with 1 flow should succeed: {:?}",
result.error
);
assert_eq!(result.packets.len(), 1);
if let NetflowPacket::V5(v5) = &result.packets[0] {
assert_eq!(v5.flowsets.len(), 1);
assert_eq!(
v5.flowsets[0].src_addr,
std::net::Ipv4Addr::new(10, 0, 0, 0)
);
assert_eq!(v5.flowsets[0].protocol_number, 6);
} else {
panic!("Expected V5 packet");
}
}
fn build_v7_packet(count: u16, actual_flows: u16) -> Vec<u8> {
let mut packet = vec![
0x00, 0x07, ];
packet.extend_from_slice(&count.to_be_bytes()); packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); for i in 0..actual_flows {
let mut flow = [0u8; 52];
flow[0] = 192;
flow[1] = 168;
flow[3] = (i & 0xFF) as u8;
flow[38] = 17;
flow[48] = 10;
flow[51] = 1;
packet.extend_from_slice(&flow);
}
packet
}
#[test]
fn test_v7_count_zero_rejected() {
let mut parser = NetflowParser::default();
let packet = build_v7_packet(0, 0);
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_some(),
"V7 with count=0 must produce an error"
);
}
#[test]
fn test_v7_count_exceeds_max_flows() {
let mut parser = NetflowParser::default();
let packet = build_v7_packet(29, 29);
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_some(),
"V7 with count=29 (>28) must produce an error"
);
}
#[test]
fn test_v7_max_flow_count_succeeds() {
let mut parser = NetflowParser::default();
let packet = build_v7_packet(28, 28);
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_none(),
"V7 with count=28 should parse successfully: {:?}",
result.error
);
assert_eq!(result.packets.len(), 1);
if let NetflowPacket::V7(v7) = &result.packets[0] {
assert_eq!(v7.flowsets.len(), 28, "Should have 28 flowsets");
} else {
panic!("Expected V7 packet");
}
}
#[test]
fn test_v7_truncated_flow_data() {
let mut parser = NetflowParser::default();
let packet = build_v7_packet(3, 2); let result = parser.parse_bytes(&packet);
assert!(
result.error.is_some(),
"V7 with truncated flow data must produce an error"
);
}
#[test]
fn test_v7_single_flow_field_extraction() {
let mut parser = NetflowParser::default();
let packet = build_v7_packet(1, 1);
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_none(),
"V7 with 1 flow should succeed: {:?}",
result.error
);
assert_eq!(result.packets.len(), 1);
if let NetflowPacket::V7(v7) = &result.packets[0] {
assert_eq!(v7.flowsets.len(), 1);
assert_eq!(
v7.flowsets[0].src_addr,
std::net::Ipv4Addr::new(192, 168, 0, 0)
);
assert_eq!(v7.flowsets[0].protocol_number, 17);
assert_eq!(
v7.flowsets[0].router_src,
std::net::Ipv4Addr::new(10, 0, 0, 1)
);
} else {
panic!("Expected V7 packet");
}
}
#[test]
fn test_v5_header_too_short() {
let mut parser = NetflowParser::default();
let packet = vec![
0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, ];
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_some(),
"V5 with truncated header must produce an error"
);
}
#[test]
fn test_v7_header_too_short() {
let mut parser = NetflowParser::default();
let packet = vec![
0x00, 0x07, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, ];
let result = parser.parse_bytes(&packet);
assert!(
result.error.is_some(),
"V7 with truncated header must produce an error"
);
}