Implement os_trace_relay

This commit is contained in:
Jackson Coxson
2025-05-16 17:19:45 -06:00
parent b8e0989549
commit 6c03e8f60d
6 changed files with 476 additions and 1 deletions

2
Cargo.lock generated
View File

@@ -1184,7 +1184,7 @@ dependencies = [
[[package]] [[package]]
name = "idevice" name = "idevice"
version = "0.1.32" version = "0.1.33"
dependencies = [ dependencies = [
"async-recursion", "async-recursion",
"base64", "base64",

View File

@@ -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 /// Sends raw binary data to the device
/// ///
/// # Arguments /// # 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 /// Upgrades the connection to TLS using device pairing credentials
/// ///
/// # Arguments /// # Arguments

View File

@@ -21,6 +21,8 @@ pub mod lockdown;
pub mod misagent; pub mod misagent;
#[cfg(feature = "mobile_image_mounter")] #[cfg(feature = "mobile_image_mounter")]
pub mod mobile_image_mounter; pub mod mobile_image_mounter;
#[cfg(feature = "syslog_relay")]
pub mod os_trace_relay;
#[cfg(feature = "springboardservices")] #[cfg(feature = "springboardservices")]
pub mod springboardservices; pub mod springboardservices;
#[cfg(feature = "syslog_relay")] #[cfg(feature = "syslog_relay")]

View 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),
})
}
}

View File

@@ -77,6 +77,10 @@ path = "src/pair.rs"
name = "syslog_relay" name = "syslog_relay"
path = "src/syslog_relay.rs" path = "src/syslog_relay.rs"
[[bin]]
name = "os_trace_relay"
path = "src/os_trace_relay.rs"
[dependencies] [dependencies]
idevice = { path = "../idevice", features = ["full"] } idevice = { path = "../idevice", features = ["full"] }
tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] } tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] }

View 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")
);
}
}