1.0.4
May 9, 2026 · View on GitHub
Fixes
-
TemplateStore: source-eviction inAutoScopedParserno longer leaks store entries. Whenmax_sourcescapacity was reached and a per-source parser was popped from the LRU, every template that source had written under its scope (e.g.v9:10.0.0.1:2055/0) was orphaned in the store forever.evict_global_lrunow callsclear_v9_templates/clear_ipfix_templateson the evicted parser before dropping it, so the external store keyspace tracks the live source set. -
TemplateStore:clear_v9_templates/clear_ipfix_templatesnow record backend errors. Previously these methods calledlet _ = store.remove(...), silently swallowing failures. They now bumptemplate_store_backend_errorson each failedremove, matching every other store call site. -
TemplateStore: misleading inline doc comment onclear_*_templatesremoved. The comment claimed that after a clear, "subsequent reads do not transparently repopulate the in-process cache via read-through" — true only for templates that were in the in-process LRU at clear time. Templates evicted from the LRU before the call, or written by another parser instance under the same scope, remain reachable via read-through. The trait intentionally exposes onlyget/put/remove, not a per-scope wipe; the comment now documents this honestly. -
set_template_store_scoperustdoc: clarified that the scope must be set before the firstparse_bytescall to avoid orphaning entries written under the previous scope, and thatwith_template_store_scopeon a builder fed intoAutoScopedParseris overridden by the auto-derived per-source scope. -
Read-through hit semantics documented. Clarified that a successful
TemplateStoreread-through incrementshits(counted as a hit, not a miss) and that restored templates have their TTL re-stamped toInstant::now().
Tests
-
Rewrote
read_through_drives_pending_flow_replay. The previous test never queued a pending flow before the template arrived — it would have passed even if the read-through-driven pending-flow replay code were deleted. Now exercises the full path: data record arrives before any template is known → queued → template is written to the store by another replica → next data record's read-through restores the template AND triggers replay of the queued flow. -
Strengthened
auto_scoped_parser_uses_per_source_scope. Now also validates cross-replica round-trip: a freshAutoScopedParserreading the store must decode each source's data record against the correctly-scoped template. -
Added eviction-cleanup test for
AutoScopedParserverifying that evicted source parsers' store entries are removed. -
Coverage filled in for backend
removefailures (inject_remove_failuresis now exercised), IPFIX-side codec corruption rejection, IPFIX-side LRU eviction propagation to the store, and IPFIX-sideTemplateEvent::Restoredfiring.
Examples
-
New example:
horizontal_scale_out_template_store. Demonstrates the feature's headline use case — twoNetflowParserinstances sharing anInMemoryTemplateStore. Replica A learns a template and goes away; replica B starts cold and decodes a data record against the template via read-through. Run with:cargo run --example horizontal_scale_out_template_store
1.0.3
Features
-
Pluggable secondary template storage (
TemplateStore). Templates can now be persisted to and re-read from an external backend such as Redis or NATS KV via a newTemplateStoretrait. With a store configured the parser writes through every learned template, consults the store on every cache miss, and propagates LRU evictions, withdrawals, and explicit clears so the store stays in sync. This unblocks running multiple stateless parser instances behind a UDP load balancer without source-IP-affinity routing.Wire up via the new builder hooks:
let store = Arc::new(my_redis_backed_store); let parser = NetflowParser::builder() .with_template_store(store) .with_template_store_scope("collector-eu-west-1") .build()?;AutoScopedParserautomatically derives a per-source scope so two exporters using the same template ID with different layouts do not collide in the store. The trait sees opaqueVec<u8>payloads encoded with a small custom binary wire format — noserde_jsonor other runtime serializer is added to the dependency tree. AnInMemoryTemplateStorereference impl is provided for tests. See the newtemplate_storemodule for the protocol. -
New public API:
TemplateStore,TemplateStoreKey,TemplateKind,TemplateStoreError,InMemoryTemplateStore,NetflowParserBuilder::with_template_store,NetflowParserBuilder::with_template_store_scope,NetflowParser::set_template_store_scope.
Tests
- Restored 34 snapshot-based parser tests in a new
restored_legacy_testsmodule insrc/tests.rs. These tests had been deleted in PR #210 ("further template validation"), leaving 33 orphaned.snapfiles that were swept up later in PR #262. Coverage included real-world v9/IPFIX captures (it_parses_v9_ipv6flowlabel,it_parses_v9_template_and_data_packet,it_parses_ipfix_scappy_example, mixed-enterprise field templates, multi-template IPFIX, etc.), version-filter checks (it_doesnt_allow_v5/v7/v9/ipfix), and re-export round-trip tests (it_parses_*_and_re_exports). - Tests adapted to current APIs:
TemplateWithTtl::new(...)→TemplateWithTtl::new_without_ttl(Arc::new(...))(cache now stores Arc-wrapped templates);with_allowed_versions(&[])is rejected by the builder, so version-filter tests now allow only the other versions to verify filtering. - Snapshot count restored from 14 → 48.
Dependencies
- Bumped
lrufrom 0.16 to 0.18. - Bumped
etherparsefrom 0.19 to 0.20.
1.0.2
Performance
-
Inlined
parse_data_fieldsinto V9 record loop — eliminates per-record function call overhead and Vec allocation, giving the optimizer a single loop nest. ~8% improvement in V9 data parsing benchmarks (130 µs → 120 µs for 1000 flows). -
Added fast big-endian parsers (
fast_parsemodule) — hand-rolledfrom_be_bytesreplacements for nom's genericbe_uint, which LLVM cannot optimize intobswap/revat wider widths. 9–26x speedup on micro-benchmarks for u64/u128 parsing. Applied toDataNumber::parse, IP/MAC field parsing, and IPFIX variable-length fields. -
Added
hot_path_benchfor targeted V9/IPFIX data parsing benchmarks.
1.0.1
Documentation
- Removed outdated 0.7.x → 0.8.0 migration guide from README
- Consolidated redundant README sections: removed duplicate hex dump comments, complete configuration example, and inline struct definitions
- Merged "V9/IPFIX Notes" into the Template Management Guide overview
- Merged "Template Collision Detection" into Template Cache Metrics
- Removed manual "Handling Missing Templates" section (superseded by Pending Flow Caching)
- Consolidated Iterator API benefits and examples into a single section
1.0.0
Breaking Changes
-
ipfix_lookupandv9_lookupmodules moved into their protocol directoriesvariable_versions::ipfix_lookup::*→variable_versions::ipfix::lookup::*variable_versions::v9_lookup::*→variable_versions::v9::lookup::*- All types (
IPFixField,IANAIPFixField,CiscoIPFixField,V9Field,ScopeFieldType, etc.) remain unchanged — only the module path has changed - Migration: update
usestatements to the new paths
-
FieldValue::Durationnow wrapsDurationValueinstead ofstd::time::Duration- New
DurationValueenum preserves the original time unit (Seconds,Millis,MicrosNtp,NanosNtp), field width (4 or 8 bytes), and raw NTP fractional seconds - Enables lossless round-trip serialization — previously, unit and width information was lost during parsing
- Use
DurationValue::as_duration()to get astd::time::Durationfor ergonomic access - JSON serialization output is unchanged (delegates to
Duration)
- New
-
FieldValue::Stringnow wrapsStringValueinstead ofString- New
StringValuestruct containsvalue: String(cleaned display string) andraw: Vec<u8>(original wire bytes) - Enables lossless round-trip serialization — previously, lossy UTF-8 conversion, control character filtering, and P4 prefix stripping made the string non-invertible
TryFrom<&FieldValue> for Stringstill returns the cleanedvalue- JSON serialization output is unchanged (serializes only the
valuefield)
- New
-
FieldValue::MacAddrnow wraps[u8; 6]instead ofString- Eliminates a heap allocation per MAC address field
- Serialization output is unchanged (
"aa:bb:cc:dd:ee:ff"format)
-
DataNumber::to_be_bytes()andFieldValue::to_be_bytes()removed- Use
write_be_bytes()instead, which writes directly into a caller-provided buffer
- Use
-
NoTemplateInfochanges- Removed
available_templatesfield. Useparser.v9_available_template_ids()orparser.ipfix_available_template_ids()instead - Added
truncated: boolfield. Code that destructuresNoTemplateInfomust include the new field (or use..)
- Removed
-
Cache observability types renamed for clarity
CacheStats→CacheInfo(structural cache state: size, capacity, TTL, pending count)CacheMetricsSnapshot→CacheMetrics(operational counters: hits, misses, evictions, etc.)ParserCacheStats→ParserCacheInfo(groups V9 + IPFIXCacheInfo)- The internal mutable metrics type is now
pub(crate) CacheMetricsInner(not part of public API) - Methods renamed:
v9_cache_stats()→v9_cache_info(),ipfix_cache_stats()→ipfix_cache_info() - Scoped parser methods renamed:
all_stats()→all_info(),get_source_stats()→get_source_info(),v9_stats()→v9_info(),ipfix_stats()→ipfix_info(),legacy_stats()→legacy_info() - Migration: rename types and method calls. The
CacheInfo.metricsfield is nowCacheMetrics(wasCacheMetricsSnapshot)
-
CacheMetrics(formerlyCacheMetricsInner) methods now require&mut selfinstead of&self- Uses plain
u64counters instead ofAtomicU64, removing atomic overhead in the single-threaded parser
- Uses plain
-
NetflowParserfields are nowpub(crate)v9_parser,ipfix_parser,allowed_versions,max_error_sample_sizeare no longer public- Use accessor methods:
v9_parser(),v9_parser_mut(),ipfix_parser(),ipfix_parser_mut(),allowed_versions(),is_version_allowed(),max_error_sample_size()
-
NetflowParserBuilder::build()returnsConfigErrorinstead ofStringon failure- Now calls
validate()and rejects out-of-range version numbers inallowed_versions - Added
NetflowParserBuilder::validate()for lightweight config validation without allocating parser internals
- Now calls
-
with_allowed_versions()now takes&[u16]instead ofHashSet<u16>- The
allowed_versionsfield is nowpub(crate) [bool; 11]; useallowed_versions()oris_version_allowed()accessors - Rejects out-of-range version numbers via
ConfigError::InvalidAllowedVersion
- The
-
ProtocolTypes::Unknownis nowUnknown(u8)- Carries the original protocol number instead of being a unit variant
- Pattern matching must use
Unknown(_)orUnknown(v) #[repr(u8)]removed; useu8::from(protocol)instead ofprotocol as u8PartialOrd/Ordnow compare by protocol number value, not enum declaration order
-
V5 and V7 structs no longer derive
Nom- Code calling
V5::parse()orV7::parse()via the nom-deriveParsetrait must useV5::parse_direct()/V7::parse_direct()instead - The
V5Parser::parse()andV7Parser::parse()entry points are unchanged
- Code calling
-
V5/V7
countfield now rejects values exceeding specification limits- V5 rejects
count > 30with a parse error instead of silently capping - V7 rejects
count > 28with a parse error instead of silently capping
- V5 rejects
-
V9
OptionsDataFields.options_fieldschanged fromVec<Vec<V9FieldPair>>toVec<V9FieldPair>- Code that iterates nested Vecs must flatten
-
TemplateHooksignature now returnsResult<(), TemplateHookError>- Hooks registered via
on_template_event()must returnOk(())on success - New
TemplateHookErrortype for hook error reporting - The parser logs hook errors but continues processing (hooks cannot abort parsing)
- Hooks registered via
-
RouterScopedParser::parse_from_sourceandAutoScopedParser::parse_from_sourcenow returnParseResultinstead ofResult<Vec<NetflowPacket>, NetflowError>- Consistent with
NetflowParser::parse_bytes()return type - Builder errors now return
ParseResult { packets: vec![], error: Some(...) }instead ofErr(...)
- Consistent with
-
Renamed types and variants
V9Field::BpgIpv6NextHop→V9Field::BgpIpv6NextHop(typo fix)V9Field::ImpIpv6CodeValue→V9Field::IcmpIpv6CodeValue(field ID 179, typo fix)IpFixFlowRecord→IPFixFlowRecordfor consistent casing- Module
variable_versions::data_number→variable_versions::field_value
-
Removed deprecated items
NetflowPacketErrorandNetflowParseErrortype aliases — useNetflowErrordirectlywith_builder()onRouterScopedParserandAutoScopedParser— usetry_with_builder()multi_source()onNetflowParserBuilder— usetry_multi_source()IpFixFlowRecordtype alias — useIPFixFlowRecordvariable_versions::data_numbermodule — usevariable_versions::field_valuecrate::field_typesmodule — usevariable_versions::field_typescrate::template_eventsmodule — usevariable_versions::template_eventsFieldValue::Unknownvariant — useFieldValue::Vec
-
New enum variants (exhaustive match impact)
ConfigErrorgainsInvalidAllowedVersion(u16),InvalidFieldCount(usize),InvalidTemplateTotalSize(usize),InvalidEntriesPerTemplate(usize),InvalidEntrySize(usize),InvalidTtlDuration,EmptyAllowedVersions,InvalidPendingTotalBytes { max_total_bytes, max_entry_size_bytes }
-
RouterScopedParser::iter_packets_from_sourceandAutoScopedParser::iter_packets_from_sourcenow returnResult- Return type changed from
impl IteratortoResult<impl Iterator, NetflowError> - Returns an error when the max source limit is reached
- Callers must unwrap or match on the result before iterating
- Return type changed from
-
ScopeDataFieldgains anUnknown(u16, Vec<u8>)variant- Code with exhaustive
matchonScopeDataFieldmust add anUnknown(_, _)arm
- Code with exhaustive
-
ApplicationId.selector_idchanged fromDataNumbertoOption<DataNumber>Nonewhen the field is 1 byte (classification engine ID only, no selector)- Fixes round-trip serialization: previously a 1-byte field serialized to 2 bytes
-
CacheInfostruct field changesmax_sizerenamed tomax_size_per_cache(clarifies that it applies per internal LRU cache)- Added
num_caches: usizefield (V9 has 2 caches, IPFIX has 4) - Code that destructures
CacheInfomust update the field name and includenum_caches(or use..)
-
TemplateEventfieldtemplate_idchanged fromu16toOption<u16>- All variants (
Learned,Collision,Evicted,Expired,MissingTemplate) now useOption<u16> Nonewhen the event is derived from metric deltas (specific ID not available from metrics layer)- Pattern matching must use
template_id: Some(id)ortemplate_id: _
- All variants (
-
CacheMetricsInnerrecord methods scoped topub(crate)record_hit(),record_miss(),record_eviction(),record_insertion(),record_expiration(),record_collision(), and pending flow record methods changed frompubtopub(crate)reset()method removed entirelysnapshot(),new(),hit_rate()remain public
-
trigger_template_event()changed from&selfto&mut self- Required because hook error counters are now plain
u64(not atomic) - Code calling this method from an immutable reference must switch to
&mut
- Required because hook error counters are now plain
-
parse_bytes_as_netflow_common_flowsets()return type changed (featurenetflow_common)- Was:
Vec<NetflowCommonFlowSet> - Now:
(Vec<NetflowCommonFlowSet>, Option<NetflowError>) - Callers must destructure the tuple or use
.0to get the flowsets
- Was:
-
NetflowCommonFlowSet.first_seenandlast_seenwidened fromOption<u32>toOption<u64>(featurenetflow_common)- Supports IPFIX absolute epoch millisecond timestamps that exceed
u32::MAX - Code that destructures or stores these as
u32must update
- Supports IPFIX absolute epoch millisecond timestamps that exceed
-
NetflowCommonError::UnknownVersionnow wrapsu16instead ofNetflowPacket(featurenetflow_common)- Carries only the version number, not the entire packet
- Pattern matching must use
UnknownVersion(version)instead ofUnknownVersion(packet)
-
get_source_info()on scoped parsers changed from&selfto&mut self- LRU cache iteration requires mutable access
- Code calling this from an immutable reference must switch to
&mut
-
#[non_exhaustive]added to public types- Affected types:
NetflowPacket,ParseResult,NetflowError,ConfigError,FieldValue,Config,PendingFlowsConfig,CacheInfo,ParserCacheInfo,CacheMetrics,NoTemplateInfo,TemplateEvent,TemplateProtocol,ScopingInfo - External code with exhaustive
matchstatements must add a wildcard_ =>arm - External code constructing these structs directly must use
..for forward compatibility
- Affected types:
-
New
NetflowErrorvariant:FilteredVersion { version: u16 }- Returned when a packet's version is not in
allowed_versions - Code with exhaustive
matchonNetflowErrormust add this arm
- Returned when a packet's version is not in
-
Enterprise field registration is now IPFIX-only
register_enterprise_field()andregister_enterprise_fields()on the builder no longer register into V9 config- V9 does not support the enterprise bit; previously the registry was silently stored but unused
New Features
-
New IPFIX field types for flags, bitmasks, and enumerations
- Added 12 new dedicated field types in
field_typesmodule, following theForwardingStatuspattern:- Bitmask/flag types:
FragmentFlags(field 197),TcpControlBits(field 6),Ipv6ExtensionHeaders(field 64),Ipv4Options(field 208),TcpOptions(field 209),IsMulticast(field 206),MplsLabelExp(fields 203, 237) - Enumeration types:
FlowEndReason(field 136),NatEvent(field 230),FirewallEvent(field 233),MplsTopLabelType(field 46),NatOriginatingAddressRealm(field 229)
- Bitmask/flag types:
- These fields were previously decoded as
UnsignedDataNumberand now produce structured, self-describing values - All types support round-trip conversion (parse → typed value → raw bytes)
- Each type has corresponding
FieldDataTypeandFieldValuevariants
- Added 12 new dedicated field types in
-
New
field_typesmodule withForwardingStatusenum- Added
field_types::ForwardingStatus— decodes field ID 89 (RFC 7270) into status category and reason code variants - Status categories: Unknown, Forwarded, Dropped, Consumed — with specific reason codes (e.g.,
DroppedAclDeny,ForwardedFragmented,ConsumedTerminatedForUs) - Added
FieldDataType::ForwardingStatusandFieldValue::ForwardingStatusfor automatic decoding in both V9 and IPFIX field_typesmodule is designed for future custom field type additions
- Added
-
New V9 field types (IDs 128-175)
- Added 48 new
V9Fieldvariants from the IANA IPFIX Information Elements registry - Includes:
BgpNextAdjacentAsNumber,ExporterIpv4Address,ExporterIpv6Address,DroppedOctetDeltaCount,FlowEndReason,WlanSsid,FlowStartSeconds,FlowEndSeconds,FlowStartMicroseconds,FlowEndMicroseconds,FlowStartNanoseconds,FlowEndNanoseconds,DestinationIpv6Prefix,SourceIpv6Prefix, and more - Each field has the correct
FieldDataTypemapping per the IANA registry
- Added 48 new
-
Naming aliases
- Added Rust-idiomatic type aliases:
Ipfix,IpfixParser,IpfixField,IpfixFieldPair,IpfixFlowRecord - Re-exported from crate root for convenience
- Added Rust-idiomatic type aliases:
-
Error type improvements
ConfigError,DataNumberError,FieldValueError, andNetflowCommonErrornow implementDisplayandstd::error::ErrorUnallowedVersionnow carries the version number
-
Source eviction API for
AutoScopedParser- Added
remove_ipfix_source(),remove_v9_source(),remove_legacy_source()for pruning stale sources - Prevents monotonic growth of internal
HashMaps in long-running deployments
- Added
-
#[must_use]onParseResult- Compiler warns when
parse_bytes()return values are silently discarded
- Compiler warns when
-
Builder methods for record and template size limits
with_max_records_per_flowset(),with_v9_max_records_per_flowset(),with_ipfix_max_records_per_flowset()— controlVec::with_capacitycap for parsed records (default 1024)with_max_template_total_size(),with_v9_max_template_total_size(),with_ipfix_max_template_total_size()— control maximum total byte size across all fields in a template
-
Idle source pruning for scoped parsers
prune_idle_sources(older_than: Duration)on bothRouterScopedParserandAutoScopedParser- Removes sources that haven't been accessed within the given duration
- Returns the number of pruned sources
-
hook_error_count()onNetflowParser- Returns total number of hook errors and panics encountered across all registered hooks
- Useful for production monitoring of hook health
-
#![forbid(unsafe_code)]enforced- The crate contains zero
unsafeblocks; this is now enforced at the crate level
- The crate contains zero
-
Cross-variant numeric extraction on
DataNumberandFieldValueDataNumber::as_u8(),as_u16(),as_u64()— try to extract a value from any numeric variant, narrowing or widening as needed (returnsNoneif the value doesn't fit)FieldValue::as_u8()— delegates toDataNumberand also convertsProtocolTypeto its numeric valueFieldValue::as_u16()— delegates toDataNumberFieldValue::as_u64()— delegates toDataNumberand convertsDurationvariants to milliseconds- Unlike the existing
TryFromimpls (which only match the exact variant), these methods work across all numeric widths
-
Expanded root re-exports
Config,ConfigError,TtlConfig,EnterpriseFieldRegistry,CacheMetrics,NoTemplateInfo,DEFAULT_MAX_RECORDS_PER_FLOWSET,DEFAULT_MAX_SOURCES— now available at crate rootDataNumber,FieldDataType,FieldValue— commonly used field/data types at crate rootV9Field,V9FieldPair,V9FlowRecord— symmetric with IPFIX equivalents already at root
Bug Fixes
-
Fixed
ApplicationIdparsing and round-trip for 1-byte fields- A 1-byte
ApplicationId(classification engine ID only, no selector) previously caused a parse error selector_idis nowOption<DataNumber>(Nonefor zero-length selectors), fixing round-trip serialization
- A 1-byte
-
Fixed template event hooks never firing during parsing
on_template_event()callbacks were registered but never triggered by V9/IPFIX parsing- Now fires
TemplateEvent::Learnedfor each template in parsed packets - Now fires
TemplateEvent::MissingTemplatefor NoTemplate flowsets
-
Fixed IPFIX reserved set IDs 4-255 stopping flowset parsing
- Per RFC 7011, set IDs 4-255 are reserved for future use
- Previously caused a parse error that stopped processing remaining flowsets in the message
- Now skipped gracefully
-
Corrected V9 field data type mappings
IfName(82),IfDesc(83),SamplerName(84) now correctly map toFieldDataType::Stringinstead ofUnsignedDataNumberLayer2packetSectionData(104) now correctly maps toFieldDataType::Vecinstead ofUnsignedDataNumber
-
Fixed
DurationNanosNTPunit conversion bug- Fractional NTP seconds were passed to
Duration::from_micros()instead ofDuration::from_nanos(), producing durations 1000x too large
- Fractional NTP seconds were passed to
-
Fixed IPFIX template serialization losing the enterprise bit
- Round-trip (parse → serialize) now correctly restores bit 15 of
field_type_numberfor enterprise fields
- Round-trip (parse → serialize) now correctly restores bit 15 of
-
Fixed
ScopeDataFieldsilently truncating scope field values- Previously truncated to 4 bytes regardless of the template-declared
field_length
- Previously truncated to 4 bytes regardless of the template-declared
-
Fixed
NoTemplateInfo.truncatedfield- Now correctly set to
truewhen raw data is truncated tomax_error_sample_size
- Now correctly set to
-
Fixed
Dot1qCustomerSourceMacaddress(IPFIX field 414) mapped toStringinstead ofMacAddr- Now consistent with
Dot1qCustomerDestinationMacaddress(field 415) and the reverse information element entries
- Now consistent with
-
Fixed
NatEventfield type mapping in V9 lookup- V9 field 230 (
NatEvent) was mapped toUnsignedDataNumberinstead ofNatEvent - Now correctly decoded as the structured
NatEventenum
- V9 field 230 (
-
Fixed
ReverseApplicationId(IPFIX enterprise field 95) using wrongFieldDataType- Was
FieldDataType::String, now correctlyFieldDataType::ApplicationIdper RFC 5103
- Was
-
Fixed
ReverseForwardingStatus(IPFIX enterprise field 89) using wrongFieldDataType- Was
FieldDataType::UnsignedDataNumber, now correctlyFieldDataType::ForwardingStatusper RFC 5103
- Was
-
Fixed
TcpOptionsfield length guard checking for 4 bytes instead of 8TcpOptionsis a 64-bit bitmask; the guard now correctly requiresfield_length == 8
-
Fixed
TcpControlBitsfield length guard being too permissive- Changed from
field_length <= 2tofield_length == 2to prevent misinterpretation of wider fields
- Changed from
-
Fixed
PendingFlowsConfig::max_entry_size_bytesdefault exceeding valid FlowSet length- Default changed from
u16::MAX(65535) tou16::MAX - 4(65531), the maximum data size that fits within the 16-bit FlowSet length field after the 4-byte header
- Default changed from
-
Fixed V9
serialize_options_data_bodymissing 4-byte padding- RFC 3954 requires all flowsets to be padded to 4-byte boundaries
- The V9 OptionsData serializer was the only flowset body that omitted padding
-
Fixed
set_ttl_config()not validatingDuration::ZEROset_ttl_config(Some(TtlConfig::new(Duration::ZERO)))could bypass validation thatadd_config()enforced, causing all templates to instantly expire
-
Fixed IPFIX duplicate field validation for V9-style templates in IPFIX packets
- Added
has_duplicate_fields()check to V9Template validation in the IPFIX parser - Added
has_duplicate_scope_fields()andhas_duplicate_option_fields()checks to V9OptionsTemplate validation
- Added
Safety and Correctness
-
parse_bytes()reports aFilteredVersionerror instead of silently stopping on unallowed versions -
Versions >= 11 now correctly return
UnsupportedVersioninstead of being misclassified asFilteredVersion -
ApplicationIdfield parsing useschecked_subinstead ofsaturating_subto properly error on zero-length fields -
Vec::with_capacityfor parsed records is capped at 1024 in V9 and IPFIX to prevent untrusted input from causing large allocations -
V9
Template::is_valid()now rejects templates with empty fields or all-zero-length fields -
V9 and IPFIX
OptionsTemplatevalidation now rejects templates with zero scope fields (RFC 3954/7011 require at least one) -
V9
OptionsTemplate::is_valid()now rejectsoptions_scope_lengthandoptions_lengththat aren't multiples of 4 -
V9 templates embedded in IPFIX packets are now validated against parser limits (field count, total size, zero-length fields)
-
Scoped parser source count limits
RouterScopedParserandAutoScopedParsernow enforce a maximum source count (default: 10,000)- Prevents unbounded memory growth from spoofed or misconfigured source addresses
- New sources are rejected with an error when at capacity
- Configurable via
with_max_sources()
-
V5/V7 flow count validation
- V5
countfield capped at 30 per Cisco specification - V7
countfield capped at 28 per Cisco specification - Prevents oversized
Vec::with_capacityallocations from untrusted input
- V5
-
CacheMetricscounter overflow protection- All metric counters now use
saturating_addinstead of+= 1 hit_rate()andtotal_lookups()usesaturating_addto prevent overflow in rate calculations
- All metric counters now use
-
ScopeDataFieldhandles unknown scope field types gracefully- Previously, unknown scope field types caused a hard parse error
- Now parses them as
ScopeDataField::Unknown(field_type_number, raw_bytes) - Improves robustness with vendor-specific scope types
-
NetflowPacketIteratornow implementsDebug- Shows
remaining_bytescount anderroredstate for easier debugging
- Shows
-
Added
rust-version = "1.88"toCargo.toml- Documents the minimum supported Rust version required by the crate
-
IPFIX header length validation
- IPFIX messages with
header.length < 16are now rejected as malformed - Previously,
saturating_sub(16)silently accepted them as valid empty messages
- IPFIX messages with
-
IPFIX
FieldParserinfinite loop prevention- Added progress check (
std::ptr::eq) in bothparseandparse_with_registryloops - If no bytes are consumed after parsing a full record, the loop breaks instead of spinning forever
- Defends against crafted templates with all-zero-length variable-width fields
- Added progress check (
-
V9
OptionsTemplate::is_valid()now rejects all-zero-length fields- Previously only
Template::is_valid()checked for at least one non-zero field length - A crafted OptionsTemplate with all
field_length = 0fields could causemany0to loop infinitely - Now requires at least one scope or option field with
field_length > 0
- Previously only
-
PendingFlowsConfigvalidation hardenedmax_total_bytes == 0now returns an errormax_entry_size_bytes > 65531now returns an error (exceeds FlowSet length field capacity)max_entries_per_template == 0now returns an error
-
Empty
allowed_versionsrejected at validation timewith_allowed_versions(&[])now returnsConfigError::EmptyAllowedVersionsinstead of silently disabling all parsing
-
Scoped parser builder errors no longer panic
RouterScopedParserandAutoScopedParserno longer useexpect()when building parsers for new sources- Builder failures now return errors through
ParseResultorResultinstead of panicking
Performance
-
V5/V7 direct byte parsing
- Replaced nom-derive generated parsers with hand-written direct byte reads for V5 and V7
- Fixed-layout protocols now use a single bounds check instead of per-field nom combinator calls
- V5 parsing is ~2x faster at scale (e.g., 100 flows: 1,147ns → 626ns, -44%)
- V7 parsing receives the same treatment (52-byte fixed flow records)
Ipv4Addrfields constructed directly from bytes instead ofbe_u32→Ipv4Addr::from()to_be_bytes()now pre-allocates withVec::with_capacity()based on known sizes
-
Hot-path allocation reduction
- Added
DataNumber::write_be_bytes()andFieldValue::write_be_bytes()methods that write directly into a caller-provided buffer, avoiding per-fieldVec<u8>allocations TemplateMetadata::inserted_atis nowOption<Instant>, skippingInstant::now()when TTL is disabledcalculate_padding()returns&'static [u8]instead of allocating aVec<u8>OptionsFieldParserreturns a flatVec<V9FieldPair>instead ofVec<Vec<V9FieldPair>>- String parsing avoids a double allocation when stripping the
"P4"prefix
- Added
-
NoTemplateInfo hot-path optimization
- Removed
available_templatesfield fromNoTemplateInfoto avoid collecting template IDs on every cache miss - Added
V9Parser::available_template_ids()andIPFixParser::available_template_ids()for on-demand querying
- Removed
-
Scoped parser optimization
AutoScopedParser::parse_from_sourceanditer_packets_from_sourceno longer clone the parser builder on every call; builder is only cloned on cache miss
-
Bulk pending flow drop tracking
- Added
CacheMetrics::record_pending_dropped_n(n)for batch metric updates - Replaced per-entry loops with single bulk calls in pending flow cache eviction paths
- Added
Refactoring
-
Reduced code duplication between V9 and IPFIX parsers
- Extracted shared
calculate_padding(),NoTemplateInfo,get_valid_template(), constants (DEFAULT_MAX_TEMPLATE_CACHE_SIZE,MAX_FIELD_COUNT,TemplateId) intovariable_versionsmodule - Consolidated
ParserConfigtrait with default method implementations foradd_config,set_max_template_cache_size,set_ttl_config,pending_flows_enabled,pending_flow_count, andclear_pending_flows - Introduced
ParserFieldsaccessor trait to enable shared default implementations
- Extracted shared
-
Module restructuring
- Split
v9.rsintov9/{mod.rs, parser.rs, serializer.rs} - Split
ipfix.rsintoipfix/{mod.rs, parser.rs, serializer.rs} - Renamed
data_number.rs→field_value.rs - Moved
field_typesfrom crate root tovariable_versions::field_types - Moved
template_eventsfrom crate root tovariable_versions::template_events
- Split
-
Code cleanup
- Removed unused
enterprise_registryfield fromV9Parser(was#[allow(dead_code)]) - Replaced
contains_key+unwrappattern inAutoScopedParserwithentry()API - Added compile-time assertion for
DEFAULT_MAX_TEMPLATE_CACHE_SIZE > 0 - Deleted orphaned snapshot file
CommonTemplate::get_fieldsreturns&[TemplateField]instead of&Vec<TemplateField>— idiomatic Rust: return slices rather than references toVec
- Removed unused
Dependencies
- Removed
byteordercrate — manual 3-byte big-endian serialization for u24/i24 types - Removed
mac_addresscrate — MAC addresses parsed directly from raw bytes
Documentation
- Added module-level
//!docs tov9/mod.rs,ipfix/mod.rs,ttl.rs, and all integration test files - Added
///docstrings to all undocumented public structs, enums, traits, and methods (Config,V9,V9Parser,IPFix,IPFixParser,FlowSetBody,Header,FlowSet,Template,OptionsTemplate,TemplateField,CommonTemplate, etc.) - Added
//comments to all unit and integration test functions describing what they verify - Fixed malformed doc block where
build()andon_template_event()docs were merged inNetflowParserBuilder - Fixed unclosed code fence in
ScopeDataField::parsedoc comment - Fixed doc link warning for
EnterpriseFieldRegistryinvariable_versionsmodule docs
Testing and Benchmarks
- Added concurrent parsing tests (
Arc<Mutex<RouterScopedParser>>shared across threads, independent parsers per thread) - Added memory bounds tests (cache stats within configured limits, error sample size bounded)
- Added
steady_state_bench— V9 and IPFIX benchmarks with pre-warmed template cache (5, 10, 30, 100 flows) - Added comprehensive round-trip serialization tests (
tests/round_trip.rs) — 31 tests covering V7, V9 (template + data), IPFIX (template + data), all 13 IANA typed field types,ApplicationIdvariants (1-byte and 4-byte), andVecfallback for wrong-length fields
Behavioral Changes
-
clear_v9_templates()andclear_ipfix_templates()now also clear pending flows- Prevents stale pending flows from being replayed against a replacement template with the same ID
- Previously, clearing templates left orphaned pending flows in the cache
-
has_v9_template()andhas_ipfix_template()now respect TTL- Returns
falsefor expired templates when TTL is configured - Previously returned
truefor templates that would be rejected at parse time
- Returns
-
v9_available_template_ids()andipfix_available_template_ids()now return sorted, deduplicated results- Same template ID could previously appear twice (once from templates cache, once from options_templates cache)
-
clear_v9_pending_flows()andclear_ipfix_pending_flows()now record dropped metrics- Previously, clearing pending flows did not update
pending_droppedcounters - Now records the count of cleared entries via
record_pending_dropped_n()
- Previously, clearing pending flows did not update
-
Data::with_template_field_lengths()now validates field count- Panics if
template_field_lengthslength doesn't match the field count of the first record - Prevents silent corrupt serialization from mismatched metadata
- Panics if
-
IPFIX variable-length field serialization rejects zero-length fields
to_be_bytes()now returns an error for zero-length variable-length fields per RFC 7011 Section 7- Previously wrote a
0x00prefix which the parser would reject, breaking round-trip
Known Limitations
- IPFIX variable-length field serialization requires
template_field_lengthsto_be_bytes()on IPFIX messages correctly emits RFC 7011 Section 7 variable-length prefixes whentemplate_field_lengthsis populated (which the parser does automatically)- However, manually constructed
Datastructs viaData::new()have emptytemplate_field_lengths, causing variable-length fields to serialize without the length prefix - Workaround: populate
template_field_lengthsfrom the corresponding template before serializing
0.9.0
New Features
- Pending Flow Caching
- Flows arriving before their template are now cached and automatically replayed when the template arrives
- Configurable LRU cache with optional TTL expiration per pending entry
- Disabled by default; enable via builder:
with_pending_flows(),with_v9_pending_flows(), orwith_ipfix_pending_flows() - New
PendingFlowsConfigstruct for controllingmax_pending_flows(default 256),max_entries_per_template(default 1024),max_entry_size_bytes(default 65535), andttl - Pending flow metrics tracked:
pending_cached,pending_replayed,pending_dropped,pending_replay_failed - New methods:
clear_v9_pending_flows(),clear_ipfix_pending_flows() - When caching is enabled, successfully-cached
NoTemplateflowsets are removed from the parsed output; entries dropped by the cache (size/cap/LRU limits) keep theirNoTemplateflowset in the output for diagnostics - Oversized flowset bodies (exceeding
max_entry_size_bytes) are truncated tomax_error_sample_sizeat parse time, avoiding a full allocation before the cache can reject them
Safety and Correctness
NoTemplateraw_data truncationNoTemplateraw_data is truncated tomax_error_sample_sizewhen pending flow caching is disabled- Prevents large allocations from missing-template traffic when caching is not in use
- Full raw data is only retained when pending flow caching is enabled and the entry is within
max_entry_size_bytes
Bug Fixes
to_be_bytes()now recomputes header length/count from actually-serialized flowsets- V9
header.countand IPFIXheader.lengthare written based on emitted flowsets, not the struct field - Previously, skipped
NoTemplate/Emptyflowsets caused a mismatch between the header and serialized body - Returns an error if V9 flowset count or IPFIX message length exceeds
u16::MAX, instead of silently truncating - IPFIX
serialize_flowset_body()now handles allFlowSetBodyvariants (V9Templates,OptionsTemplates,V9OptionsTemplates); previously these fell through to a catch-all that produced empty bodies
- V9
Breaking Changes
- V9
FlowSetBodygains aNoTemplate(NoTemplateInfo)variant- V9 now continues parsing remaining flowsets when a template is missing, matching IPFIX behavior
- Previously, a missing template would stop parsing the entire packet
- Code with exhaustive
matchonv9::FlowSetBodymust add aNoTemplate(_)arm
ConfigErrorgains anInvalidPendingCacheSize(usize)variant- Returned when
PendingFlowsConfig::max_pending_flowsis 0 - Exhaustive matches on
ConfigErrormust add this arm
- Returned when
CacheInfo(formerlyCacheStats) gains apending_flow_count: usizefield- Code that destructures
CacheInfomust include the new field (or use..)
- Code that destructures
CacheMetricsgains four fieldspending_cached,pending_replayed,pending_dropped,pending_replay_failed- Code that destructures the struct must include the new fields (or use
..)
0.8.4
Breaking Changes
- Replaced tuple returns with named
ParserCacheInfostruct (formerlyParserCacheStats)- Functions
get_source_info(),all_info(),ipfix_info(),v9_info(), andlegacy_info()now returnParserCacheInfowith.v9and.ipfixfields instead of(CacheInfo, CacheInfo)tuples - This eliminates ambiguity about which positional element is V9 vs IPFIX
- Migration: Replace
(key, v9_stats, ipfix_stats)destructuring with(key, stats)and accessstats.v9/stats.ipfix
- Functions
Performance
- Optimized template caching using Arc for reduced cloning and added inlining hints for hot-path functions
Bug Fixes
- Fixed CI workflow: cargo-deny/cargo-audit install now skips if binary already exists (prevents cache conflict errors)
Code Cleanup
- General code cleanup
0.8.3
- Simplified docs.rs README updates
0.8.2
- Updated missing docs.rs information
0.8.1
Bug Fixes
- Fixed collision detection to only count true collisions (same template ID, different definition)
- Previously, any template retransmission was incorrectly counted as a collision
- RFC 7011 (IPFIX) and RFC 3954 (NetFlow v9) recommend sending templates multiple times at startup for reliability
- Retransmitting the same template (same ID, identical definition) is now correctly handled as a template refresh
- Only templates with the same ID but different definitions are now counted as collisions
- Uses
LruCache::peek()to check existing templates without affecting LRU ordering - No code changes required — metrics will automatically be more accurate
0.8.0
Breaking Changes
parse_bytes()now returnsParseResultinstead ofVec<NetflowPacket>- Preserves successfully parsed packets even when errors occur mid-stream
- Access packets via
.packetsfield and errors via.errorfield - Use
.is_ok()and.is_err()to check parsing status
NetflowPacket::Errorvariant removed from the enum- Errors are no longer inline with successful packets
- Use
iter_packets()which now yieldsResult<NetflowPacket, NetflowError> - Or use
parse_bytes()and check the.errorfield ofParseResult
iter_packets()now yieldsResult<NetflowPacket, NetflowError>instead ofNetflowPacket- Change from:
for packet in iter { match packet { NetflowPacket::Error(e) => ... } } - Change to:
for result in iter { match result { Ok(packet) => ..., Err(e) => ... } }
- Change from:
FlowSetBody::NoTemplatevariant changed fromVec<u8>toNoTemplateInfostruct- Provides template ID, available templates list, and raw data for debugging
- See README for detailed migration examples
New Features
- AutoScopedParser — RFC-compliant automatic template scoping
- V9:
(source_addr, source_id)per RFC 3954 - IPFIX:
(source_addr, observation_domain_id)per RFC 7011 - Prevents template collisions in multi-router deployments
- V9:
- RouterScopedParser — Generic multi-source parser with per-source template caches
- Template Cache Metrics — Performance tracking with atomic counters
- Accessible via
v9_cache_stats()andipfix_cache_stats() - Tracks hits, misses, evictions, collisions, expirations
- Accessible via
- Template Event Hooks — Callback system for monitoring template lifecycle
- Events: Learned, Collision, Evicted, Expired, MissingTemplate
Safety and Correctness
- Enhanced template validation with three layers of protection
- Field count limits (configurable, default 10,000)
- Total size limits (default u16::MAX, prevents memory exhaustion)
- Duplicate field detection (rejects malformed templates)
- Templates validated before caching; invalid templates rejected immediately
- Added public
is_valid()methods for IPFIX templates - Removed unsafe unwrap operations in field parsing
- Improved buffer boundary validation
Bug Fixes
- Fixed compilation error in
parse_bytes_as_netflow_common_flowsets() - Fixed unreachable pattern warning in
NetflowCommon::try_from() - Fixed
max_error_sample_sizeconfiguration inconsistency- Added
max_error_sample_sizefield toConfigstruct - Now properly propagates from builder to V9Parser and IPFixParser
- Previously, builder setting only affected main parser, not internal parsers
with_max_error_sample_size()now correctly updates all parser instances
- Added
Documentation
- New "Template Management Guide" in README covering multi-source deployments
- RFC compliance documentation (RFC 3954 for V9, RFC 7011 for IPFIX)
- New examples:
template_management_demo.rs,multi_source_comparison.rs,template_hooks.rs - Updated UDP listener examples to use AutoScopedParser/RouterScopedParser
- Added CI status, crates.io version, and docs.rs badges to README
0.7.4
Bug Fixes
- Fixed critical bug in protocol.rs
- Fixed
impl From<u8> for ProtocolTypesmapping that was off-by-one - Added missing case for
0→ProtocolTypes::Hopopt - Fixed case
1fromHopopttoIcmp(correct mapping) - Fixed case
144fromReservedtoAggfrag(correct mapping) - Added missing case for
255→ProtocolTypes::Reserved - No code changes required, just update dependency version
- Fixed
0.7.3
- Fixed several re-export issues in documentation
- Corrected static_versions module imports
- All types now properly accessible through documented paths
- Documentation builds successfully with correct type links
0.7.2
- Re-exports
lrucrate at crate root for easier access - Fixes broken doc links for LRU types in template cache documentation
0.7.1
- Added complete serde support for all public types
- Fixed missing Serialize/Deserialize derives on several structs
- All NetFlow packet types can now be serialized to JSON/other formats
- No breaking changes — purely additive
0.7.0
Breaking Changes
- Removed packet-based and combined TTL modes
- Only time-based TTL is now supported via
TtlConfig - Simplified TTL API reduces complexity and maintenance burden
- Migration: Replace
TtlMode::Packetswith time-basedTtlConfig(see README)
- Only time-based TTL is now supported via
0.6.0
New Features
- Template TTL (Time-to-Live) support
- Templates can now expire based on time or packet count
- Configurable per-parser via builder pattern
- New
TtlConfigandTtlModetypes - See README for usage examples