mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
Implement os_trace_relay
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1184,7 +1184,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "idevice"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"base64",
|
||||
|
||||
@@ -173,6 +173,31 @@ impl Idevice {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a binary plist-formatted message to the device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `message` - The plist value to send
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if serialization or transmission fails
|
||||
async fn send_bplist(&mut self, message: plist::Value) -> Result<(), IdeviceError> {
|
||||
if let Some(socket) = &mut self.socket {
|
||||
debug!("Sending plist: {}", pretty_print_plist(&message));
|
||||
|
||||
let buf = Vec::new();
|
||||
let mut writer = BufWriter::new(buf);
|
||||
message.to_writer_binary(&mut writer)?;
|
||||
let message = writer.into_inner().unwrap();
|
||||
let len = message.len() as u32;
|
||||
socket.write_all(&len.to_be_bytes()).await?;
|
||||
socket.write_all(&message).await?;
|
||||
socket.flush().await?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(IdeviceError::NoEstablishedConnection)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends raw binary data to the device
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -327,6 +352,23 @@ impl Idevice {
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_until_byte(&mut self, stopper: u8) -> Result<Vec<u8>, IdeviceError> {
|
||||
if let Some(socket) = &mut self.socket {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
loop {
|
||||
let b = socket.read_u8().await?;
|
||||
if b == stopper {
|
||||
return Ok(buf);
|
||||
} else {
|
||||
buf.push(b);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(IdeviceError::NoEstablishedConnection)
|
||||
}
|
||||
}
|
||||
|
||||
/// Upgrades the connection to TLS using device pairing credentials
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
@@ -21,6 +21,8 @@ pub mod lockdown;
|
||||
pub mod misagent;
|
||||
#[cfg(feature = "mobile_image_mounter")]
|
||||
pub mod mobile_image_mounter;
|
||||
#[cfg(feature = "syslog_relay")]
|
||||
pub mod os_trace_relay;
|
||||
#[cfg(feature = "springboardservices")]
|
||||
pub mod springboardservices;
|
||||
#[cfg(feature = "syslog_relay")]
|
||||
|
||||
359
idevice/src/services/os_trace_relay.rs
Normal file
359
idevice/src/services/os_trace_relay.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
//! iOS Device OsTraceRelay Service Abstraction
|
||||
//! Note that there are unknown fields that will hopefully be filled in the future.
|
||||
//! Huge thanks to pymobiledevice3 for the struct implementation
|
||||
//! https://github.com/doronz88/pymobiledevice3/blob/master/pymobiledevice3/services/os_trace.py
|
||||
|
||||
use chrono::{DateTime, NaiveDateTime};
|
||||
use plist::Dictionary;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService};
|
||||
|
||||
/// Client for interacting with the iOS device OsTraceRelay service
|
||||
pub struct OsTraceRelayClient {
|
||||
/// The underlying device connection with established OsTraceRelay service
|
||||
pub idevice: Idevice,
|
||||
}
|
||||
|
||||
impl IdeviceService for OsTraceRelayClient {
|
||||
/// Returns the OsTraceRelay service name as registered with lockdownd
|
||||
fn service_name() -> &'static str {
|
||||
"com.apple.os_trace_relay"
|
||||
}
|
||||
|
||||
/// Establishes a connection to the OsTraceRelay service
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - Device connection provider
|
||||
///
|
||||
/// # Returns
|
||||
/// A connected `OsTraceRelayClient` instance
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `IdeviceError` if any step of the connection process fails
|
||||
///
|
||||
/// # Process
|
||||
/// 1. Connects to lockdownd service
|
||||
/// 2. Starts a lockdown session
|
||||
/// 3. Requests the OsTraceRelay service port
|
||||
/// 4. Establishes connection to the OsTraceRelay port
|
||||
/// 5. Optionally starts TLS if required by service
|
||||
async fn connect(
|
||||
provider: &dyn crate::provider::IdeviceProvider,
|
||||
) -> Result<Self, IdeviceError> {
|
||||
let mut lockdown = LockdownClient::connect(provider).await?;
|
||||
lockdown
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
|
||||
let (port, ssl) = lockdown.start_service(Self::service_name()).await?;
|
||||
|
||||
let mut idevice = provider.connect(port).await?;
|
||||
if ssl {
|
||||
idevice
|
||||
.start_session(&provider.get_pairing_file().await?)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(Self { idevice })
|
||||
}
|
||||
}
|
||||
|
||||
/// An initialized client for receiving logs
|
||||
pub struct OsTraceRelayReceiver {
|
||||
inner: OsTraceRelayClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OsTraceLog {
|
||||
pid: u32,
|
||||
timestamp: NaiveDateTime,
|
||||
level: LogLevel,
|
||||
image_name: String,
|
||||
filename: String,
|
||||
message: String,
|
||||
label: Option<SyslogLabel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SyslogLabel {
|
||||
subsystem: String,
|
||||
category: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LogLevel {
|
||||
Notice = 0,
|
||||
Info = 1,
|
||||
Debug = 2,
|
||||
Error = 10,
|
||||
Fault = 11,
|
||||
}
|
||||
|
||||
impl OsTraceRelayClient {
|
||||
/// Starts the stream of logs from the relay
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pid` - An optional pid to stream logs from
|
||||
pub async fn start_trace(
|
||||
mut self,
|
||||
pid: Option<u32>,
|
||||
) -> Result<OsTraceRelayReceiver, IdeviceError> {
|
||||
let pid = match pid {
|
||||
Some(p) => p as i64,
|
||||
None => -1,
|
||||
};
|
||||
let mut req = Dictionary::new();
|
||||
req.insert("Request".into(), "StartActivity".into());
|
||||
req.insert("Pid".into(), Into::into(pid));
|
||||
req.insert("MessageFilter".into(), Into::into(65_535));
|
||||
req.insert("StreamFlags".into(), Into::into(60));
|
||||
|
||||
self.idevice
|
||||
.send_bplist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
|
||||
// Read a single byte
|
||||
self.idevice.read_raw(1).await?;
|
||||
|
||||
// Result
|
||||
let res = self.idevice.read_plist().await?;
|
||||
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some(r) => {
|
||||
if r == "RequestSuccessful" {
|
||||
Ok(OsTraceRelayReceiver { inner: self })
|
||||
} else {
|
||||
Err(IdeviceError::UnexpectedResponse)
|
||||
}
|
||||
}
|
||||
None => Err(IdeviceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the list of available PIDs
|
||||
pub async fn get_pid_list(&mut self) -> Result<Vec<u64>, IdeviceError> {
|
||||
let mut req = Dictionary::new();
|
||||
req.insert("Request".into(), "PidList".into());
|
||||
|
||||
self.idevice
|
||||
.send_bplist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
|
||||
// Read a single byte
|
||||
self.idevice.read_raw(1).await?;
|
||||
|
||||
// Result
|
||||
let res = self.idevice.read_plist().await?;
|
||||
|
||||
if let Some(pids) = res.get("Pids").and_then(|x| x.as_array()) {
|
||||
pids.iter()
|
||||
.map(|x| {
|
||||
x.as_unsigned_integer()
|
||||
.ok_or(IdeviceError::UnexpectedResponse)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Err(IdeviceError::UnexpectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a log archive and write it to the provided writer
|
||||
pub async fn create_archive<W: tokio::io::AsyncWrite + Unpin>(
|
||||
&mut self,
|
||||
out: &mut W,
|
||||
size_limit: Option<u64>,
|
||||
age_limit: Option<u64>,
|
||||
start_time: Option<u64>,
|
||||
) -> Result<(), IdeviceError> {
|
||||
let mut req = Dictionary::new();
|
||||
req.insert("Request".into(), "CreateArchive".into());
|
||||
|
||||
if let Some(size) = size_limit {
|
||||
req.insert("SizeLimit".into(), size.into());
|
||||
}
|
||||
|
||||
if let Some(age) = age_limit {
|
||||
req.insert("AgeLimit".into(), age.into());
|
||||
}
|
||||
|
||||
if let Some(time) = start_time {
|
||||
req.insert("StartTime".into(), time.into());
|
||||
}
|
||||
|
||||
self.idevice
|
||||
.send_bplist(plist::Value::Dictionary(req))
|
||||
.await?;
|
||||
|
||||
// Read a single byte
|
||||
if self.idevice.read_raw(1).await?[0] != 1 {
|
||||
return Err(IdeviceError::UnexpectedResponse);
|
||||
}
|
||||
|
||||
// Check status
|
||||
let res = self.idevice.read_plist().await?;
|
||||
match res.get("Status").and_then(|x| x.as_string()) {
|
||||
Some("RequestSuccessful") => {}
|
||||
_ => return Err(IdeviceError::UnexpectedResponse),
|
||||
}
|
||||
|
||||
// Read archive data
|
||||
loop {
|
||||
match self.idevice.read_raw(1).await {
|
||||
Ok(data) if data[0] == 0x03 => {
|
||||
let length_bytes = self.idevice.read_raw(4).await?;
|
||||
let length = u32::from_le_bytes([
|
||||
length_bytes[0],
|
||||
length_bytes[1],
|
||||
length_bytes[2],
|
||||
length_bytes[3],
|
||||
]);
|
||||
let data = self.idevice.read_raw(length as usize).await?;
|
||||
out.write_all(&data).await?;
|
||||
}
|
||||
Err(IdeviceError::Socket(_)) => break,
|
||||
_ => return Err(IdeviceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl OsTraceRelayReceiver {
|
||||
/// Get the next log from the relay
|
||||
///
|
||||
/// # Returns
|
||||
/// A string containing the log
|
||||
///
|
||||
/// # Errors
|
||||
/// UnexpectedResponse if the service sends an EOF
|
||||
pub async fn next(&mut self) -> Result<OsTraceLog, IdeviceError> {
|
||||
// Read 0x02, at the beginning of each packet
|
||||
if self.inner.idevice.read_raw(1).await?[0] != 0x02 {
|
||||
return Err(IdeviceError::UnexpectedResponse);
|
||||
}
|
||||
|
||||
// Read the len of the packet
|
||||
let pl = self.inner.idevice.read_raw(4).await?;
|
||||
let packet_length = u32::from_le_bytes([pl[0], pl[1], pl[2], pl[3]]);
|
||||
|
||||
let packet = self.inner.idevice.read_raw(packet_length as usize).await?;
|
||||
|
||||
// 9 bytes of padding
|
||||
let packet = &packet[9..];
|
||||
|
||||
// Parse PID (4 bytes)
|
||||
let pid = u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
||||
let packet = &packet[4..];
|
||||
|
||||
// Skip 42 unknown bytes
|
||||
let packet = &packet[42..];
|
||||
|
||||
// Parse timestamp (seconds + microseconds)
|
||||
let seconds = u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
||||
let packet = &packet[8..]; // skip 4 bytes padding after seconds
|
||||
let microseconds = u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]);
|
||||
let packet = &packet[4..];
|
||||
|
||||
// Skip 1 byte padding
|
||||
let packet = &packet[1..];
|
||||
|
||||
// Parse log level
|
||||
let log_level = packet[0];
|
||||
let log_level: LogLevel = log_level.try_into()?;
|
||||
let packet = &packet[1..];
|
||||
|
||||
// Skip 38 unknown bytes
|
||||
let packet = &packet[38..];
|
||||
|
||||
// Parse string sizes
|
||||
let image_name_size = u16::from_le_bytes([packet[0], packet[1]]) as usize;
|
||||
let packet = &packet[2..];
|
||||
let message_size = u16::from_le_bytes([packet[0], packet[1]]) as usize;
|
||||
let packet = &packet[2..];
|
||||
|
||||
// Skip 6 bytes
|
||||
let packet = &packet[6..];
|
||||
|
||||
// Parse subsystem and category sizes
|
||||
let subsystem_size =
|
||||
u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]) as usize;
|
||||
let packet = &packet[4..];
|
||||
let category_size =
|
||||
u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]) as usize;
|
||||
let packet = &packet[4..];
|
||||
|
||||
// Skip 4 bytes
|
||||
let packet = &packet[4..];
|
||||
|
||||
// Parse filename (null-terminated string)
|
||||
let filename_end = packet
|
||||
.iter()
|
||||
.position(|&b| b == 0)
|
||||
.ok_or(IdeviceError::UnexpectedResponse)?;
|
||||
let filename = String::from_utf8_lossy(&packet[..filename_end]).into_owned();
|
||||
let packet = &packet[filename_end + 1..];
|
||||
|
||||
// Parse image name
|
||||
let image_name_bytes = &packet[..image_name_size];
|
||||
let image_name =
|
||||
String::from_utf8_lossy(&image_name_bytes[..image_name_bytes.len() - 1]).into_owned();
|
||||
let packet = &packet[image_name_size..];
|
||||
|
||||
// Parse message
|
||||
let message_bytes = &packet[..message_size];
|
||||
let message =
|
||||
String::from_utf8_lossy(&message_bytes[..message_bytes.len() - 1]).into_owned();
|
||||
let packet = &packet[message_size..];
|
||||
|
||||
// Parse label if subsystem and category exist
|
||||
let label = if subsystem_size > 0 && category_size > 0 && !packet.is_empty() {
|
||||
let subsystem_bytes = &packet[..subsystem_size];
|
||||
let subsystem =
|
||||
String::from_utf8_lossy(&subsystem_bytes[..subsystem_bytes.len() - 1]).into_owned();
|
||||
let packet = &packet[subsystem_size..];
|
||||
|
||||
let category_bytes = &packet[..category_size];
|
||||
let category =
|
||||
String::from_utf8_lossy(&category_bytes[..category_bytes.len() - 1]).into_owned();
|
||||
|
||||
Some(SyslogLabel {
|
||||
subsystem,
|
||||
category,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let timestamp = match DateTime::from_timestamp(seconds as i64, microseconds) {
|
||||
Some(t) => t.naive_local(),
|
||||
None => return Err(IdeviceError::UnexpectedResponse),
|
||||
};
|
||||
|
||||
Ok(OsTraceLog {
|
||||
pid,
|
||||
timestamp,
|
||||
level: log_level,
|
||||
image_name,
|
||||
filename,
|
||||
message,
|
||||
label,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for LogLevel {
|
||||
type Error = IdeviceError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, IdeviceError> {
|
||||
Ok(match value {
|
||||
0 => Self::Notice,
|
||||
1 => Self::Info,
|
||||
2 => Self::Debug,
|
||||
0x10 => Self::Error,
|
||||
0x11 => Self::Fault,
|
||||
_ => return Err(IdeviceError::UnexpectedResponse),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,10 @@ path = "src/pair.rs"
|
||||
name = "syslog_relay"
|
||||
path = "src/syslog_relay.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "os_trace_relay"
|
||||
path = "src/os_trace_relay.rs"
|
||||
|
||||
[dependencies]
|
||||
idevice = { path = "../idevice", features = ["full"] }
|
||||
tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] }
|
||||
|
||||
68
tools/src/os_trace_relay.rs
Normal file
68
tools/src/os_trace_relay.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{os_trace_relay::OsTraceRelayClient, IdeviceService};
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("os_trace_relay")
|
||||
.about("Relay system logs")
|
||||
.arg(
|
||||
Arg::new("host")
|
||||
.long("host")
|
||||
.value_name("HOST")
|
||||
.help("IP address of the device"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("pairing_file")
|
||||
.long("pairing-file")
|
||||
.value_name("PATH")
|
||||
.help("Path to the pairing file"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("udid")
|
||||
.value_name("UDID")
|
||||
.help("UDID of the device (overrides host/pairing file)"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("Relay logs on the device");
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
|
||||
let udid = matches.get_one::<String>("udid");
|
||||
let host = matches.get_one::<String>("host");
|
||||
let pairing_file = matches.get_one::<String>("pairing_file");
|
||||
|
||||
let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let log_client = OsTraceRelayClient::connect(&*provider)
|
||||
.await
|
||||
.expect("Unable to connect to misagent");
|
||||
|
||||
let mut relay = log_client.start_trace(None).await.expect("Start failed");
|
||||
|
||||
loop {
|
||||
println!(
|
||||
"{:#?}",
|
||||
relay.next().await.expect("Failed to read next log")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user