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