Skip to main content

rustbac_datalink/
capture.rs

1//! PCAP packet capture via a [`DataLink`](crate::DataLink) wrapper.
2//!
3//! [`CapturingDataLink`] wraps any transport and writes all sent/received
4//! frames to a PCAP file for offline analysis (e.g. with Wireshark).
5
6use crate::{DataLink, DataLinkAddress, DataLinkError};
7use std::io::{self, Write};
8use std::sync::Arc;
9use std::time::{SystemTime, UNIX_EPOCH};
10use tokio::sync::Mutex;
11
12/// PCAP link type for raw BACnet/IP (UDP payload).
13///
14/// Using `USER0` (147) since there is no official link type for BACnet
15/// application-layer capture.
16const PCAP_LINK_TYPE_USER0: u32 = 147;
17const PCAP_MAGIC: u32 = 0xa1b2c3d4;
18const PCAP_VERSION_MAJOR: u16 = 2;
19const PCAP_VERSION_MINOR: u16 = 4;
20const PCAP_MAX_SNAPLEN: u32 = 65535;
21
22/// Direction of a captured packet.
23#[derive(Debug, Clone, Copy)]
24pub enum Direction {
25    In,
26    Out,
27}
28
29/// A PCAP writer that writes the global header once and appends packet records.
30struct PcapWriter<W: Write + Send> {
31    inner: W,
32}
33
34impl<W: Write + Send> PcapWriter<W> {
35    fn new(mut writer: W) -> io::Result<Self> {
36        // Write PCAP global header.
37        writer.write_all(&PCAP_MAGIC.to_le_bytes())?;
38        writer.write_all(&PCAP_VERSION_MAJOR.to_le_bytes())?;
39        writer.write_all(&PCAP_VERSION_MINOR.to_le_bytes())?;
40        writer.write_all(&0i32.to_le_bytes())?; // thiszone
41        writer.write_all(&0u32.to_le_bytes())?; // sigfigs
42        writer.write_all(&PCAP_MAX_SNAPLEN.to_le_bytes())?;
43        writer.write_all(&PCAP_LINK_TYPE_USER0.to_le_bytes())?;
44        writer.flush()?;
45        Ok(Self { inner: writer })
46    }
47
48    fn write_packet(&mut self, data: &[u8]) -> io::Result<()> {
49        let now = SystemTime::now()
50            .duration_since(UNIX_EPOCH)
51            .unwrap_or_default();
52        let ts_sec = now.as_secs() as u32;
53        let ts_usec = now.subsec_micros();
54        let len = data.len() as u32;
55
56        self.inner.write_all(&ts_sec.to_le_bytes())?;
57        self.inner.write_all(&ts_usec.to_le_bytes())?;
58        self.inner.write_all(&len.to_le_bytes())?; // incl_len
59        self.inner.write_all(&len.to_le_bytes())?; // orig_len
60        self.inner.write_all(data)?;
61        self.inner.flush()
62    }
63}
64
65/// A [`DataLink`] wrapper that captures all frames to a PCAP file.
66///
67/// ```no_run
68/// # use rustbac_datalink::capture::CapturingDataLink;
69/// # use rustbac_datalink::bip::transport::BacnetIpTransport;
70/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
71/// let transport = BacnetIpTransport::bind("0.0.0.0:47808".parse()?).await?;
72/// let capturing = CapturingDataLink::to_file(transport, "capture.pcap")?;
73/// // Use `capturing` as your DataLink — all traffic is logged.
74/// # Ok(())
75/// # }
76/// ```
77pub struct CapturingDataLink<D: DataLink> {
78    inner: D,
79    writer: Arc<Mutex<PcapWriter<std::io::BufWriter<std::fs::File>>>>,
80}
81
82impl<D: DataLink> CapturingDataLink<D> {
83    /// Create a new capturing wrapper that writes frames to the given file path.
84    pub fn to_file(inner: D, path: impl AsRef<std::path::Path>) -> io::Result<Self> {
85        let file = std::fs::File::create(path)?;
86        let buf_writer = std::io::BufWriter::new(file);
87        let pcap = PcapWriter::new(buf_writer)?;
88        Ok(Self {
89            inner,
90            writer: Arc::new(Mutex::new(pcap)),
91        })
92    }
93}
94
95impl<D: DataLink> DataLink for CapturingDataLink<D> {
96    async fn send(&self, address: DataLinkAddress, payload: &[u8]) -> Result<(), DataLinkError> {
97        {
98            let mut w = self.writer.lock().await;
99            let _ = w.write_packet(payload);
100        }
101        self.inner.send(address, payload).await
102    }
103
104    async fn recv(&self, buf: &mut [u8]) -> Result<(usize, DataLinkAddress), DataLinkError> {
105        let result = self.inner.recv(buf).await?;
106        {
107            let mut w = self.writer.lock().await;
108            let _ = w.write_packet(&buf[..result.0]);
109        }
110        Ok(result)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn pcap_global_header_format() {
120        let mut buf = Vec::new();
121        let _writer = PcapWriter::new(&mut buf).unwrap();
122        assert_eq!(buf.len(), 24); // PCAP global header is 24 bytes
123        assert_eq!(
124            u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
125            PCAP_MAGIC
126        );
127    }
128
129    #[test]
130    fn pcap_write_packet() {
131        let mut buf = Vec::new();
132        let mut writer = PcapWriter::new(&mut buf).unwrap();
133        writer.write_packet(&[0x01, 0x02, 0x03]).unwrap();
134        // 24 (header) + 16 (packet header) + 3 (data) = 43
135        assert_eq!(buf.len(), 43);
136    }
137}