Skip to content

Commit 8e461cd

Browse files
committed
improve IPFIX decoding fidelity
1 parent f50106f commit 8e461cd

1 file changed

Lines changed: 83 additions & 22 deletions

File tree

src/networking/ipfix/wire.rs

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@ pub const SET_ID_OPTIONS_TEMPLATE: u16 = 3;
2525
pub const MIN_DATA_SET_ID: u16 = 256;
2626
pub const VARIABLE_LENGTH: u16 = 0xFFFF;
2727

28-
#[allow(dead_code)]
2928
pub 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).
240272
fn 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.
342401
fn 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.
387448
fn 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

Comments
 (0)