1use 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 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 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}