Skip to content

Commit ddd2f84

Browse files
committed
Implement pcapd
1 parent d9bcecb commit ddd2f84

6 files changed

Lines changed: 385 additions & 11 deletions

File tree

idevice/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ mobile_image_mounter = ["dep:sha2"]
8585
mobilebackup2 = []
8686
location_simulation = []
8787
pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"]
88+
pcapd = []
8889
obfuscate = ["dep:obfstr"]
8990
restore_service = []
9091
rsd = ["xpc"]
@@ -120,6 +121,7 @@ full = [
120121
"mobile_image_mounter",
121122
"mobilebackup2",
122123
"pair",
124+
"pcapd",
123125
"restore_service",
124126
"rsd",
125127
"springboardservices",

idevice/src/lib.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -373,24 +373,30 @@ impl Idevice {
373373
/// # Errors
374374
/// Returns `IdeviceError` if reading, parsing fails, or device reports an error
375375
async fn read_plist(&mut self) -> Result<plist::Dictionary, IdeviceError> {
376+
let res = self.read_plist_value().await?;
377+
let res: plist::Dictionary = plist::from_value(&res)?;
378+
debug!("Received plist: {}", pretty_print_dictionary(&res));
379+
380+
if let Some(e) = res.get("Error") {
381+
let e: String = plist::from_value(e)?;
382+
if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) {
383+
return Err(e);
384+
} else {
385+
return Err(IdeviceError::UnknownErrorType(e));
386+
}
387+
}
388+
Ok(res)
389+
}
390+
391+
async fn read_plist_value(&mut self) -> Result<plist::Value, IdeviceError> {
376392
if let Some(socket) = &mut self.socket {
377393
debug!("Reading response size");
378394
let mut buf = [0u8; 4];
379395
socket.read_exact(&mut buf).await?;
380396
let len = u32::from_be_bytes(buf);
381397
let mut buf = vec![0; len as usize];
382398
socket.read_exact(&mut buf).await?;
383-
let res: plist::Dictionary = plist::from_bytes(&buf)?;
384-
debug!("Received plist: {}", pretty_print_dictionary(&res));
385-
386-
if let Some(e) = res.get("Error") {
387-
let e: String = plist::from_value(e)?;
388-
if let Some(e) = IdeviceError::from_device_error_type(e.as_str(), &res) {
389-
return Err(e);
390-
} else {
391-
return Err(IdeviceError::UnknownErrorType(e));
392-
}
393-
}
399+
let res: plist::Value = plist::from_bytes(&buf)?;
394400
Ok(res)
395401
} else {
396402
Err(IdeviceError::NoEstablishedConnection)
@@ -689,6 +695,8 @@ pub enum IdeviceError {
689695
UnsupportedWatchKey = -63,
690696
#[error("malformed command")]
691697
MalformedCommand = -64,
698+
#[error("integer overflow")]
699+
IntegerOverflow = -65,
692700
}
693701

694702
impl IdeviceError {
@@ -839,6 +847,7 @@ impl IdeviceError {
839847
IdeviceError::FfiBufferTooSmall(_, _) => -62,
840848
IdeviceError::UnsupportedWatchKey => -63,
841849
IdeviceError::MalformedCommand => -64,
850+
IdeviceError::IntegerOverflow => -65,
842851
}
843852
}
844853
}

idevice/src/services/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub mod mobile_image_mounter;
3333
pub mod mobilebackup2;
3434
#[cfg(feature = "syslog_relay")]
3535
pub mod os_trace_relay;
36+
#[cfg(feature = "pcapd")]
37+
pub mod pcapd;
3638
#[cfg(feature = "restore_service")]
3739
pub mod restore_service;
3840
#[cfg(feature = "rsd")]

idevice/src/services/pcapd.rs

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
//! Abstraction for pcapd
2+
3+
use plist::Value;
4+
use tokio::io::AsyncWrite;
5+
use tokio::io::AsyncWriteExt;
6+
7+
use crate::{Idevice, IdeviceError, IdeviceService, RsdService, obf};
8+
9+
const ETHERNET_HEADER: &[u8] = &[
10+
0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0xBE, 0xEF, 0x08, 0x00,
11+
];
12+
13+
/// Client for interacting with the pcapd service on the device.
14+
pub struct PcapdClient {
15+
/// The underlying device connection with established service
16+
pub idevice: Idevice,
17+
}
18+
19+
impl IdeviceService for PcapdClient {
20+
fn service_name() -> std::borrow::Cow<'static, str> {
21+
obf!("com.apple.pcapd")
22+
}
23+
24+
async fn from_stream(idevice: Idevice) -> Result<Self, crate::IdeviceError> {
25+
Ok(Self::new(idevice))
26+
}
27+
}
28+
29+
impl RsdService for PcapdClient {
30+
fn rsd_service_name() -> std::borrow::Cow<'static, str> {
31+
obf!("com.apple.pcapd.shim.remote")
32+
}
33+
34+
async fn from_stream(stream: Box<dyn crate::ReadWrite>) -> Result<Self, crate::IdeviceError> {
35+
Ok(Self::new(Idevice::new(stream, "".to_string())))
36+
}
37+
}
38+
39+
/// A Rust representation of the iOS pcapd device packet header and data.
40+
#[derive(Debug, Clone)]
41+
pub struct DevicePacket {
42+
pub header_length: u32,
43+
pub header_version: u8,
44+
pub packet_length: u32,
45+
pub interface_type: u8,
46+
pub unit: u16,
47+
pub io: u8,
48+
pub protocol_family: u32,
49+
pub frame_pre_length: u32,
50+
pub frame_post_length: u32,
51+
pub interface_name: String,
52+
pub pid: u32,
53+
pub comm: String,
54+
pub svc: u32,
55+
pub epid: u32,
56+
pub ecomm: String,
57+
pub seconds: u32,
58+
pub microseconds: u32,
59+
pub data: Vec<u8>,
60+
}
61+
62+
impl PcapdClient {
63+
pub fn new(idevice: Idevice) -> Self {
64+
Self { idevice }
65+
}
66+
67+
pub async fn next_packet(&mut self) -> Result<DevicePacket, IdeviceError> {
68+
let packet = self.idevice.read_plist_value().await?;
69+
let packet = match packet {
70+
Value::Data(p) => p,
71+
_ => {
72+
return Err(IdeviceError::UnexpectedResponse);
73+
}
74+
};
75+
let mut packet = DevicePacket::from_vec(&packet)?;
76+
packet.normalize_data();
77+
Ok(packet)
78+
}
79+
}
80+
81+
impl DevicePacket {
82+
/// Normalizes the packet data by adding a fake Ethernet header if necessary.
83+
/// This is required for tools like Wireshark to correctly dissect raw IP packets.
84+
pub fn normalize_data(&mut self) {
85+
if self.frame_pre_length == 0 {
86+
// Prepend the fake ethernet header for raw IP packets.
87+
let mut new_data = ETHERNET_HEADER.to_vec();
88+
new_data.append(&mut self.data);
89+
self.data = new_data;
90+
} else if self.interface_name.starts_with("pdp_ip") {
91+
// For cellular interfaces, skip the first 4 bytes of the original data
92+
// before prepending the header.
93+
if self.data.len() >= 4 {
94+
let mut new_data = ETHERNET_HEADER.to_vec();
95+
new_data.extend_from_slice(&self.data[4..]);
96+
self.data = new_data;
97+
}
98+
}
99+
}
100+
101+
/// Parses a byte vector into a DevicePacket.
102+
///
103+
/// This is the primary method for creating a struct from the raw data
104+
/// received from the device.
105+
///
106+
/// # Arguments
107+
/// * `bytes` - A `Vec<u8>` containing the raw bytes of a single packet frame.
108+
///
109+
/// # Returns
110+
/// A `Result` containing the parsed `DevicePacket`
111+
pub fn from_vec(bytes: &[u8]) -> Result<Self, IdeviceError> {
112+
let mut r = ByteReader::new(bytes);
113+
114+
// --- Parse Header ---
115+
let header_length = r.read_u32_be()?;
116+
let header_version = r.read_u8()?;
117+
let packet_length = r.read_u32_be()?;
118+
let interface_type = r.read_u8()?;
119+
let unit = r.read_u16_be()?;
120+
let io = r.read_u8()?;
121+
let protocol_family = r.read_u32_be()?;
122+
let frame_pre_length = r.read_u32_be()?;
123+
let frame_post_length = r.read_u32_be()?;
124+
let interface_name = r.read_cstr(16)?;
125+
let pid = r.read_u32_le()?; // Little Endian
126+
let comm = r.read_cstr(17)?;
127+
let svc = r.read_u32_be()?;
128+
let epid = r.read_u32_le()?; // Little Endian
129+
let ecomm = r.read_cstr(17)?;
130+
let seconds = r.read_u32_be()?;
131+
let microseconds = r.read_u32_be()?;
132+
133+
// --- Extract Packet Data ---
134+
// The data starts at an absolute offset defined by `header_length`.
135+
let data_start = header_length as usize;
136+
let data_end = data_start.saturating_add(packet_length as usize);
137+
138+
if data_end > bytes.len() {
139+
return Err(IdeviceError::NotEnoughBytes(bytes.len(), data_end));
140+
}
141+
let data = bytes[data_start..data_end].to_vec();
142+
143+
Ok(DevicePacket {
144+
header_length,
145+
header_version,
146+
packet_length,
147+
interface_type,
148+
unit,
149+
io,
150+
protocol_family,
151+
frame_pre_length,
152+
frame_post_length,
153+
interface_name,
154+
pid,
155+
comm,
156+
svc,
157+
epid,
158+
ecomm,
159+
seconds,
160+
microseconds,
161+
data,
162+
})
163+
}
164+
}
165+
166+
/// A helper struct to safely read from a byte slice.
167+
struct ByteReader<'a> {
168+
slice: &'a [u8],
169+
cursor: usize,
170+
}
171+
172+
impl<'a> ByteReader<'a> {
173+
fn new(slice: &'a [u8]) -> Self {
174+
Self { slice, cursor: 0 }
175+
}
176+
177+
/// Reads an exact number of bytes and advances the cursor.
178+
fn read_exact(&mut self, len: usize) -> Result<&'a [u8], IdeviceError> {
179+
let end = self
180+
.cursor
181+
.checked_add(len)
182+
.ok_or(IdeviceError::IntegerOverflow)?;
183+
if end > self.slice.len() {
184+
return Err(IdeviceError::NotEnoughBytes(len, self.slice.len()));
185+
}
186+
let result = &self.slice[self.cursor..end];
187+
self.cursor = end;
188+
Ok(result)
189+
}
190+
191+
fn read_u8(&mut self) -> Result<u8, IdeviceError> {
192+
self.read_exact(1).map(|s| s[0])
193+
}
194+
195+
fn read_u16_be(&mut self) -> Result<u16, IdeviceError> {
196+
self.read_exact(2)
197+
.map(|s| u16::from_be_bytes(s.try_into().unwrap()))
198+
}
199+
200+
fn read_u32_be(&mut self) -> Result<u32, IdeviceError> {
201+
self.read_exact(4)
202+
.map(|s| u32::from_be_bytes(s.try_into().unwrap()))
203+
}
204+
205+
fn read_u32_le(&mut self) -> Result<u32, IdeviceError> {
206+
self.read_exact(4)
207+
.map(|s| u32::from_le_bytes(s.try_into().unwrap()))
208+
}
209+
210+
/// Reads a fixed-size, null-padded C-style string.
211+
fn read_cstr(&mut self, len: usize) -> Result<String, IdeviceError> {
212+
let buffer = self.read_exact(len)?;
213+
let end = buffer.iter().position(|&b| b == 0).unwrap_or(len);
214+
String::from_utf8(buffer[..end].to_vec()).map_err(IdeviceError::Utf8)
215+
}
216+
}
217+
218+
/// A writer for creating `.pcap` files from DevicePackets without external dependencies.
219+
pub struct PcapFileWriter<W: AsyncWrite + Unpin> {
220+
writer: W,
221+
}
222+
223+
impl<W: AsyncWrite + Unpin> PcapFileWriter<W> {
224+
/// Creates a new writer and asynchronously writes the pcap global header.
225+
pub async fn new(mut writer: W) -> Result<Self, std::io::Error> {
226+
// Correct pcap global header for LINKTYPE_ETHERNET.
227+
// We use big-endian format, as is traditional.
228+
let header = [
229+
0xa1, 0xb2, 0xc3, 0xd4, // magic number (big-endian)
230+
0x00, 0x02, // version_major
231+
0x00, 0x04, // version_minor
232+
0x00, 0x00, 0x00, 0x00, // thiszone (GMT)
233+
0x00, 0x00, 0x00, 0x00, // sigfigs (accuracy)
234+
0x00, 0x04, 0x00, 0x00, // snaplen (max packet size, 262144)
235+
0x00, 0x00, 0x00, 0x01, // network (LINKTYPE_ETHERNET)
236+
];
237+
writer.write_all(&header).await?;
238+
Ok(Self { writer })
239+
}
240+
241+
/// Asynchronously writes a single DevicePacket to the pcap file.
242+
pub async fn write_packet(&mut self, packet: &DevicePacket) -> Result<(), std::io::Error> {
243+
let mut record_header = [0u8; 16];
244+
245+
// Use the packet's own timestamp for accuracy.
246+
record_header[0..4].copy_from_slice(&packet.seconds.to_be_bytes());
247+
record_header[4..8].copy_from_slice(&packet.microseconds.to_be_bytes());
248+
249+
// incl_len and orig_len
250+
let len_bytes = (packet.data.len() as u32).to_be_bytes();
251+
record_header[8..12].copy_from_slice(&len_bytes);
252+
record_header[12..16].copy_from_slice(&len_bytes);
253+
254+
// Write the record header and packet data sequentially.
255+
self.writer.write_all(&record_header).await?;
256+
self.writer.write_all(&packet.data).await?;
257+
258+
Ok(())
259+
}
260+
}

tools/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ path = "src/diagnosticsservice.rs"
113113
name = "bt_packet_logger"
114114
path = "src/bt_packet_logger.rs"
115115

116+
[[bin]]
117+
name = "pcapd"
118+
path = "src/pcapd.rs"
119+
116120
[dependencies]
117121
idevice = { path = "../idevice", features = ["full"], default-features = false }
118122
tokio = { version = "1.43", features = ["full"] }

0 commit comments

Comments
 (0)