Skip to content

Commit b17732a

Browse files
committed
add IPFIX decoding and collector tests
1 parent 7465b65 commit b17732a

3 files changed

Lines changed: 342 additions & 0 deletions

File tree

src/networking/ipfix/collect.rs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,198 @@ fn exporter_as_addresses(peer: IpAddr) -> Vec<Address> {
283283
dst_addr: None,
284284
}]
285285
}
286+
287+
#[cfg(test)]
288+
mod tests {
289+
use super::*;
290+
use crate::networking::types::data_representation::DataRepr;
291+
use crate::networking::types::traffic_direction::TrafficDirection;
292+
use std::net::{Ipv4Addr, Ipv6Addr};
293+
294+
/// The field specifiers `sniffnet-agent` puts in its templates, after the
295+
/// two address fields that differ between the IPv4 and IPv6 variants.
296+
const AGENT_COMMON_FIELDS: [(u16, u16); 10] = [
297+
(7, 2),
298+
(11, 2),
299+
(4, 1),
300+
(56, 6),
301+
(80, 6),
302+
(61, 1),
303+
(352, 8),
304+
(2, 8),
305+
(152, 8),
306+
(153, 8),
307+
];
308+
309+
fn agent_fields(addr_fields: [(u16, u16); 2]) -> Vec<(u16, u16)> {
310+
addr_fields.into_iter().chain(AGENT_COMMON_FIELDS).collect()
311+
}
312+
313+
fn set(set_id: u16, body: &[u8]) -> Vec<u8> {
314+
let mut out = set_id.to_be_bytes().to_vec();
315+
out.extend_from_slice(&u16::try_from(body.len() + 4).unwrap().to_be_bytes());
316+
out.extend_from_slice(body);
317+
out
318+
}
319+
320+
fn template_set(template_id: u16, fields: &[(u16, u16)]) -> Vec<u8> {
321+
let mut body = template_id.to_be_bytes().to_vec();
322+
body.extend_from_slice(&u16::try_from(fields.len()).unwrap().to_be_bytes());
323+
for (ie, len) in fields {
324+
body.extend_from_slice(&ie.to_be_bytes());
325+
body.extend_from_slice(&len.to_be_bytes());
326+
}
327+
set(wire::SET_ID_TEMPLATE, &body)
328+
}
329+
330+
/// Message header plus the given sets, with the length backfilled.
331+
fn datagram(sets: &[Vec<u8>]) -> Vec<u8> {
332+
let mut out = IPFIX_VERSION.to_be_bytes().to_vec();
333+
out.extend_from_slice(&[0, 0]); // length placeholder
334+
out.extend_from_slice(&[0; 4]); // export time
335+
out.extend_from_slice(&[0; 4]); // sequence number
336+
out.extend_from_slice(&[0; 4]); // observation domain
337+
for s in sets {
338+
out.extend_from_slice(s);
339+
}
340+
let len = u16::try_from(out.len()).unwrap().to_be_bytes();
341+
out[2] = len[0];
342+
out[3] = len[1];
343+
out
344+
}
345+
346+
/// Record tail shared by both address families, in the agent's field order.
347+
fn record_tail(bytes: u64, packets: u64) -> Vec<u8> {
348+
let mut r = Vec::new();
349+
r.extend_from_slice(&443u16.to_be_bytes()); // source port
350+
r.extend_from_slice(&50_000u16.to_be_bytes()); // destination port
351+
r.push(6); // TCP
352+
r.extend_from_slice(&[0xAA; 6]); // source MAC
353+
r.extend_from_slice(&[0; 6]); // destination MAC: not observed
354+
r.push(0x00); // flowDirection: ingress
355+
r.extend_from_slice(&bytes.to_be_bytes());
356+
r.extend_from_slice(&packets.to_be_bytes());
357+
r.extend_from_slice(&20_000u64.to_be_bytes()); // flow start: 20s
358+
r.extend_from_slice(&25_000u64.to_be_bytes()); // flow end: 25s
359+
r
360+
}
361+
362+
fn run(bytes: &[u8]) -> (InfoTraffic, AddressesResolutionState) {
363+
let mut templates = TemplateCache::new();
364+
let mut info = InfoTraffic::default();
365+
let mut resolutions = AddressesResolutionState::new_detached();
366+
process_datagram(
367+
bytes,
368+
"203.0.113.9:4739".parse().unwrap(),
369+
&mut templates,
370+
&mut info,
371+
&IpBlacklist::default(),
372+
&mut resolutions,
373+
);
374+
(info, resolutions)
375+
}
376+
377+
/// A template set plus a one-record data set, shaped exactly as the agent
378+
/// emits them.
379+
fn agent_datagram(
380+
template_id: u16,
381+
addr_fields: [(u16, u16); 2],
382+
addrs: &[u8],
383+
bytes: u64,
384+
packets: u64,
385+
) -> Vec<u8> {
386+
let mut record = addrs.to_vec();
387+
record.extend_from_slice(&record_tail(bytes, packets));
388+
datagram(&[
389+
template_set(template_id, &agent_fields(addr_fields)),
390+
set(template_id, &record),
391+
])
392+
}
393+
394+
#[test]
395+
fn decodes_an_agent_shaped_ipv4_datagram() {
396+
let mut addrs = Ipv4Addr::new(10, 0, 0, 1).octets().to_vec();
397+
addrs.extend_from_slice(&Ipv4Addr::new(8, 8, 8, 8).octets());
398+
let (info, resolutions) = run(&agent_datagram(256, [(8, 4), (12, 4)], &addrs, 1500, 10));
399+
400+
let key = AddressPortPair {
401+
source: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
402+
sport: Some(443),
403+
dest: IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)),
404+
dport: Some(50_000),
405+
protocol: Protocol::TCP,
406+
};
407+
let entry = info.map.get(&key).expect("flow present");
408+
assert_eq!(entry.transmitted_bytes, 1500);
409+
assert_eq!(entry.transmitted_packets, 10);
410+
// flowDirection 0x00 is ingress, and it overrides any address guess
411+
assert_eq!(entry.traffic_direction, TrafficDirection::Incoming);
412+
assert_eq!(entry.mac_address1, Some("aa:aa:aa:aa:aa:aa".to_string()));
413+
assert_eq!(entry.mac_address2, None, "all-zero MAC means not observed");
414+
assert_eq!(entry.initial_timestamp, Timestamp::new(20, 0));
415+
assert_eq!(entry.final_timestamp, Timestamp::new(25, 0));
416+
417+
assert_eq!(info.tot_data_info.tot_data(DataRepr::Bytes), 1500);
418+
assert_eq!(info.tot_data_info.tot_data(DataRepr::Packets), 10);
419+
assert_eq!(info.services.len(), 1);
420+
// no rDNS threads are running, so the address is left awaiting lookup
421+
assert_eq!(resolutions.addresses_waiting_resolution.len(), 1);
422+
assert!(info.hosts.is_empty());
423+
}
424+
425+
#[test]
426+
fn decodes_an_agent_shaped_ipv6_datagram() {
427+
let src = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
428+
let dst = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 2);
429+
let mut addrs = src.octets().to_vec();
430+
addrs.extend_from_slice(&dst.octets());
431+
let (info, _) = run(&agent_datagram(257, [(27, 16), (28, 16)], &addrs, 800, 4));
432+
433+
let key = AddressPortPair {
434+
source: IpAddr::V6(src),
435+
sport: Some(443),
436+
dest: IpAddr::V6(dst),
437+
dport: Some(50_000),
438+
protocol: Protocol::TCP,
439+
};
440+
let entry = info.map.get(&key).expect("flow present");
441+
assert_eq!(entry.transmitted_bytes, 800);
442+
assert_eq!(entry.transmitted_packets, 4);
443+
}
444+
445+
#[test]
446+
fn record_without_counters_is_skipped() {
447+
let mut addrs = Ipv4Addr::new(10, 0, 0, 1).octets().to_vec();
448+
addrs.extend_from_slice(&Ipv4Addr::new(8, 8, 8, 8).octets());
449+
let (info, _) = run(&agent_datagram(256, [(8, 4), (12, 4)], &addrs, 0, 0));
450+
451+
assert!(info.map.is_empty(), "no traffic to account for");
452+
assert_eq!(info.tot_data_info.tot_data(DataRepr::Packets), 0);
453+
}
454+
455+
#[test]
456+
fn data_set_without_a_known_template_is_skipped() {
457+
// Data referencing template 256 before any template set has arrived.
458+
let bytes = datagram(&[set(256, &[0xAA; 58])]);
459+
let (info, _) = run(&bytes);
460+
assert!(info.map.is_empty());
461+
}
462+
463+
#[test]
464+
fn trailing_padding_does_not_produce_an_extra_record() {
465+
let mut addrs = Ipv4Addr::new(10, 0, 0, 1).octets().to_vec();
466+
addrs.extend_from_slice(&Ipv4Addr::new(8, 8, 8, 8).octets());
467+
let mut record = addrs;
468+
record.extend_from_slice(&record_tail(1500, 10));
469+
record.extend_from_slice(&[0; 3]); // pad to a 4-byte boundary
470+
471+
let bytes = datagram(&[
472+
template_set(256, &agent_fields([(8, 4), (12, 4)])),
473+
set(256, &record),
474+
]);
475+
let (info, _) = run(&bytes);
476+
477+
assert_eq!(info.map.len(), 1);
478+
assert_eq!(info.tot_data_info.tot_data(DataRepr::Packets), 10);
479+
}
480+
}

