#![cfg(all(feature = "pcap", feature = "test-helpers"))]
use std::borrow::Cow;
use std::io::Cursor;
use std::time::Duration;
use flowscope::extract::parse::test_frames::ipv4_udp;
use flowscope::pcap::{CaptureFormat, CaptureReader, PcapFlowSource};
use flowscope::{FlowSide, FlowState, Timestamp};
use pcap_file::DataLink;
use pcap_file::pcapng::PcapNgWriter;
use pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketBlock;
use pcap_file::pcapng::blocks::interface_description::{
InterfaceDescriptionBlock, InterfaceDescriptionOption,
};
fn pcapng(tsresol: Option<u8>, raw_ticks: u64, frame: &[u8]) -> Vec<u8> {
let mut w = PcapNgWriter::new(Vec::new()).unwrap();
let options = tsresol
.map(|v| vec![InterfaceDescriptionOption::IfTsResol(v)])
.unwrap_or_default();
w.write_pcapng_block(InterfaceDescriptionBlock {
linktype: DataLink::ETHERNET,
snaplen: 65535,
options,
})
.unwrap();
w.write_pcapng_block(EnhancedPacketBlock {
interface_id: 0,
timestamp: Duration::from_nanos(raw_ticks),
original_len: frame.len() as u32,
data: Cow::Borrowed(frame),
options: vec![],
})
.unwrap();
w.into_inner()
}
#[test]
fn pcapng_default_resolution_is_microseconds() {
let frame = ipv4_udp([10, 0, 0, 1], [10, 0, 0, 2], 1, 53, b"q");
let bytes = pcapng(None, 1_700_000_000_250_000, &frame);
let mut r = CaptureReader::new(Cursor::new(bytes)).unwrap();
assert_eq!(r.format(), CaptureFormat::PcapNg);
let p = r.next_packet().unwrap().unwrap();
assert_eq!(p.timestamp, Duration::new(1_700_000_000, 250_000_000));
assert_eq!(p.data, frame);
assert_eq!(p.datalink, DataLink::ETHERNET);
assert!(r.next_packet().is_none());
}
#[test]
fn pcapng_nanosecond_and_binary_resolutions() {
let frame = ipv4_udp([10, 0, 0, 1], [10, 0, 0, 2], 1, 53, b"q");
let ns = pcapng(Some(9), 1_700_000_000_000_000_123, &frame);
let p = CaptureReader::new(Cursor::new(ns))
.unwrap()
.next_packet()
.unwrap()
.unwrap();
assert_eq!(p.timestamp, Duration::new(1_700_000_000, 123));
let bin = pcapng(Some(0x80 | 10), 3 * 1024 + 512, &frame);
let p = CaptureReader::new(Cursor::new(bin))
.unwrap()
.next_packet()
.unwrap()
.unwrap();
assert_eq!(p.timestamp, Duration::from_millis(3_500));
}
#[test]
fn pcap_flow_source_replays_pcapng() {
let frame = ipv4_udp([10, 0, 0, 1], [10, 0, 0, 2], 1, 53, b"q");
let bytes = pcapng(Some(6), 2_000_000, &frame);
let source = PcapFlowSource::from_reader(Cursor::new(bytes)).unwrap();
assert_eq!(source.format(), CaptureFormat::PcapNg);
let views: Vec<_> = source.views().collect::<Result<_, _>>().unwrap();
assert_eq!(views.len(), 1);
assert_eq!(views[0].timestamp, Timestamp::new(2, 0));
}
#[test]
fn flow_side_and_state_have_stable_labels() {
assert_eq!(FlowSide::Initiator.as_str(), "initiator");
assert_eq!(FlowSide::Responder.to_string(), "responder");
assert_eq!(FlowSide::Initiator.opposite(), FlowSide::Responder);
assert_eq!(FlowState::Established.as_str(), "established");
assert_eq!(FlowState::SynSent.to_string(), "syn_sent");
assert_eq!(FlowState::ClosingTcp.as_str(), "closing_tcp");
}
fn pcap_with(datalink: DataLink, records: &[Vec<u8>]) -> Vec<u8> {
use pcap_file::pcap::{PcapHeader, PcapPacket, PcapWriter};
let header = PcapHeader {
datalink,
..Default::default()
};
let mut w = PcapWriter::with_header(Vec::new(), header).unwrap();
for (i, r) in records.iter().enumerate() {
w.write_packet(&PcapPacket::new(
Duration::from_secs(1 + i as u64),
r.len() as u32,
r,
))
.unwrap();
}
w.into_writer()
}
fn ip_of(frame: &[u8]) -> Vec<u8> {
frame[14..].to_vec()
}
fn flows_of(bytes: Vec<u8>) -> usize {
use flowscope::FlowEvent;
use flowscope::extract::FiveTuple;
PcapFlowSource::from_reader(Cursor::new(bytes))
.unwrap()
.with_extractor(FiveTuple::bidirectional())
.filter(|e| matches!(e, Ok(FlowEvent::Started { .. })))
.count()
}
#[test]
fn linux_cooked_capture_is_tracked() {
let frame = ipv4_udp([10, 0, 0, 1], [10, 0, 0, 2], 1, 53, b"q");
let mut sll = vec![0, 4, 0, 1, 0, 6, 2, 2, 2, 2, 2, 2, 0, 0, 0x08, 0x00];
sll.extend_from_slice(&ip_of(&frame));
let bytes = pcap_with(DataLink::LINUX_SLL, &[sll]);
let p = CaptureReader::new(Cursor::new(bytes.clone()))
.unwrap()
.next_packet()
.unwrap()
.unwrap();
assert_eq!(
p.direction,
Some(flowscope::pcap::CaptureDirection::Outbound)
);
assert_eq!(
flows_of(bytes),
1,
"tcpdump -i any captures are no longer unmatched"
);
}
#[test]
fn raw_ip_capture_is_tracked() {
let frame = ipv4_udp([10, 0, 0, 1], [10, 0, 0, 2], 1, 53, b"q");
assert_eq!(flows_of(pcap_with(DataLink::RAW, &[ip_of(&frame)])), 1);
}
#[test]
fn unsupported_link_types_are_skipped_and_counted() {
let bytes = pcap_with(DataLink::IEEE802_11, &[vec![0u8; 40]]);
let mut views = PcapFlowSource::from_reader(Cursor::new(bytes))
.unwrap()
.views();
assert!(views.next().is_none());
assert_eq!(views.unsupported(), 1);
}
#[test]
fn pcapng_epb_flags_give_the_direction() {
use pcap_file::pcapng::blocks::enhanced_packet::EnhancedPacketOption;
let frame = ipv4_udp([10, 0, 0, 1], [10, 0, 0, 2], 1, 53, b"q");
let mut w = PcapNgWriter::new(Vec::new()).unwrap();
w.write_pcapng_block(InterfaceDescriptionBlock {
linktype: DataLink::ETHERNET,
snaplen: 65535,
options: vec![],
})
.unwrap();
for flags in [1u32, 2u32] {
w.write_pcapng_block(EnhancedPacketBlock {
interface_id: 0,
timestamp: Duration::from_nanos(1),
original_len: frame.len() as u32,
data: Cow::Borrowed(&frame),
options: vec![EnhancedPacketOption::Flags(flags)],
})
.unwrap();
}
let dirs: Vec<_> = CaptureReader::new(Cursor::new(w.into_inner()))
.unwrap()
.map(|p| p.unwrap().direction)
.collect();
use flowscope::pcap::CaptureDirection::{Inbound, Outbound};
assert_eq!(dirs, vec![Some(Inbound), Some(Outbound)]);
}