Skip to content

Commit adff4b4

Browse files
authored
fix: correct NetFlow v9 Count and framing (#302)
1 parent baeb440 commit adff4b4

14 files changed

Lines changed: 407 additions & 74 deletions

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,26 @@ let parser = NetflowParser::builder()
224224
- Each parser instance maintains its own template cache
225225
- For multi-source deployments, use `RouterScopedParser` (see Template Management section)
226226

227+
### NetFlow v9 Frame Boundaries
228+
229+
NetFlow v9 does not carry a packet-length field. Its header `Count` is the
230+
number of Template, Options Template, and Data records, not the number of
231+
FlowSets and not a byte length. Pass exactly one complete, transport-delimited
232+
v9 export packet to each `parse_bytes` or `iter_packets` call.
233+
234+
The default maximum v9 frame size is 65,535 bytes, including the 20-byte
235+
header. Callers using a transport that permits larger frames can configure a
236+
higher finite bound:
237+
238+
```rust
239+
use netflow_parser::NetflowParser;
240+
241+
let parser = NetflowParser::builder()
242+
.with_v9_max_frame_size_bytes(128 * 1024)
243+
.build()
244+
.expect("valid frame limit");
245+
```
246+
227247
### Maximum Field Count (Security)
228248

229249
Configure the maximum number of fields allowed per template to prevent DoS attacks via malicious packets with excessive field counts:

SECURITY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ The parser includes several DoS mitigations:
159159
- **Template Field Count Limit:** Default 10,000 fields per template
160160
- **Template Total Size Validation:** Maximum 65,535 bytes per template
161161
- **Cumulative Decoded Output:** Defaults to 65,536 field values and 4 MiB of field content per message
162+
- **NetFlow v9 Frame Size Limit:** Default 65,535 bytes per caller-delimited packet
162163
- **Error Sample Size Limit:** Default 256 bytes to prevent memory exhaustion
163164
- **LRU Template Cache:** Prevents unbounded cache growth
164165

src/lib.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ pub use variable_versions::ttl::TtlConfig;
6161
pub use variable_versions::{
6262
Config, ConfigError, DEFAULT_MAX_DECODED_FIELD_PAYLOAD_BYTES_PER_MESSAGE,
6363
DEFAULT_MAX_DECODED_FIELD_VALUES_PER_MESSAGE, DEFAULT_MAX_RECORDS_PER_FLOWSET,
64-
DecodedOutputLimit, DecodedOutputLimits, NoTemplateInfo, PendingFlowsConfig,
64+
DEFAULT_MAX_V9_FRAME_SIZE_BYTES, DecodedOutputLimit, DecodedOutputLimits, NoTemplateInfo,
65+
PendingFlowsConfig,
6566
};
6667

6768
// Rust-idiomatic naming aliases
@@ -283,6 +284,7 @@ pub struct NetflowParserBuilder {
283284
/// Raw version numbers passed to `with_allowed_versions`, for validation
284285
requested_versions: Option<Vec<u16>>,
285286
max_error_sample_size: usize,
287+
v9_max_frame_size_bytes: usize,
286288
template_hooks: TemplateHooks,
287289
template_store: Option<Arc<dyn TemplateStore>>,
288290
template_store_scope: Arc<str>,
@@ -312,6 +314,7 @@ impl std::fmt::Debug for NetflowParserBuilder {
312314
.field("ipfix_config", &self.ipfix_config)
313315
.field("allowed_versions", &self.allowed_versions)
314316
.field("max_error_sample_size", &self.max_error_sample_size)
317+
.field("v9_max_frame_size_bytes", &self.v9_max_frame_size_bytes)
315318
.field(
316319
"template_hooks",
317320
&format!("{} hooks", self.template_hooks.len()),
@@ -337,6 +340,7 @@ impl Default for NetflowParserBuilder {
337340
allowed_versions: versions_to_array(&[5, 7, 9, 10]),
338341
requested_versions: None,
339342
max_error_sample_size: 256,
343+
v9_max_frame_size_bytes: DEFAULT_MAX_V9_FRAME_SIZE_BYTES,
340344
template_hooks: TemplateHooks::new(),
341345
template_store: None,
342346
template_store_scope: Arc::from(""),
@@ -641,6 +645,16 @@ impl NetflowParserBuilder {
641645
self
642646
}
643647

648+
/// Sets the maximum size of one caller-delimited NetFlow v9 export packet.
649+
///
650+
/// The size includes the complete 20-byte v9 header. The default is 65,535
651+
/// bytes. Callers using a transport that permits larger frames may raise it.
652+
#[must_use = "builder methods consume self and return a new builder; the return value must be used"]
653+
pub fn with_v9_max_frame_size_bytes(mut self, size: usize) -> Self {
654+
self.v9_max_frame_size_bytes = size;
655+
self
656+
}
657+
644658
/// Registers a custom enterprise field definition for both V9 and IPFIX parsers.
645659
///
646660
/// This allows library users to define their own enterprise-specific fields without
@@ -926,6 +940,9 @@ impl NetflowParserBuilder {
926940
pub fn validate(&self) -> Result<(), ConfigError> {
927941
V9Parser::validate_config(&self.v9_config)?;
928942
IPFixParser::validate_config(&self.ipfix_config)?;
943+
if self.v9_max_frame_size_bytes == 0 {
944+
return Err(ConfigError::InvalidV9FrameSize(0));
945+
}
929946
// Check that all requested versions are supported (5, 7, 9, 10)
930947
if let Some(versions) = &self.requested_versions {
931948
if versions.is_empty() {
@@ -961,13 +978,15 @@ impl NetflowParserBuilder {
961978
/// ```
962979
pub fn build(self) -> Result<NetflowParser, ConfigError> {
963980
self.validate()?;
981+
let v9_max_frame_size_bytes = self.v9_max_frame_size_bytes;
964982
let mut v9_config = self.v9_config;
965983
let mut ipfix_config = self.ipfix_config;
966984
v9_config.template_store = self.template_store.clone();
967985
v9_config.template_store_scope = Arc::clone(&self.template_store_scope);
968986
ipfix_config.template_store = self.template_store;
969987
ipfix_config.template_store_scope = self.template_store_scope;
970-
let v9_parser = V9Parser::try_new(v9_config)?;
988+
let mut v9_parser = V9Parser::try_new(v9_config)?;
989+
v9_parser.set_max_frame_size_bytes(v9_max_frame_size_bytes)?;
971990
let ipfix_parser = IPFixParser::try_new(ipfix_config)?;
972991

973992
Ok(NetflowParser {
@@ -1679,6 +1698,10 @@ impl NetflowParser {
16791698
/// * `packets` - All successfully parsed packets (even if error occurred)
16801699
/// * `error` - `None` if fully successful, `Some(error)` if parsing stopped
16811700
///
1701+
/// NetFlow v9 has no packet-length field. Pass exactly one complete,
1702+
/// transport-delimited v9 export packet per call. Concatenated v9 packets
1703+
/// cannot be separated from FlowSets and are not supported.
1704+
///
16821705
/// # Examples
16831706
///
16841707
/// ## Basic usage
@@ -1750,6 +1773,8 @@ impl NetflowParser {
17501773

17511774
/// Returns an iterator that yields NetflowPacket items without allocating a Vec.
17521775
/// This is useful for processing large batches of packets without collecting all results in memory.
1776+
/// NetFlow v9 input must still contain exactly one transport-delimited
1777+
/// export packet because v9 has no packet-length field.
17531778
///
17541779
/// # Examples
17551780
///

src/snapshots/netflow_parser__tests__base_tests__v9_example_from_integration_test.snap

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: "parser.parse_bytes(&hex::decode(hex_hex1).unwrap()).packets"
55
- V9:
66
header:
77
version: 9
8-
count: 2
8+
count: 1
99
sys_up_time: 6
1010
unix_secs: 1672687345
1111
sequence_number: 2
@@ -36,9 +36,3 @@ expression: "parser.parse_bytes(&hex::decode(hex_hex1).unwrap()).packets"
3636
- Ip4Addr: 0.0.0.1
3737
- - Ipv4DstAddr
3838
- Ip4Addr: 0.0.0.1
39-
- header:
40-
flowset_id: 256
41-
length: 8
42-
body:
43-
Data:
44-
fields: []

src/snapshots/netflow_parser__tests__base_tests__v9_example_from_integration_test_2.snap

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: "parser.parse_bytes(&hex::decode(hex2).unwrap()).packets"
55
- V9:
66
header:
77
version: 9
8-
count: 2
8+
count: 1
99
sys_up_time: 6
1010
unix_secs: 1672687345
1111
sequence_number: 2
@@ -36,9 +36,3 @@ expression: "parser.parse_bytes(&hex::decode(hex2).unwrap()).packets"
3636
- Ip4Addr: 0.0.0.1
3737
- - Ipv4DstAddr
3838
- Ip4Addr: 0.0.0.1
39-
- header:
40-
flowset_id: 256
41-
length: 8
42-
body:
43-
Data:
44-
fields: []

src/snapshots/netflow_parser__tests__restored_legacy_tests__it_parses_multiple_packets.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
source: src/tests.rs
3-
expression: "NetflowParser::default().parse_bytes(&all).packets"
3+
expression: packets
44
---
55
- V9:
66
header:

src/snapshots/netflow_parser__tests__restored_legacy_tests__it_parses_v9_ipv6flowlabel.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
source: src/tests.rs
3-
expression: "NetflowParser::default().parse_bytes(&packets).packets"
3+
expression: packets
44
---
55
- V9:
66
header:

src/tests.rs

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -122,33 +122,36 @@ mod base_tests {
122122
}));
123123
}
124124

125-
// Verify that combined v9 options template, data template, and data records parse together
125+
// Verify that sequential v9 options template, data template, and data datagrams parse together
126126
#[test]
127127
fn can_read_v9_with_options_template_and_template() {
128-
// Three v9 messages in one buffer: options template, data template,
128+
// Three separately framed v9 messages: options template, data template,
129129
// and one data record using template 256.
130130
let hex_hex0 =
131131
"00090001000000000000000000000000000000010001001401000004000400010004002900020000";
132132
let hex_hex1 = "00090001000000000000000000000000000000010000000c0100000100080004";
133133
let hex_hex2 = "000900010000000000000000000000000000000101000008c0a80001";
134134

135-
let combined = format!("{}{}{}", hex_hex0, hex_hex1, hex_hex2);
136-
137135
let mut parser = NetflowParser::builder()
138136
.with_cache_size(100)
139137
.build()
140138
.unwrap();
141139

142-
let packets = hex::decode(combined).unwrap();
143-
let results = parser.parse_bytes(&packets).packets;
140+
let mut results = Vec::new();
141+
for packet_hex in [hex_hex0, hex_hex1, hex_hex2] {
142+
let parsed = parser.parse_bytes(&hex::decode(packet_hex).unwrap());
143+
assert!(parsed.error.is_none());
144+
results.extend(parsed.packets);
145+
}
144146
assert_yaml_snapshot!(results);
145147
}
146148

147149
// Verify that a v9 template packet is parsed and can be serialized back to bytes
148150
#[test]
149151
fn can_read_v9() {
150-
// Template
151-
let hex = "0009000100000e1061db09bd000000010000000100000028010000080001000400020004000a00040004000400080004000c0004000700020015000400050001000600010016000400100004";
152+
// One 40-byte Template FlowSet. An older fixture included 16 bytes
153+
// beyond its declared FlowSet length; those bytes are not part of it.
154+
let hex = "0009000100000e1061db09bd000000010000000100000028010000080001000400020004000a00040004000400080004000c00040007000200150004";
152155

153156
let mut parser = NetflowParser::builder()
154157
.with_cache_size(100)
@@ -349,10 +352,11 @@ mod base_tests {
349352
}
350353
}
351354

352-
// Verify that v9 options template followed by a zeroed-out data record parses correctly
355+
// Verify that a malformed v9 options template is not cached and its subsequent
356+
// data record is retained as NoTemplate without relying on bytes outside either FlowSet.
353357
#[test]
354358
fn options_no_data() {
355-
let hex = "0009000100000001639073f3000000010000000100010034010200210001000400020004000e000400160004001500040009000100070002001000040011000400180004000600010005000100b0000200b1000200b2000200b4000200b7000200b8000200ad000200ac00010038000200b9000200bd000200be000200c1000200c2000200c5000200c3000200c4000200c6000200c7000200c8000200c9000200ca000200cb000200ce000200";
359+
let hex = "0009000100000001639073f3000000010000000100010034010200210001000400020004000e00040016000400150004000900010007000200100004001100040018000400060001";
356360

357361
let mut parser = NetflowParser::builder()
358362
.with_cache_size(100)
@@ -362,7 +366,7 @@ mod base_tests {
362366
let packet = hex::decode(hex).unwrap();
363367
let _ = parser.parse_bytes(&packet);
364368

365-
let hex_data = "0009000100000002639073f300000002000000010102008400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
369+
let hex_data = "0009000100000002639073f30000000200000001010200840000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
366370

367371
let packet = hex::decode(hex_data).unwrap();
368372
assert_yaml_snapshot!(parser.parse_bytes(&packet).packets);
@@ -395,9 +399,11 @@ mod base_tests {
395399
// Verify that v9 template followed by data in separate packets produces correct output
396400
#[test]
397401
fn v9_example_from_integration_test() {
398-
let hex_template = "000900020000000563b32ef1000000010000000100000024010000060001000400020004000a00040004000400080004000c000400010038010000020001000400020004";
402+
// Each packet ends at its declared FlowSet boundary. Older fixtures
403+
// appended malformed bytes that the Count-as-FlowSet bug hid.
404+
let hex_template = "000900010000000563b32ef1000000010000000100000024010000060001000400020004000a00040004000400080004000c000400010038";
399405

400-
let hex_hex1 = "000900020000000663b32ef10000000200000001010000240a70090a0a70090b00000001000000010000000100000001000000030000000601000008192a80e3192a80e4";
406+
let hex_hex1 = "000900010000000663b32ef10000000200000001010000240a70090a0a70090b000000010000000100000001000000010000000300000006";
401407

402408
let _hex_hex2 = "000900020000000763b32ef10000000300000001010000240a700a050a700a0600000001000000010000000100000001000000030000000601000008192a80e3192a80e4";
403409

@@ -413,9 +419,10 @@ mod base_tests {
413419
// Verify that v9 multi-template data parsing works across sequential packets
414420
#[test]
415421
fn v9_example_from_integration_test_2() {
416-
let hex1 = "000900020000000563b32ef1000000010000000100000024010000060001000400020004000a00040004000400080004000c000400010038010000020001000400020004";
422+
// Same corrected caller-delimited fixture pair as the integration case above.
423+
let hex1 = "000900010000000563b32ef1000000010000000100000024010000060001000400020004000a00040004000400080004000c000400010038";
417424

418-
let hex2 = "000900020000000663b32ef10000000200000001010000240a70090a0a70090b00000001000000010000000100000001000000030000000601000008192a80e3192a80e4";
425+
let hex2 = "000900010000000663b32ef10000000200000001010000240a70090a0a70090b000000010000000100000001000000010000000300000006";
419426

420427
let mut parser = NetflowParser::builder()
421428
.with_cache_size(100)
@@ -1080,9 +1087,14 @@ mod restored_legacy_tests {
10801087
fn it_parses_v9_ipv6flowlabel() {
10811088
let templates_hex = "0009000400a21e176658cb4600000155000000080000004c0102001100080004000c0004000f000400070002000b0002000a0002000e000200fc000400fd000400020004000100040016000400150004000400010005000101000002003d0001000000540103001300080004000c0004000f000400070002000b000200060001000a0002000e000200fc000400fd000400020004000100040016000400150004000400010005000100d1000801000002003d00010000005401050013001b0010001c0010003e001000070002000b000200060001000a0002000e000200fc000400fd00040002000400010004001600040015000400040001000500010050000601000002003d00010000005801060014001b0010001c0010003e0010001f000300070002000b000200060001000a0002000e000200fc000400fd00040002000400010004001600040015000400040001000500010050000601000002003d0001";
10821089
let packets_hex = "0009000200a3a50e6658cbab000001640000000801020066c0a8120a8d180c0200000000c2c00035000200000000000000000000000000010000005200a31e7700a31e771100080001c0a8120a8d180c02000000009e230035000200000000000000000000000000010000005200a3197b00a3197b110008000101060063fd010008000000002a20235f1f7b9379fd00000000000000b2f208fffe2011800000000000000000000000000000000001f676e2b5003500000200000000000000000000000000010000006600a3197b00a3197b1100109027e0436d86dd01";
1083-
let combined = format!("{}{}", templates_hex, packets_hex);
1084-
let packets = hex::decode(combined).unwrap();
1085-
assert_yaml_snapshot!(NetflowParser::default().parse_bytes(&packets).packets);
1090+
let mut parser = NetflowParser::default();
1091+
let mut packets = Vec::new();
1092+
for packet_hex in [templates_hex, packets_hex] {
1093+
let parsed = parser.parse_bytes(&hex::decode(packet_hex).unwrap());
1094+
assert!(parsed.error.is_none());
1095+
packets.extend(parsed.packets);
1096+
}
1097+
assert_yaml_snapshot!(packets);
10861098
}
10871099

10881100
#[test]
@@ -1140,14 +1152,22 @@ mod restored_legacy_tests {
11401152
4, 0, 12, 0, 4, 0, 2, 0, 4, 1, 0, 0, 28, 1, 2, 3, 4, 1, 2, 3, 3, 1, 2, 3, 2, 0, 2,
11411153
0, 2, 0, 1, 2, 3, 4, 5, 6, 7,
11421154
];
1143-
let mut all = vec![];
1144-
all.extend_from_slice(&v9_packet);
1145-
all.extend_from_slice(&v5_packet);
1146-
all.extend_from_slice(&v7_packet);
1147-
all.extend_from_slice(&v9_packet);
1148-
all.extend_from_slice(&ipfix_packet);
1149-
all.extend_from_slice(&v5_packet);
1150-
assert_yaml_snapshot!(NetflowParser::default().parse_bytes(&all).packets);
1155+
let mut parser = NetflowParser::default();
1156+
let mut packets = parser.parse_bytes(&v9_packet).packets;
1157+
1158+
let mut legacy_batch = Vec::new();
1159+
legacy_batch.extend_from_slice(&v5_packet);
1160+
legacy_batch.extend_from_slice(&v7_packet);
1161+
packets.extend(parser.parse_bytes(&legacy_batch).packets);
1162+
1163+
packets.extend(parser.parse_bytes(&v9_packet).packets);
1164+
1165+
let mut sized_batch = Vec::new();
1166+
sized_batch.extend_from_slice(&ipfix_packet);
1167+
sized_batch.extend_from_slice(&v5_packet);
1168+
packets.extend(parser.parse_bytes(&sized_batch).packets);
1169+
1170+
assert_yaml_snapshot!(packets);
11511171
}
11521172

11531173
#[test]

src/variable_versions/config.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ pub const MAX_FIELD_COUNT: usize = 10_000;
2727
/// This prevents CPU-bound DoS from maliciously large flowsets.
2828
pub const DEFAULT_MAX_RECORDS_PER_FLOWSET: usize = 1024;
2929

30+
/// Default maximum size of one caller-delimited NetFlow v9 export packet.
31+
pub const DEFAULT_MAX_V9_FRAME_SIZE_BYTES: usize = 65_535;
32+
3033
/// Configuration for V9 and IPFIX parsers.
3134
///
3235
/// Controls template cache size, field limits, TTL, enterprise field definitions,
@@ -104,6 +107,8 @@ pub enum ConfigError {
104107
InvalidDecodedFieldValueLimit(usize),
105108
/// Decoded field-payload-byte message limit must be greater than 0.
106109
InvalidDecodedFieldPayloadByteLimit(usize),
110+
/// NetFlow v9 frame size must be greater than 0
111+
InvalidV9FrameSize(usize),
107112
/// Pending flow max_total_bytes must be >= max_entry_size_bytes
108113
InvalidPendingTotalBytes {
109114
max_total_bytes: usize,
@@ -193,6 +198,13 @@ impl std::fmt::Display for ConfigError {
193198
bytes
194199
)
195200
}
201+
ConfigError::InvalidV9FrameSize(size) => {
202+
write!(
203+
f,
204+
"Invalid NetFlow v9 frame size: {}. Must be greater than 0.",
205+
size
206+
)
207+
}
196208
ConfigError::EmptyAllowedVersions => {
197209
write!(
198210
f,

src/variable_versions/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ pub use config::ParserConfig;
8787
pub(crate) use config::ParserFields;
8888
pub use config::{
8989
Config, ConfigError, DEFAULT_MAX_RECORDS_PER_FLOWSET, DEFAULT_MAX_TEMPLATE_CACHE_SIZE,
90-
MAX_FIELD_COUNT,
90+
DEFAULT_MAX_V9_FRAME_SIZE_BYTES, MAX_FIELD_COUNT,
9191
};
9292
pub use output_budget::{
9393
DEFAULT_MAX_DECODED_FIELD_PAYLOAD_BYTES_PER_MESSAGE,

0 commit comments

Comments
 (0)