Skip to main content

ios_core/services/pcap/
mod.rs

1//! Minimal packet capture client for `com.apple.pcapd`.
2//!
3//! The service sends lockdown plist frames whose payload is a `Data` blob containing
4//! an iOS-specific packet header followed by the captured packet bytes.
5
6use plist::Value;
7use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
8
9pub const SERVICE_NAME: &str = "com.apple.pcapd";
10const DEFAULT_HEADER_SIZE: usize = 95;
11const FAKE_ETHERNET_HEADER: [u8; 14] = [
12    0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0xbe, 0xfe, 0x08, 0x00,
13];
14
15#[derive(Debug, thiserror::Error)]
16pub enum PcapError {
17    #[error("IO error: {0}")]
18    Io(#[from] std::io::Error),
19    #[error("plist error: {0}")]
20    Plist(String),
21    #[error("protocol error: {0}")]
22    Protocol(String),
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct CapturedPacket {
27    pub ts_sec: u32,
28    pub ts_usec: u32,
29    pub interface_name: String,
30    pub pid: i32,
31    pub pid2: i32,
32    pub proc_name: String,
33    pub proc_name2: String,
34    pub payload: Vec<u8>,
35}
36
37pub struct PcapClient<S> {
38    stream: S,
39}
40
41impl<S: AsyncRead + AsyncWrite + Unpin> PcapClient<S> {
42    pub fn new(stream: S) -> Self {
43        Self { stream }
44    }
45
46    pub async fn next_packet(&mut self) -> Result<CapturedPacket, PcapError> {
47        let mut len_buf = [0u8; 4];
48        self.stream.read_exact(&mut len_buf).await?;
49        let len = u32::from_be_bytes(len_buf) as usize;
50        const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
51        if len > MAX_PLIST_SIZE {
52            return Err(PcapError::Protocol(format!(
53                "plist length {len} exceeds maximum of {MAX_PLIST_SIZE}"
54            )));
55        }
56
57        let mut buf = vec![0u8; len];
58        self.stream.read_exact(&mut buf).await?;
59        let payload = plist::from_bytes::<Value>(&buf)
60            .map_err(|e| PcapError::Plist(e.to_string()))?
61            .into_data()
62            .ok_or_else(|| PcapError::Protocol("pcap plist payload was not data".into()))?;
63
64        decode_packet(&payload)
65    }
66}
67
68#[derive(Debug, Clone, Default, PartialEq, Eq)]
69pub struct PacketFilter {
70    pub pid: Option<i32>,
71    pub process_prefix: Option<String>,
72}
73
74impl PacketFilter {
75    pub fn matches(&self, packet: &CapturedPacket) -> bool {
76        if let Some(pid) = self.pid {
77            if packet.pid != pid && packet.pid2 != pid {
78                return false;
79            }
80        }
81
82        if let Some(prefix) = &self.process_prefix {
83            if !packet.proc_name.starts_with(prefix) && !packet.proc_name2.starts_with(prefix) {
84                return false;
85            }
86        }
87
88        true
89    }
90}
91
92pub fn write_global_header<W: std::io::Write>(writer: &mut W) -> Result<(), PcapError> {
93    writer.write_all(&[
94        0xd4, 0xc3, 0xb2, 0xa1, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
95        0x00, 0xff, 0xff, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
96    ])?;
97    Ok(())
98}
99
100pub fn write_packet_record<W: std::io::Write>(
101    writer: &mut W,
102    packet: &CapturedPacket,
103) -> Result<(), PcapError> {
104    let length = packet.payload.len() as u32;
105    writer.write_all(&packet.ts_sec.to_le_bytes())?;
106    writer.write_all(&packet.ts_usec.to_le_bytes())?;
107    writer.write_all(&length.to_le_bytes())?;
108    writer.write_all(&length.to_le_bytes())?;
109    writer.write_all(&packet.payload)?;
110    Ok(())
111}
112
113fn decode_packet(buf: &[u8]) -> Result<CapturedPacket, PcapError> {
114    if buf.len() < DEFAULT_HEADER_SIZE {
115        return Err(PcapError::Protocol(format!(
116            "pcap frame too short for header: {}",
117            buf.len()
118        )));
119    }
120
121    let hdr_size = be_u32(buf, 0)? as usize;
122    if hdr_size < DEFAULT_HEADER_SIZE {
123        return Err(PcapError::Protocol(format!(
124            "pcap header too small: {hdr_size}"
125        )));
126    }
127    if buf.len() < hdr_size {
128        return Err(PcapError::Protocol(format!(
129            "pcap frame shorter than header size: {} < {hdr_size}",
130            buf.len()
131        )));
132    }
133
134    let frame_pre_length = be_u32(buf, 17)?;
135    let interface_name = parse_fixed_string(buf, 25, 16)?;
136    let pid = le_i32(buf, 41)?;
137    let proc_name = parse_fixed_string(buf, 45, 17)?;
138    let pid2 = le_i32(buf, 66)?;
139    let proc_name2 = parse_fixed_string(buf, 70, 17)?;
140    let ts_sec = be_u32(buf, 87)?;
141    let ts_usec = be_u32(buf, 91)?;
142
143    let payload = &buf[hdr_size..];
144    let payload = if frame_pre_length == 0 {
145        let mut packet = Vec::with_capacity(FAKE_ETHERNET_HEADER.len() + payload.len());
146        packet.extend_from_slice(&FAKE_ETHERNET_HEADER);
147        packet.extend_from_slice(payload);
148        packet
149    } else {
150        payload.to_vec()
151    };
152
153    Ok(CapturedPacket {
154        ts_sec,
155        ts_usec,
156        interface_name,
157        pid,
158        pid2,
159        proc_name,
160        proc_name2,
161        payload,
162    })
163}
164
165fn be_u32(buf: &[u8], offset: usize) -> Result<u32, PcapError> {
166    let bytes = buf
167        .get(offset..offset + 4)
168        .ok_or_else(|| PcapError::Protocol(format!("missing u32 at offset {offset}")))?;
169    // Safety: .get(offset..offset+4) returns exactly 4 bytes, so try_into::<[u8; 4]>() is infallible.
170    Ok(u32::from_be_bytes(bytes.try_into().unwrap()))
171}
172
173fn le_i32(buf: &[u8], offset: usize) -> Result<i32, PcapError> {
174    let bytes = buf
175        .get(offset..offset + 4)
176        .ok_or_else(|| PcapError::Protocol(format!("missing i32 at offset {offset}")))?;
177    // Safety: .get(offset..offset+4) returns exactly 4 bytes, so try_into::<[u8; 4]>() is infallible.
178    Ok(i32::from_le_bytes(bytes.try_into().unwrap()))
179}
180
181fn parse_fixed_string(buf: &[u8], offset: usize, len: usize) -> Result<String, PcapError> {
182    let bytes = buf
183        .get(offset..offset + len)
184        .ok_or_else(|| PcapError::Protocol(format!("missing string at offset {offset}")))?;
185    let trimmed = bytes
186        .iter()
187        .copied()
188        .take_while(|byte| *byte != 0)
189        .collect::<Vec<_>>();
190    String::from_utf8(trimmed).map_err(|e| PcapError::Protocol(format!("invalid string: {e}")))
191}
192
193#[cfg(test)]
194mod tests {
195    use std::pin::Pin;
196    use std::task::{Context, Poll};
197
198    use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
199
200    use super::*;
201
202    #[derive(Default)]
203    struct MockStream {
204        read_buf: Vec<u8>,
205        read_pos: usize,
206    }
207
208    impl MockStream {
209        fn with_packet_data(data: Vec<u8>) -> Self {
210            let mut plist_buf = Vec::new();
211            plist::to_writer_xml(&mut plist_buf, &Value::Data(data)).unwrap();
212            let mut read_buf = Vec::new();
213            read_buf.extend_from_slice(&(plist_buf.len() as u32).to_be_bytes());
214            read_buf.extend_from_slice(&plist_buf);
215            Self {
216                read_buf,
217                read_pos: 0,
218            }
219        }
220    }
221
222    impl AsyncRead for MockStream {
223        fn poll_read(
224            mut self: Pin<&mut Self>,
225            _cx: &mut Context<'_>,
226            buf: &mut ReadBuf<'_>,
227        ) -> Poll<std::io::Result<()>> {
228            let remaining = self.read_buf.len().saturating_sub(self.read_pos);
229            if remaining == 0 {
230                return Poll::Ready(Err(std::io::Error::new(
231                    std::io::ErrorKind::UnexpectedEof,
232                    "no more test data",
233                )));
234            }
235            let to_copy = remaining.min(buf.remaining());
236            let start = self.read_pos;
237            let end = start + to_copy;
238            buf.put_slice(&self.read_buf[start..end]);
239            self.read_pos = end;
240            Poll::Ready(Ok(()))
241        }
242    }
243
244    impl AsyncWrite for MockStream {
245        fn poll_write(
246            self: Pin<&mut Self>,
247            _cx: &mut Context<'_>,
248            buf: &[u8],
249        ) -> Poll<std::io::Result<usize>> {
250            Poll::Ready(Ok(buf.len()))
251        }
252
253        fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
254            Poll::Ready(Ok(()))
255        }
256
257        fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
258            Poll::Ready(Ok(()))
259        }
260    }
261
262    fn sample_header(frame_pre_length: u32) -> Vec<u8> {
263        let mut buf = vec![0u8; DEFAULT_HEADER_SIZE];
264        buf[0..4].copy_from_slice(&(DEFAULT_HEADER_SIZE as u32).to_be_bytes());
265        buf[17..21].copy_from_slice(&frame_pre_length.to_be_bytes());
266        buf[25..29].copy_from_slice(b"en0\0");
267        buf[41..45].copy_from_slice(&1234i32.to_le_bytes());
268        buf[45..52].copy_from_slice(b"Safari\0");
269        buf[66..70].copy_from_slice(&4321i32.to_le_bytes());
270        buf[70..77].copy_from_slice(b"WebKit\0");
271        buf[87..91].copy_from_slice(&123u32.to_be_bytes());
272        buf[91..95].copy_from_slice(&456u32.to_be_bytes());
273        buf
274    }
275
276    #[test]
277    fn decode_packet_prepends_fake_ethernet_header_for_ip_payloads() {
278        let mut raw = sample_header(0);
279        raw.extend_from_slice(&[0x45, 0x00, 0x00, 0x14]);
280
281        let packet = decode_packet(&raw).unwrap();
282        assert_eq!(packet.ts_sec, 123);
283        assert_eq!(packet.ts_usec, 456);
284        assert_eq!(packet.interface_name, "en0");
285        assert_eq!(packet.pid, 1234);
286        assert_eq!(packet.proc_name, "Safari");
287        assert_eq!(&packet.payload[..14], &FAKE_ETHERNET_HEADER);
288        assert_eq!(&packet.payload[14..], &[0x45, 0x00, 0x00, 0x14]);
289    }
290
291    #[tokio::test]
292    async fn next_packet_roundtrips_plist_data_frame() {
293        let mut raw = sample_header(14);
294        raw.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
295        let stream = MockStream::with_packet_data(raw);
296        let mut client = PcapClient::new(stream);
297
298        let packet = client.next_packet().await.unwrap();
299        assert_eq!(packet.ts_sec, 123);
300        assert_eq!(packet.ts_usec, 456);
301        assert_eq!(packet.pid2, 4321);
302        assert_eq!(packet.proc_name2, "WebKit");
303        assert_eq!(packet.payload, vec![0xaa, 0xbb, 0xcc, 0xdd]);
304    }
305
306    #[test]
307    fn write_global_header_writes_standard_pcap_magic() {
308        let mut buf = Vec::new();
309        write_global_header(&mut buf).unwrap();
310        assert_eq!(&buf[..4], &[0xd4, 0xc3, 0xb2, 0xa1]);
311        assert_eq!(buf.len(), 24);
312    }
313
314    #[test]
315    fn packet_filter_matches_on_either_pid_field() {
316        let packet = CapturedPacket {
317            ts_sec: 0,
318            ts_usec: 0,
319            interface_name: "en0".into(),
320            pid: 111,
321            pid2: 222,
322            proc_name: "Safari".into(),
323            proc_name2: "WebKit".into(),
324            payload: vec![1, 2, 3],
325        };
326
327        assert!(PacketFilter {
328            pid: Some(111),
329            process_prefix: None
330        }
331        .matches(&packet));
332        assert!(PacketFilter {
333            pid: Some(222),
334            process_prefix: None
335        }
336        .matches(&packet));
337        assert!(!PacketFilter {
338            pid: Some(333),
339            process_prefix: None
340        }
341        .matches(&packet));
342    }
343
344    #[test]
345    fn packet_filter_matches_on_either_process_name_field() {
346        let packet = CapturedPacket {
347            ts_sec: 0,
348            ts_usec: 0,
349            interface_name: "en0".into(),
350            pid: 111,
351            pid2: 222,
352            proc_name: "Safari".into(),
353            proc_name2: "WebKit.Networking".into(),
354            payload: vec![1, 2, 3],
355        };
356
357        assert!(PacketFilter {
358            pid: None,
359            process_prefix: Some("Saf".into())
360        }
361        .matches(&packet));
362        assert!(PacketFilter {
363            pid: None,
364            process_prefix: Some("WebKit".into())
365        }
366        .matches(&packet));
367        assert!(!PacketFilter {
368            pid: None,
369            process_prefix: Some("SpringBoard".into())
370        }
371        .matches(&packet));
372    }
373}