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