diff --git a/Cargo.lock b/Cargo.lock index a1a36d3..c56ad1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,12 @@ dependencies = [ "half", ] +[[package]] +name = "circular" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea" + [[package]] name = "clang-sys" version = "1.8.1" @@ -769,6 +775,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pcap-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8d57cc6bdf76d7abd6d3cc1113278047dab29c2ff6d97190e8d1c29d4efdac" +dependencies = [ + "circular", + "nom", + "rusticata-macros", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -1033,6 +1050,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1410,6 +1436,7 @@ dependencies = [ name = "spar-trace-topology" version = "0.9.3" dependencies = [ + "pcap-parser", "serde", "serde_json", "spar-base-db", diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index 675166a..e18e69d 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -1863,4 +1863,20 @@ artifacts: status: implemented tags: [trace-topology, ingest, lldp, v0100] + - id: REQ-TRACE-TOPOLOGY-003 + type: requirement + title: PCAPNG FrameSource for L2 frame ingest + description: > + System shall provide a PCAPNG-backed FrameSource implementation + in spar-trace-topology that, given a `.pcapng` file recorded + with tcpdump / tshark / Wireshark from a TAP/SPAN port, yields + a stream of CapturedFrame records carrying mac_src, mac_dst, + optional 802.1Q VLAN-ID and PCP, and a Unix-epoch ns timestamp + derived from the per-IDB ts_resol option. Built atop the + `pcap-parser` crate. Errors carry concrete kinds (Truncated, + UnsupportedLinkType, MalformedPcapng, Io). Per the v0.10.0 + trace-topology design's §"Implementation phasing" PCAPNG entry. + status: implemented + tags: [trace-topology, ingest, pcapng, v0100] + # Research findings tracked separately in research/findings.yaml diff --git a/artifacts/verification.yaml b/artifacts/verification.yaml index 96ba763..136b304 100644 --- a/artifacts/verification.yaml +++ b/artifacts/verification.yaml @@ -2429,3 +2429,23 @@ artifacts: links: - type: satisfies target: REQ-TRACE-TOPOLOGY-004 + + - id: TEST-TRACE-TOPOLOGY-PCAPNG + type: feature + title: PcapngFrameSource yields correct typed CapturedFrames + description: > + Tests in crates/spar-trace-topology/src/ingest.rs cover an + untagged Ethernet frame roundtrip, an 802.1Q-tagged frame with + VLAN-ID and PCP extraction, a truncated frame producing + IngestError::Truncated, an unsupported link type producing + IngestError::UnsupportedLinkType, and per-IDB ts_resol + handling for both µs and ns resolutions. + fields: + method: automated-test + steps: + - run: cargo test -p spar-trace-topology --lib -- ingest::tests + status: passing + tags: [trace-topology, ingest, pcapng, v0100] + links: + - type: satisfies + target: REQ-TRACE-TOPOLOGY-003 diff --git a/crates/spar-trace-topology/Cargo.toml b/crates/spar-trace-topology/Cargo.toml index b478f23..166b1cd 100644 --- a/crates/spar-trace-topology/Cargo.toml +++ b/crates/spar-trace-topology/Cargo.toml @@ -12,6 +12,7 @@ spar-base-db.workspace = true spar-syntax.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +pcap-parser = "0.16" [dev-dependencies] tempfile = "3" diff --git a/crates/spar-trace-topology/src/ingest.rs b/crates/spar-trace-topology/src/ingest.rs index bd0a9be..418c0b8 100644 --- a/crates/spar-trace-topology/src/ingest.rs +++ b/crates/spar-trace-topology/src/ingest.rs @@ -1,32 +1,51 @@ -//! Runtime-artefact parser surfaces. +//! Runtime-artefact parsers feeding the v0.11.0 reconciliation engine. //! -//! v0.10.0 shipped placeholder traits only. v0.10.x sibling commits -//! land the real parsers — PCAPNG (FrameSource), LLDP -//! (TopologySource), Qcc YANG (SwitchConfigSource), gPTP -//! (PtpTimeSource). +//! v0.10.x lands these incrementally: //! -//! v0.10.x B-3 (this commit): real LLDP JSON `TopologySource` -//! implementation backed by `lldpctl -f json` output (see -//! ). The other three traits remain -//! placeholders and are filled in by sibling commits. +//! - **PCAPNG** ([`PcapngFrameSource`]) — implemented in v0.10.x B-2. +//! Uses Pierre Chifflier's `pcap-parser` crate; yields typed +//! [`CapturedFrame`] records carrying L2 identity (mac_src, mac_dst, +//! optional 802.1Q VLAN-ID and PCP) plus a Unix-epoch nanosecond +//! timestamp resolved via the per-IDB `ts_resol` option. +//! - **LLDP** ([`LldpJsonTopologySource`]) — implemented in v0.10.x B-3. +//! Backed by `lldpctl -f json` output (see ); +//! yields [`LldpNeighbor`] records carrying local_port + typed +//! remote chassis-id / port-id / system-name. +//! - **Qcc YANG** ([`SwitchConfigSource`]) — placeholder, sibling commit. +//! - **gPTP** ([`PtpTimeSource`]) — placeholder, sibling commit. //! //! See `docs/designs/v0.10.0-trace-topology.md` §"Implementation //! phasing" for the per-source roadmap. use std::path::Path; +use pcap_parser::traits::PcapReaderIterator; +use pcap_parser::{Block, Linktype, PcapBlockOwned, PcapError, PcapNGReader}; + +/// One captured L2 frame, distilled to the fields the v0.11.0 +/// reconciler consumes. Higher-layer headers are ignored — this is +/// strictly L2 identity + timestamp. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CapturedFrame { + /// Source MAC address (Ethernet bytes 6..12). + pub mac_src: [u8; 6], + /// Destination MAC address (Ethernet bytes 0..6). + pub mac_dst: [u8; 6], + /// 802.1Q VLAN-ID (0..=4094) if the frame carried a 0x8100 tag. + pub vlan_id: Option, + /// 802.1Q PCP (0..=7) if the frame carried a 0x8100 tag. + pub pcp: Option, + /// Capture timestamp, normalised to nanoseconds since the + /// Unix epoch using the IDB `ts_resol` option (defaults to 1µs + /// per pcapng spec). + pub timestamp_ns: u64, +} + /// Source of L2 frames captured at runtime — typically a PCAPNG file /// recorded with `tcpdump`, `tshark`, or a TAP/SPAN port. -/// -/// TODO(v0.10.0+): real parser — PCAPNG (RFC pcapng-draft / IETF -/// opsawg-pcapng). See design doc §"Input artefact set" for the full -/// list of supported link types and capture-options. pub trait FrameSource { - /// Open the frame source at `path`. v0.10.0 placeholder — the - /// real parser returns an iterator of typed frames. - fn open(path: &Path) -> Result - where - Self: Sized; + /// Iterate captured frames in capture order. + fn frames(&mut self) -> Box> + '_>; } /// Source of LLDP topology snapshots — neighbor adjacency observed at @@ -279,32 +298,45 @@ fn type_name(v: &serde_json::Value) -> &'static str { /// v0.10.0 shipped only `Unimplemented`; v0.10.x parsers extend this /// enum additively with concrete I/O / format-decode kinds. The /// `Unimplemented` variant is preserved for the placeholder trait -/// `open()` calls that haven't been replaced yet. +/// `open()` calls that haven't been replaced yet (Qcc YANG, gPTP). #[derive(Debug)] pub enum IngestError { + /// Underlying I/O error opening or reading the artefact file. + Io(std::io::Error), + /// Captured frame is shorter than a full L2 header (or shorter + /// than the 16 bytes required when an 802.1Q tag is present). + Truncated, + /// pcap-parser reported a malformed pcapng block / record. + MalformedPcapng(String), + /// pcapng link type other than Ethernet (LINKTYPE_ETHERNET = 1). + UnsupportedLinkType(i32), + /// LLDP JSON dump did not match the `lldpctl -f json` schema. + MalformedLldpJson(String), /// The requested parser surface is not implemented in this /// build of spar-trace-topology. v0.10.0 returned this from /// every `open` call; v0.10.x parsers replace it with concrete /// kinds as they land. Unimplemented, - /// Underlying I/O error opening the artefact file. - Io(std::io::Error), - /// LLDP JSON dump did not match the `lldpctl -f json` schema. - MalformedLldpJson(String), } impl core::fmt::Display for IngestError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::Unimplemented => write!( + Self::Io(e) => write!(f, "I/O error: {e}"), + Self::Truncated => write!(f, "captured frame is shorter than a full L2 header"), + Self::MalformedPcapng(msg) => write!(f, "malformed pcapng: {msg}"), + Self::UnsupportedLinkType(lt) => write!( f, - "parser not implemented in v0.10.0 foundation; see \ - docs/designs/v0.10.0-trace-topology.md" + "unsupported pcapng link type {lt}; only Ethernet (1) is supported" ), - Self::Io(e) => write!(f, "I/O error: {e}"), Self::MalformedLldpJson(msg) => { write!(f, "malformed lldpctl JSON: {msg}") } + Self::Unimplemented => write!( + f, + "parser not implemented in v0.10.0 foundation; see \ + docs/designs/v0.10.0-trace-topology.md" + ), } } } @@ -318,6 +350,245 @@ impl std::error::Error for IngestError { } } +impl From for IngestError { + fn from(e: std::io::Error) -> Self { + Self::Io(e) + } +} + +/// PCAPNG-backed [`FrameSource`] using the `pcap-parser` crate. +/// +/// Reads the entire `.pcapng` into memory at `open()` time — pcapng +/// captures from real deployments are bounded artefacts (typically +/// tens to hundreds of MB), not pipes, so the simpler in-memory parse +/// avoids the streaming-`consume()` lifetime gymnastics that +/// `PcapNGReader` would otherwise require us to fight. +#[derive(Debug)] +pub struct PcapngFrameSource { + /// Raw pcapng bytes — owned so iteration can hold borrows + /// without juggling lifetimes against the source struct. + bytes: Vec, + /// Pre-validated link type from the first IDB. The current + /// implementation only accepts captures whose first IDB declares + /// LINKTYPE_ETHERNET; multi-IDB or per-frame link-type variation + /// is out-of-scope for v0.10.x. + linktype: Linktype, +} + +impl PcapngFrameSource { + /// Open a pcapng file and validate the first Interface Description + /// Block declares `LINKTYPE_ETHERNET`. + pub fn open(path: &Path) -> Result { + let bytes = std::fs::read(path)?; + let linktype = first_idb_linktype(&bytes)?; + if linktype != Linktype::ETHERNET { + return Err(IngestError::UnsupportedLinkType(linktype.0)); + } + Ok(Self { bytes, linktype }) + } +} + +impl FrameSource for PcapngFrameSource { + fn frames(&mut self) -> Box> + '_> { + Box::new(PcapngFrameIter::new(&self.bytes, self.linktype)) + } +} + +/// Iterator over captured frames in a pcapng buffer. +struct PcapngFrameIter<'a> { + reader: Option>, + /// IDB ts_resolution (fractions-of-a-second; 1_000_000 = µs). + ts_resolution: u64, + /// Set true once we surface a fatal stream-level error so we + /// don't keep retrying the underlying parser. + done: bool, + /// Total input length — `PcapNGReader::reader_exhausted()` only + /// flips when the backing reader yields zero bytes on a refill, + /// which never happens for a `&[u8]` source that was fully + /// preloaded. Comparing against `position()` lets us distinguish + /// a truly-truncated tail block from clean EOF. + total_len: usize, + /// Pre-validated link type from `open()`. We ignore mid-stream + /// IDB changes for v0.10.x. + _linktype: Linktype, +} + +impl<'a> PcapngFrameIter<'a> { + fn new(bytes: &'a [u8], linktype: Linktype) -> Self { + let reader = PcapNGReader::new(bytes.len().max(65536), bytes).ok(); + let done = reader.is_none(); + Self { + reader, + ts_resolution: DEFAULT_TS_RESOLUTION, + done, + total_len: bytes.len(), + _linktype: linktype, + } + } +} + +/// Default IDB ts_resolution per pcapng spec §4.2 (`if_tsresol = 6`, +/// i.e. 10^-6 seconds = µs). Means 1_000_000 ticks per second. +const DEFAULT_TS_RESOLUTION: u64 = 1_000_000; + +impl Iterator for PcapngFrameIter<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + if self.done { + return None; + } + let reader = self.reader.as_mut()?; + loop { + match reader.next() { + Ok((offset, block)) => { + let outcome = match block { + PcapBlockOwned::NG(Block::SectionHeader(_)) => Step::Continue, + PcapBlockOwned::NG(Block::InterfaceDescription(idb)) => { + if let Some(res) = idb.ts_resolution() { + self.ts_resolution = res; + } else { + self.ts_resolution = DEFAULT_TS_RESOLUTION; + } + Step::Continue + } + PcapBlockOwned::NG(Block::EnhancedPacket(epb)) => { + match decode_ethernet(epb.data) { + Ok(eth) => { + let timestamp_ns = epb_timestamp_ns( + epb.ts_high, + epb.ts_low, + self.ts_resolution, + ); + Step::Yield(Ok(CapturedFrame { + mac_src: eth.mac_src, + mac_dst: eth.mac_dst, + vlan_id: eth.vlan_id, + pcp: eth.pcp, + timestamp_ns, + })) + } + Err(e) => Step::Yield(Err(e)), + } + } + _ => Step::Continue, + }; + reader.consume(offset); + match outcome { + Step::Continue => continue, + Step::Yield(item) => return Some(item), + } + } + Err(PcapError::Eof) => { + self.done = true; + return None; + } + Err(PcapError::Incomplete(_)) => { + self.done = true; + if reader.position() >= self.total_len { + return None; + } + return Some(Err(IngestError::MalformedPcapng( + "incomplete trailing block".to_string(), + ))); + } + Err(e) => { + self.done = true; + return Some(Err(IngestError::MalformedPcapng(format!("{e:?}")))); + } + } + } + } +} + +enum Step { + Continue, + Yield(Result), +} + +/// L2 fields extracted from a single Ethernet frame's header. +struct EthHeader { + mac_dst: [u8; 6], + mac_src: [u8; 6], + vlan_id: Option, + pcp: Option, +} + +/// Decode the L2 header of an Ethernet frame. +fn decode_ethernet(data: &[u8]) -> Result { + if data.len() < 14 { + return Err(IngestError::Truncated); + } + let mut mac_dst = [0u8; 6]; + let mut mac_src = [0u8; 6]; + mac_dst.copy_from_slice(&data[0..6]); + mac_src.copy_from_slice(&data[6..12]); + let ethertype = u16::from_be_bytes([data[12], data[13]]); + if ethertype == 0x8100 { + if data.len() < 16 { + return Err(IngestError::Truncated); + } + let tci = u16::from_be_bytes([data[14], data[15]]); + let pcp = ((tci >> 13) & 0x7) as u8; + let vlan_id = tci & 0x0FFF; + Ok(EthHeader { + mac_dst, + mac_src, + vlan_id: Some(vlan_id), + pcp: Some(pcp), + }) + } else { + Ok(EthHeader { + mac_dst, + mac_src, + vlan_id: None, + pcp: None, + }) + } +} + +/// Convert an EPB ts_high/ts_low pair to ns-since-Unix-epoch using +/// the IDB's ts_resolution (fractions-of-a-second). +fn epb_timestamp_ns(ts_high: u32, ts_low: u32, resolution: u64) -> u64 { + let ticks = (u64::from(ts_high) << 32) | u64::from(ts_low); + if resolution == 0 { + return 0; + } + ticks + .saturating_mul(1_000_000_000) + .saturating_div(resolution) +} + +/// Pull the link type from the first Interface Description Block. +fn first_idb_linktype(bytes: &[u8]) -> Result { + let mut reader = PcapNGReader::new(bytes.len().max(65536), bytes) + .map_err(|e| IngestError::MalformedPcapng(format!("{e:?}")))?; + loop { + match reader.next() { + Ok((offset, block)) => { + if let PcapBlockOwned::NG(Block::InterfaceDescription(idb)) = block { + let lt = idb.linktype; + return Ok(lt); + } + reader.consume(offset); + } + Err(PcapError::Eof) => { + return Err(IngestError::MalformedPcapng( + "no InterfaceDescriptionBlock found".to_string(), + )); + } + Err(PcapError::Incomplete(_)) => { + return Err(IngestError::MalformedPcapng( + "incomplete pcapng prefix".to_string(), + )); + } + Err(e) => { + return Err(IngestError::MalformedPcapng(format!("{e:?}"))); + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -473,4 +744,182 @@ mod tests { assert_eq!(src.neighbors().len(), 1); assert_eq!(src.neighbors()[0].local_port, "eth0"); } + + // ── PCAPNG tests ──────────────────────────────────────────────── + use std::io::Write as _; + + /// Hand-build a minimal valid pcapng buffer: + /// SHB + IDB(linktype, if_tsresol) + EPB(ts_high, ts_low, frame_data). + fn build_pcapng( + linktype_id: u16, + if_tsresol: u8, + ts_high: u32, + ts_low: u32, + frame: &[u8], + ) -> Vec { + let mut out = Vec::new(); + + // Section Header Block (block_type 0x0A0D0D0A). + let shb_total = 4 + 4 + 4 + 4 + 8 + 4; + out.extend_from_slice(&0x0A0D_0D0A_u32.to_le_bytes()); + out.extend_from_slice(&(shb_total as u32).to_le_bytes()); + out.extend_from_slice(&0x1A2B_3C4D_u32.to_le_bytes()); + out.extend_from_slice(&1u16.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&(-1_i64).to_le_bytes()); + out.extend_from_slice(&(shb_total as u32).to_le_bytes()); + + // Interface Description Block (block_type 0x00000001). + let mut idb_options: Vec = Vec::new(); + idb_options.extend_from_slice(&9u16.to_le_bytes()); + idb_options.extend_from_slice(&1u16.to_le_bytes()); + idb_options.push(if_tsresol); + idb_options.extend_from_slice(&[0u8, 0, 0]); + idb_options.extend_from_slice(&0u16.to_le_bytes()); + idb_options.extend_from_slice(&0u16.to_le_bytes()); + + let idb_total = 4 + 4 + 2 + 2 + 4 + idb_options.len() + 4; + out.extend_from_slice(&0x0000_0001_u32.to_le_bytes()); + out.extend_from_slice(&(idb_total as u32).to_le_bytes()); + out.extend_from_slice(&linktype_id.to_le_bytes()); + out.extend_from_slice(&0u16.to_le_bytes()); + out.extend_from_slice(&65535u32.to_le_bytes()); + out.extend_from_slice(&idb_options); + out.extend_from_slice(&(idb_total as u32).to_le_bytes()); + + // Enhanced Packet Block (block_type 0x00000006). + let pad_len = (4 - (frame.len() % 4)) % 4; + let epb_data_padded_len = frame.len() + pad_len; + let epb_total = 4 + 4 + 4 + 4 + 4 + 4 + 4 + epb_data_padded_len + 4; + out.extend_from_slice(&0x0000_0006_u32.to_le_bytes()); + out.extend_from_slice(&(epb_total as u32).to_le_bytes()); + out.extend_from_slice(&0u32.to_le_bytes()); + out.extend_from_slice(&ts_high.to_le_bytes()); + out.extend_from_slice(&ts_low.to_le_bytes()); + out.extend_from_slice(&(frame.len() as u32).to_le_bytes()); + out.extend_from_slice(&(frame.len() as u32).to_le_bytes()); + out.extend_from_slice(frame); + out.extend_from_slice(&vec![0u8; pad_len]); + out.extend_from_slice(&(epb_total as u32).to_le_bytes()); + + out + } + + fn write_to_tempfile(bytes: &[u8]) -> tempfile::NamedTempFile { + let mut f = tempfile::NamedTempFile::new().unwrap(); + f.write_all(bytes).unwrap(); + f.flush().unwrap(); + f + } + + fn untagged_eth(dst: [u8; 6], src: [u8; 6], ethertype: u16) -> Vec { + let mut v = Vec::with_capacity(14); + v.extend_from_slice(&dst); + v.extend_from_slice(&src); + v.extend_from_slice(ðertype.to_be_bytes()); + v + } + + fn tagged_eth( + dst: [u8; 6], + src: [u8; 6], + vlan_id: u16, + pcp: u8, + inner_ethertype: u16, + ) -> Vec { + let mut v = Vec::with_capacity(16); + v.extend_from_slice(&dst); + v.extend_from_slice(&src); + v.extend_from_slice(&0x8100_u16.to_be_bytes()); + let tci = ((u16::from(pcp) & 0x7) << 13) | (vlan_id & 0x0FFF); + v.extend_from_slice(&tci.to_be_bytes()); + v.extend_from_slice(&inner_ethertype.to_be_bytes()); + v + } + + #[test] + fn pcapng_roundtrip_untagged_frame() { + let dst = [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01]; + let src = [0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x02]; + let frame = untagged_eth(dst, src, 0x0800); + let ts_high = 1u32; + let ts_low = 100u32; + let bytes = build_pcapng(1, 6, ts_high, ts_low, &frame); + let f = write_to_tempfile(&bytes); + + let mut src_iter = PcapngFrameSource::open(f.path()).unwrap(); + let frames: Vec<_> = src_iter.frames().collect(); + assert_eq!(frames.len(), 1); + let cf = frames.into_iter().next().unwrap().unwrap(); + assert_eq!(cf.mac_dst, dst); + assert_eq!(cf.mac_src, src); + assert_eq!(cf.vlan_id, None); + assert_eq!(cf.pcp, None); + let expected_ticks = (u64::from(ts_high) << 32) | u64::from(ts_low); + assert_eq!(cf.timestamp_ns, expected_ticks * 1000); + } + + #[test] + fn pcapng_roundtrip_8021q_tagged() { + let dst = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]; + let src = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]; + let frame = tagged_eth(dst, src, 100, 5, 0x0800); + let bytes = build_pcapng(1, 6, 0, 12345, &frame); + let f = write_to_tempfile(&bytes); + + let mut s = PcapngFrameSource::open(f.path()).unwrap(); + let frames: Vec<_> = s.frames().collect(); + assert_eq!(frames.len(), 1); + let cf = frames.into_iter().next().unwrap().unwrap(); + assert_eq!(cf.mac_dst, dst); + assert_eq!(cf.mac_src, src); + assert_eq!(cf.vlan_id, Some(100)); + assert_eq!(cf.pcp, Some(5)); + assert_eq!(cf.timestamp_ns, 12345 * 1000); + } + + #[test] + fn pcapng_truncated_frame_yields_error() { + let frame = vec![0u8; 8]; + let bytes = build_pcapng(1, 6, 0, 0, &frame); + let f = write_to_tempfile(&bytes); + + let mut s = PcapngFrameSource::open(f.path()).unwrap(); + let mut it = s.frames(); + let first = it.next().expect("expected one item"); + match first { + Err(IngestError::Truncated) => {} + other => panic!("expected Truncated, got {other:?}"), + } + } + + #[test] + fn pcapng_unsupported_linktype_errors_at_open() { + let frame = vec![0u8; 20]; + let bytes = build_pcapng(101, 6, 0, 0, &frame); + let f = write_to_tempfile(&bytes); + + match PcapngFrameSource::open(f.path()) { + Err(IngestError::UnsupportedLinkType(101)) => {} + other => panic!("expected UnsupportedLinkType(101), got {other:?}"), + } + } + + #[test] + fn pcapng_ts_resol_nanoseconds() { + let dst = [0x11; 6]; + let src = [0x22; 6]; + let frame = untagged_eth(dst, src, 0x88B5); + let total_ticks: u64 = 9_876_543_210; + let ts_high = (total_ticks >> 32) as u32; + let ts_low = (total_ticks & 0xFFFF_FFFF) as u32; + let bytes = build_pcapng(1, 9, ts_high, ts_low, &frame); + let f = write_to_tempfile(&bytes); + + let mut s = PcapngFrameSource::open(f.path()).unwrap(); + let frames: Vec<_> = s.frames().collect(); + let cf = frames.into_iter().next().unwrap().unwrap(); + let expected = (u64::from(ts_high) << 32) | u64::from(ts_low); + assert_eq!(cf.timestamp_ns, expected); + } } diff --git a/crates/spar-trace-topology/src/lib.rs b/crates/spar-trace-topology/src/lib.rs index 96cbf88..33ea39e 100644 --- a/crates/spar-trace-topology/src/lib.rs +++ b/crates/spar-trace-topology/src/lib.rs @@ -42,4 +42,7 @@ pub mod ingest; pub mod reconcile; pub mod report; -pub use ingest::{LldpJsonTopologySource, LldpNeighbor, TopologySource}; +pub use ingest::{ + CapturedFrame, FrameSource, IngestError, LldpJsonTopologySource, LldpNeighbor, + PcapngFrameSource, TopologySource, +}; diff --git a/supply-chain/config.toml b/supply-chain/config.toml index a4191b9..5ed2f85 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -96,6 +96,10 @@ criteria = "safe-to-run" version = "0.2.2" criteria = "safe-to-run" +[[exemptions.circular]] +version = "0.3.0" +criteria = "safe-to-deploy" + [[exemptions.clang-sys]] version = "1.8.1" criteria = "safe-to-deploy" @@ -364,6 +368,10 @@ criteria = "safe-to-deploy" version = "0.9.12" criteria = "safe-to-deploy" +[[exemptions.pcap-parser]] +version = "0.16.0" +criteria = "safe-to-deploy" + [[exemptions.petgraph]] version = "0.6.5" criteria = "safe-to-deploy" @@ -476,6 +484,10 @@ criteria = "safe-to-deploy" version = "1.1.0" criteria = "safe-to-deploy" +[[exemptions.rusticata-macros]] +version = "4.1.0" +criteria = "safe-to-deploy" + [[exemptions.rustix]] version = "1.1.4" criteria = "safe-to-run"