src/networking/ipfix/wire.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,4 +612,143 @@ mod tests {
612612
let (_, set) = parse_set(&bytes).unwrap();
613613
assert_eq!(set, Set::OptionsTemplate);
614614
}
615+
616+
/// Decode `payload` against a template built from `(ie, length)` pairs.
617+
fn decode(fields: &[(u16, u16)], payload: &[u8]) -> FlowRecord {
618+
let template: Vec<FieldSpec> = fields
619+
.iter()
620+
.map(|(ie_id, length)| FieldSpec {
621+
ie_id: *ie_id,
622+
length: *length,
623+
enterprise: None,
624+
})
625+
.collect();
626+
decode_data_record(&template, payload).expect("decode").1
627+
}
628+
629+
#[test]
630+
fn layer2_octets_win_over_ip_octets_in_either_order() {
631+
// 1500 as layer2OctetDeltaCount, 1000 as octetDeltaCount
632+
let l2 = [0, 0, 0, 0, 0, 0, 0x05, 0xDC];
633+
let ip = [0, 0, 0, 0, 0, 0, 0x03, 0xE8];
634+
635+
let mut l2_first = Vec::new();
636+
l2_first.extend_from_slice(&l2);
637+
l2_first.extend_from_slice(&ip);
638+
assert_eq!(
639+
decode(
640+
&[
641+
(ie::LAYER2_OCTET_DELTA_COUNT, 8),
642+
(ie::OCTET_DELTA_COUNT, 8)
643+
],
644+
&l2_first,
645+
)
646+
.bytes,
647+
1500,
648+
);
649+
650+
let mut ip_first = Vec::new();
651+
ip_first.extend_from_slice(&ip);
652+
ip_first.extend_from_slice(&l2);
653+
assert_eq!(
654+
decode(
655+
&[
656+
(ie::OCTET_DELTA_COUNT, 8),
657+
(ie::LAYER2_OCTET_DELTA_COUNT, 8)
658+
],
659+
&ip_first,
660+
)
661+
.bytes,
662+
1500,
663+
);
664+
}
665+
666+
#[test]
667+
fn cumulative_totals_are_not_read_as_deltas() {
668+
// The collector adds each record's counts onto a running tally, so a
669+
// total would be re-added in full every time the exporter reports it.
670+
let payload = [0, 0, 0, 0, 0, 0, 0x05, 0xDC, 0, 0, 0, 0, 0, 0, 0, 10];
671+
let record = decode(
672+
&[(ie::OCTET_TOTAL_COUNT, 8), (ie::PACKET_TOTAL_COUNT, 8)],
673+
&payload,
674+
);
675+
assert_eq!(record.bytes, 0);
676+
assert_eq!(record.packets, 0);
677+
}
678+
679+
#[test]
680+
fn milliseconds_win_over_seconds_in_either_order() {
681+
let secs = [0x00, 0x00, 0x00, 0x0A]; // 10s
682+
let millis = [0, 0, 0, 0, 0, 0, 0x4E, 0x20]; // 20_000ms == 20s
683+
let expected = Timestamp::new(20, 0);
684+
685+
let mut secs_first = Vec::new();
686+
secs_first.extend_from_slice(&secs);
687+
secs_first.extend_from_slice(&millis);
688+
assert_eq!(
689+
decode(
690+
&[
691+
(ie::FLOW_START_SECONDS, 4),
692+
(ie::FLOW_START_MILLISECONDS, 8)
693+
],
694+
&secs_first,
695+
)
696+
.flow_start,
697+
Some(expected),
698+
);
699+
700+
let mut millis_first = Vec::new();
701+
millis_first.extend_from_slice(&millis);
702+
millis_first.extend_from_slice(&secs);
703+
assert_eq!(
704+
decode(
705+
&[
706+
(ie::FLOW_START_MILLISECONDS, 8),
707+
(ie::FLOW_START_SECONDS, 4)
708+
],
709+
&millis_first,
710+
)
711+
.flow_start,
712+
Some(expected),
713+
);
714+
}
715+
716+
#[test]
717+
fn second_granularity_timestamps_decode_on_their_own() {
718+
let payload = [0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x14];
719+
let record = decode(
720+
&[(ie::FLOW_START_SECONDS, 4), (ie::FLOW_END_SECONDS, 4)],
721+
&payload,
722+
);
723+
assert_eq!(record.flow_start, Some(Timestamp::new(10, 0)));
724+
assert_eq!(record.flow_end, Some(Timestamp::new(20, 0)));
725+
}
726+
727+
#[test]
728+
fn all_zero_mac_decodes_to_none() {
729+
// `sniffnet-agent` writes all-zero when a flow carries no link header.
730+
let payload = [0, 0, 0, 0, 0, 0, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF];
731+
let record = decode(
732+
&[
733+
(ie::SOURCE_MAC_ADDRESS, 6),
734+
(ie::DESTINATION_MAC_ADDRESS, 6),
735+
],
736+
&payload,
737+
);
738+
assert_eq!(record.src_mac, None);
739+
assert_eq!(record.dst_mac, Some([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]));
740+
}
741+
742+
#[test]
743+
fn flow_direction_maps_ingress_egress_and_undefined() {
744+
let cases = [
745+
(0x00, Some(TrafficDirection::Incoming)),
746+
(0x01, Some(TrafficDirection::Outgoing)),
747+
(0xFF, None),
748+
];
749+
for (raw, expected) in cases {
750+
let record = decode(&[(ie::FLOW_DIRECTION, 1)], &[raw]);
751+
assert_eq!(record.direction, expected, "flowDirection {raw:#04x}");
752+
}
753+
}
615754
}

src/networking/parse_packets.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,14 @@ impl AddressesResolutionState {
349349
}
350350
}
351351

352+
/// Resolution state with no lookup threads behind it: requests queue up on
353+
/// the channel and are never served, which is what tests of the accounting
354+
/// path want.
355+
#[cfg(test)]
356+
pub(crate) fn new_detached() -> Self {
357+
Self::new(async_channel::unbounded().0, std::sync::mpsc::channel().1)
358+
}
359+
352360
pub(crate) fn new_hosts_to_send(&mut self) -> Vec<HostMessage> {
353361
let mut new_hosts = Vec::new();
354362
while let Ok(mut host_msg) = self.lookup_result_rx.try_recv() {

0 commit comments

Comments
 (0)