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
15service_error!(PcapError);
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct CapturedPacket {
19    pub ts_sec: u32,
20    pub ts_usec: u32,
21    pub interface_name: String,
22    pub pid: i32,
23    pub pid2: i32,
24    pub proc_name: String,
25    pub proc_name2: String,
26    pub payload: Vec<u8>,
27}
28
29pub struct PcapClient<S> {
30    stream: S,
31}
32
33impl<S: AsyncRead + AsyncWrite + Unpin> PcapClient<S> {
34    pub fn new(stream: S) -> Self {
35        Self { stream }
36    }
37
38    pub async fn next_packet(&mut self) -> Result<CapturedPacket, PcapError> {
39        let mut len_buf = [0u8; 4];
40        self.stream.read_exact(&mut len_buf).await?;
41        let len = u32::from_be_bytes(len_buf) as usize;
42        const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
43        if len > MAX_PLIST_SIZE {
44            return Err(PcapError::Protocol(format!(
45                "plist length {len} exceeds maximum of {MAX_PLIST_SIZE}"
46            )));
47        }
48
49        let mut buf = vec![0u8; len];
50        self.stream.read_exact(&mut buf).await?;
51        let payload = plist::from_bytes::<Value>(&buf)?
52            .into_data()
53            .ok_or_else(|| PcapError::Protocol("pcap plist payload was not data".into()))?;
54
55        decode_packet(&payload)
56    }
57}
58
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct PacketFilter {
61    pub pid: Option<i32>,
62    pub process_prefix: Option<String>,
63}
64
65impl PacketFilter {
66    pub fn matches(&self, packet: &CapturedPacket) -> bool {
67        if let Some(pid) = self.pid {
68            if packet.pid != pid && packet.pid2 != pid {
69                return false;
70            }
71        }
72
73        if let Some(prefix) = &self.process_prefix {
74            if !packet.proc_name.starts_with(prefix) && !packet.proc_name2.starts_with(prefix) {
75                return false;
76            }
77        }
78
79        true
80    }
81}
82
83pub fn write_global_header<W: std::io::Write>(writer: &mut W) -> Result<(), PcapError> {
84    writer.write_all(&[
85        0xd4, 0xc3, 0xb2, 0xa1, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
86        0x00, 0xff, 0xff, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
87    ])?;
88    Ok(())
89}
90
91pub fn write_packet_record<W: std::io::Write>(
92    writer: &mut W,
93    packet: &CapturedPacket,
94) -> Result<(), PcapError> {
95    let length = checked_packet_record_len(packet.payload.len())?;
96    writer.write_all(&packet.ts_sec.to_le_bytes())?;
97    writer.write_all(&packet.ts_usec.to_le_bytes())?;
98    writer.write_all(&length.to_le_bytes())?;
99    writer.write_all(&length.to_le_bytes())?;
100    writer.write_all(&packet.payload)?;
101    Ok(())
102}
103
104fn checked_packet_record_len(len: usize) -> Result<u32, PcapError> {
105    u32::try_from(len)
106        .map_err(|_| PcapError::Protocol(format!("packet payload too large for pcap: {len}")))
107}
108
109fn decode_packet(buf: &[u8]) -> Result<CapturedPacket, PcapError> {
110    if buf.len() < DEFAULT_HEADER_SIZE {
111        return Err(PcapError::Protocol(format!(
112            "pcap frame too short for header: {}",
113            buf.len()
114        )));
115    }
116
117    let hdr_size = be_u32(buf, 0)? as usize;
118    if hdr_size < DEFAULT_HEADER_SIZE {
119        return Err(PcapError::Protocol(format!(
120            "pcap header too small: {hdr_size}"
121        )));
122    }
123    if buf.len() < hdr_size {
124        return Err(PcapError::Protocol(format!(
125            "pcap frame shorter than header size: {} < {hdr_size}",
126            buf.len()
127        )));
128    }
129
130    let frame_pre_length = be_u32(buf, 17)?;
131    let interface_name = parse_fixed_string(buf, 25, 16)?;
132    let pid = le_i32(buf, 41)?;
133    let proc_name = parse_fixed_string(buf, 45, 17)?;
134    let pid2 = le_i32(buf, 66)?;
135    let proc_name2 = parse_fixed_string(buf, 70, 17)?;
136    let ts_sec = be_u32(buf, 87)?;
137    let ts_usec = be_u32(buf, 91)?;
138
139    let payload = &buf[hdr_size..];
140    let payload = if frame_pre_length == 0 {
141        let mut packet = Vec::with_capacity(FAKE_ETHERNET_HEADER.len() + payload.len());
142        packet.extend_from_slice(&FAKE_ETHERNET_HEADER);
143        packet.extend_from_slice(payload);
144        packet
145    } else {
146        payload.to_vec()
147    };
148
149    Ok(CapturedPacket {
150        ts_sec,
151        ts_usec,
152        interface_name,
153        pid,
154        pid2,
155        proc_name,
156        proc_name2,
157        payload,
158    })
159}
160
161fn be_u32(buf: &[u8], offset: usize) -> Result<u32, PcapError> {
162    let bytes = buf
163        .get(offset..offset + 4)
164        .ok_or_else(|| PcapError::Protocol(format!("missing u32 at offset {offset}")))?;
165    // Safety: .get(offset..offset+4) returns exactly 4 bytes, so try_into::<[u8; 4]>() is infallible.
166    Ok(u32::from_be_bytes(bytes.try_into().unwrap()))
167}
168
169fn le_i32(buf: &[u8], offset: usize) -> Result<i32, PcapError> {
170    let bytes = buf
171        .get(offset..offset + 4)
172        .ok_or_else(|| PcapError::Protocol(format!("missing i32 at offset {offset}")))?;
173    // Safety: .get(offset..offset+4) returns exactly 4 bytes, so try_into::<[u8; 4]>() is infallible.
174    Ok(i32::from_le_bytes(bytes.try_into().unwrap()))
175}
176
177fn parse_fixed_string(buf: &[u8], offset: usize, len: usize) -> Result<String, PcapError> {
178    let bytes = buf
179        .get(offset..offset + len)
180        .ok_or_else(|| PcapError::Protocol(format!("missing string at offset {offset}")))?;
181    let trimmed = bytes
182        .iter()
183        .copied()
184        .take_while(|byte| *byte != 0)
185        .collect::<Vec<_>>();
186    String::from_utf8(trimmed).map_err(|e| PcapError::Protocol(format!("invalid string: {e}")))
187}
188
189#[cfg(test)]
190mod tests {
191    use crate::test_util::MockStream;
192
193    use super::*;
194
195    fn sample_header(frame_pre_length: u32) -> Vec<u8> {
196        let mut buf = vec![0u8; DEFAULT_HEADER_SIZE];
197        buf[0..4].copy_from_slice(&(DEFAULT_HEADER_SIZE as u32).to_be_bytes());
198        buf[17..21].copy_from_slice(&frame_pre_length.to_be_bytes());
199        buf[25..29].copy_from_slice(b"en0\0");
200        buf[41..45].copy_from_slice(&1234i32.to_le_bytes());
201        buf[45..52].copy_from_slice(b"Safari\0");
202        buf[66..70].copy_from_slice(&4321i32.to_le_bytes());
203        buf[70..77].copy_from_slice(b"WebKit\0");
204        buf[87..91].copy_from_slice(&123u32.to_be_bytes());
205        buf[91..95].copy_from_slice(&456u32.to_be_bytes());
206        buf
207    }
208
209    #[test]
210    fn decode_packet_prepends_fake_ethernet_header_for_ip_payloads() {
211        let mut raw = sample_header(0);
212        raw.extend_from_slice(&[0x45, 0x00, 0x00, 0x14]);
213
214        let packet = decode_packet(&raw).unwrap();
215        assert_eq!(packet.ts_sec, 123);
216        assert_eq!(packet.ts_usec, 456);
217        assert_eq!(packet.interface_name, "en0");
218        assert_eq!(packet.pid, 1234);
219        assert_eq!(packet.proc_name, "Safari");
220        assert_eq!(&packet.payload[..14], &FAKE_ETHERNET_HEADER);
221        assert_eq!(&packet.payload[14..], &[0x45, 0x00, 0x00, 0x14]);
222    }
223
224    #[test]
225    fn checked_packet_record_len_rejects_large_lengths() {
226        let err = checked_packet_record_len(u32::MAX as usize + 1).unwrap_err();
227
228        assert!(matches!(
229            err,
230            PcapError::Protocol(message) if message.contains("packet payload too large")
231        ));
232    }
233
234    #[tokio::test]
235    async fn next_packet_roundtrips_plist_data_frame() {
236        let mut raw = sample_header(14);
237        raw.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
238        let stream = MockStream::with_packet_data(raw);
239        let mut client = PcapClient::new(stream);
240
241        let packet = client.next_packet().await.unwrap();
242        assert_eq!(packet.ts_sec, 123);
243        assert_eq!(packet.ts_usec, 456);
244        assert_eq!(packet.pid2, 4321);
245        assert_eq!(packet.proc_name2, "WebKit");
246        assert_eq!(packet.payload, vec![0xaa, 0xbb, 0xcc, 0xdd]);
247    }
248
249    #[test]
250    fn write_global_header_writes_standard_pcap_magic() {
251        let mut buf = Vec::new();
252        write_global_header(&mut buf).unwrap();
253        assert_eq!(&buf[..4], &[0xd4, 0xc3, 0xb2, 0xa1]);
254        assert_eq!(buf.len(), 24);
255    }
256
257    #[test]
258    fn packet_filter_matches_on_either_pid_field() {
259        let packet = CapturedPacket {
260            ts_sec: 0,
261            ts_usec: 0,
262            interface_name: "en0".into(),
263            pid: 111,
264            pid2: 222,
265            proc_name: "Safari".into(),
266            proc_name2: "WebKit".into(),
267            payload: vec![1, 2, 3],
268        };
269
270        assert!(PacketFilter {
271            pid: Some(111),
272            process_prefix: None
273        }
274        .matches(&packet));
275        assert!(PacketFilter {
276            pid: Some(222),
277            process_prefix: None
278        }
279        .matches(&packet));
280        assert!(!PacketFilter {
281            pid: Some(333),
282            process_prefix: None
283        }
284        .matches(&packet));
285    }
286
287    #[test]
288    fn packet_filter_matches_on_either_process_name_field() {
289        let packet = CapturedPacket {
290            ts_sec: 0,
291            ts_usec: 0,
292            interface_name: "en0".into(),
293            pid: 111,
294            pid2: 222,
295            proc_name: "Safari".into(),
296            proc_name2: "WebKit.Networking".into(),
297            payload: vec![1, 2, 3],
298        };
299
300        assert!(PacketFilter {
301            pid: None,
302            process_prefix: Some("Saf".into())
303        }
304        .matches(&packet));
305        assert!(PacketFilter {
306            pid: None,
307            process_prefix: Some("WebKit".into())
308        }
309        .matches(&packet));
310        assert!(!PacketFilter {
311            pid: None,
312            process_prefix: Some("SpringBoard".into())
313        }
314        .matches(&packet));
315    }
316}