@@ -25,13 +25,12 @@ pub const SET_ID_OPTIONS_TEMPLATE: u16 = 3;
2525pub const MIN_DATA_SET_ID : u16 = 256 ;
2626pub const VARIABLE_LENGTH : u16 = 0xFFFF ;
2727
28- #[ allow( dead_code) ]
2928pub mod ie {
3029 //! IANA-assigned IPFIX Information Element identifiers used by Sniffnet.
3130 //!
32- //! Constants for flow-timestamp IEs are kept even though we don't decode
33- //! them today — they document which fields a future maintainer would wire
34- //! up if per-flow timestamps from the exporter become useful .
31+ //! Note the crossed naming in the IANA registry: the "post" counterpart of
32+ //! `sourceMacAddress` (56) is 81, while the one of `destinationMacAddress`
33+ //! (80) is 57 .
3534 pub const OCTET_DELTA_COUNT : u16 = 1 ;
3635 pub const PACKET_DELTA_COUNT : u16 = 2 ;
3736 pub const PROTOCOL_IDENTIFIER : u16 = 4 ;
@@ -42,8 +41,9 @@ pub mod ie {
4241 pub const SOURCE_IPV6_ADDRESS : u16 = 27 ;
4342 pub const DESTINATION_IPV6_ADDRESS : u16 = 28 ;
4443 pub const SOURCE_MAC_ADDRESS : u16 = 56 ;
44+ pub const POST_DESTINATION_MAC_ADDRESS : u16 = 57 ;
4545 pub const FLOW_DIRECTION : u16 = 61 ;
46- pub const POST_DESTINATION_MAC_ADDRESS : u16 = 80 ;
46+ pub const DESTINATION_MAC_ADDRESS : u16 = 80 ;
4747 pub const POST_SOURCE_MAC_ADDRESS : u16 = 81 ;
4848 pub const OCTET_TOTAL_COUNT : u16 = 85 ;
4949 pub const PACKET_TOTAL_COUNT : u16 = 86 ;
@@ -81,6 +81,8 @@ pub enum Set<'a> {
8181 Template ( Vec < TemplateRecord > ) ,
8282 /// Options templates are parsed but not interpreted; the collector skips them.
8383 OptionsTemplate ,
84+ /// Reserved or unrecognised set id — consumed and skipped.
85+ Ignored ,
8486 /// Data set: the payload is left as raw bytes and decoded against the
8587 /// referenced template by the collector layer.
8688 Data {
@@ -159,7 +161,7 @@ fn parse_set(input: &[u8]) -> IResult<&[u8], Set<'_>> {
159161 payload : body,
160162 } ,
161163 // reserved set ids 0, 1, and 4..=255 — skip silently
162- _ => Set :: OptionsTemplate ,
164+ _ => Set :: Ignored ,
163165 } ;
164166 Ok ( ( rest, set) )
165167}
@@ -217,6 +219,7 @@ pub fn decode_data_record<'a>(
217219 input : & ' a [ u8 ] ,
218220) -> IResult < & ' a [ u8 ] , FlowRecord > {
219221 let mut record = FlowRecord :: default ( ) ;
222+ let mut counters = CounterPriority :: default ( ) ;
220223 let mut remaining = input;
221224
222225 for spec in template {
@@ -229,12 +232,41 @@ pub fn decode_data_record<'a>(
229232 continue ;
230233 }
231234
232- apply_ie ( spec. ie_id , raw, & mut record) ;
235+ apply_ie ( spec. ie_id , raw, & mut record, & mut counters ) ;
233236 }
234237
235238 Ok ( ( remaining, record) )
236239}
237240
241+ /// Rank of the counter IE that supplied the value currently held in the record.
242+ ///
243+ /// A template may legitimately carry several octet counters at once (e.g.
244+ /// `octetDeltaCount` alongside `layer2OctetDeltaCount`). Ranking them means the
245+ /// outcome no longer depends on which one happens to appear last in the
246+ /// template.
247+ #[ derive( Default ) ]
248+ struct CounterPriority {
249+ bytes : u8 ,
250+ packets : u8 ,
251+ }
252+
253+ /// Higher wins. Layer-2 deltas match what the pcap pipeline counts — frame
254+ /// bytes including the link header — so they outrank IP-layer deltas.
255+ fn octet_rank ( ie_id : u16 ) -> u8 {
256+ match ie_id {
257+ ie:: LAYER2_OCTET_DELTA_COUNT => 2 ,
258+ ie:: OCTET_DELTA_COUNT => 1 ,
259+ _ => 0 ,
260+ }
261+ }
262+
263+ fn packet_rank ( ie_id : u16 ) -> u8 {
264+ match ie_id {
265+ ie:: PACKET_DELTA_COUNT => 1 ,
266+ _ => 0 ,
267+ }
268+ }
269+
238270/// Read the bytes belonging to a single field, accounting for the
239271/// variable-length encoding (RFC 7011 §7).
240272fn read_field_bytes ( input : & [ u8 ] , declared_length : u16 ) -> IResult < & [ u8 ] , & [ u8 ] > {
@@ -254,25 +286,34 @@ fn read_field_bytes(input: &[u8], declared_length: u16) -> IResult<&[u8], &[u8]>
254286 Ok ( ( input, bytes) )
255287}
256288
257- fn apply_ie ( ie_id : u16 , raw : & [ u8 ] , record : & mut FlowRecord ) {
289+ fn apply_ie ( ie_id : u16 , raw : & [ u8 ] , record : & mut FlowRecord , counters : & mut CounterPriority ) {
258290 match ie_id {
259- // TODO: a foreign exporter's template may carry several octet counters
260- // at once (e.g. octetDeltaCount + layer2OctetDeltaCount, or delta +
261- // total). Today they share one arm and overwrite `record.bytes`, so the
262- // value is whichever IE appears LAST in the template — order-dependent
263- // and arbitrary across exporters. Resolve by a deterministic priority
264- // instead (suggested: layer2 delta > IP delta > total). Same applies to
265- // the packet counters below.
266- ie:: OCTET_DELTA_COUNT | ie:: OCTET_TOTAL_COUNT | ie:: LAYER2_OCTET_DELTA_COUNT => {
267- if let Some ( v) = read_unsigned ( raw) {
291+ ie:: OCTET_DELTA_COUNT | ie:: LAYER2_OCTET_DELTA_COUNT => {
292+ let rank = octet_rank ( ie_id) ;
293+ if rank >= counters. bytes
294+ && let Some ( v) = read_unsigned ( raw)
295+ {
268296 record. bytes = v;
297+ counters. bytes = rank;
269298 }
270299 }
271- ie:: PACKET_DELTA_COUNT | ie:: PACKET_TOTAL_COUNT => {
272- if let Some ( v) = read_unsigned ( raw) {
300+ ie:: PACKET_DELTA_COUNT => {
301+ let rank = packet_rank ( ie_id) ;
302+ if rank >= counters. packets
303+ && let Some ( v) = read_unsigned ( raw)
304+ {
273305 record. packets = v;
306+ counters. packets = rank;
274307 }
275308 }
309+ // Deliberately not decoded: the total counters are cumulative for the
310+ // lifetime of the flow, whereas the collector adds every record's
311+ // counts onto a running tally. Feeding a total in would re-add the
312+ // whole flow each time the exporter reports it. Supporting exporters
313+ // that only send totals means differencing them against the previous
314+ // value per flow, which is state the decoder doesn't have.
315+ #[ allow( clippy:: match_same_arms) ]
316+ ie:: OCTET_TOTAL_COUNT | ie:: PACKET_TOTAL_COUNT => { }
276317 ie:: PROTOCOL_IDENTIFIER => {
277318 if let Some ( b) = raw. first ( ) {
278319 record. protocol = Some ( * b) ;
@@ -313,7 +354,7 @@ fn apply_ie(ie_id: u16, raw: &[u8], record: &mut FlowRecord) {
313354 record. src_mac = Some ( v) ;
314355 }
315356 }
316- ie:: POST_DESTINATION_MAC_ADDRESS => {
357+ ie:: DESTINATION_MAC_ADDRESS | ie :: POST_DESTINATION_MAC_ADDRESS => {
317358 if let Some ( v) = read_mac ( raw) {
318359 record. dst_mac = Some ( v) ;
319360 }
@@ -333,10 +374,28 @@ fn apply_ie(ie_id: u16, raw: &[u8], record: &mut FlowRecord) {
333374 ie:: FLOW_END_MILLISECONDS => {
334375 record. flow_end = read_timestamp_ms ( raw) ;
335376 }
377+ // Second-granularity timestamps only fill a slot the millisecond IEs
378+ // haven't, so the finer value wins whichever order the template lists
379+ // them in.
380+ ie:: FLOW_START_SECONDS if record. flow_start . is_none ( ) => {
381+ record. flow_start = read_timestamp_secs ( raw) ;
382+ }
383+ ie:: FLOW_END_SECONDS if record. flow_end . is_none ( ) => {
384+ record. flow_end = read_timestamp_secs ( raw) ;
385+ }
336386 _ => { }
337387 }
338388}
339389
390+ /// IPFIX `dateTimeSeconds` is 4 bytes big-endian, seconds since UNIX epoch.
391+ fn read_timestamp_secs ( raw : & [ u8 ] ) -> Option < Timestamp > {
392+ if raw. len ( ) != 4 {
393+ return None ;
394+ }
395+ let secs = u32:: from_be_bytes ( raw. try_into ( ) . ok ( ) ?) ;
396+ Some ( Timestamp :: new ( i64:: from ( secs) , 0 ) )
397+ }
398+
340399/// IPFIX `dateTimeMilliseconds` is 8 bytes big-endian, ms since UNIX epoch.
341400/// Converted to Sniffnet's `Timestamp(secs, usecs)` representation.
342401fn read_timestamp_ms ( raw : & [ u8 ] ) -> Option < Timestamp > {
@@ -383,9 +442,11 @@ fn read_ipv6(raw: &[u8]) -> Option<IpAddr> {
383442 Some ( IpAddr :: V6 ( Ipv6Addr :: from ( octets) ) )
384443}
385444
386- // TODO: all zeros should be trated as None??
445+ /// An all-zero MAC is how exporters spell "not observed" — `sniffnet-agent`
446+ /// writes it whenever a flow has no link header — so it decodes to `None`
447+ /// rather than being shown as a genuine `00:00:00:00:00:00` address.
387448fn read_mac ( raw : & [ u8 ] ) -> Option < [ u8 ; 6 ] > {
388- if raw. len ( ) != 6 {
449+ if raw. len ( ) != 6 || raw . iter ( ) . all ( |b| * b == 0 ) {
389450 return None ;
390451 }
391452 let mut mac = [ 0u8 ; 6 ] ;
0 commit comments