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