diff --git a/Cargo.lock b/Cargo.lock index 4ed443b..a7d3930 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1184,7 +1184,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.30" +version = "0.1.31" dependencies = [ "async-recursion", "base64", diff --git a/README.md b/README.md index 4631213..2bed136 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,14 @@ To keep dependency bloat and compile time down, everything is contained in featu | `debug_proxy` | Send GDB commands to the device.| | `dvt` | Access Apple developer tools (e.g. Instruments).| | `heartbeat` | Maintain a heartbeat connection.| +| `house_arrest` | Manage files in app containers | | `installation_proxy` | Manage app installation and uninstallation.| | `springboardservices` | Control SpringBoard (e.g. UI interactions). Partial support.| | `misagent` | Manage provisioning profiles on the device.| | `mobile_image_mounter` | Manage DDI images.| | `location_simulation` | Simulate GPS locations on the device.| | `pair` | Pair the device.| +| `syslog_relay` | Relay system logs from the device | | `tcp` | Connect to devices over TCP.| | `tunnel_tcp_stack` | Naive in-process TCP stack for `core_device_proxy`.| | `tss` | Make requests to Apple’s TSS servers. Partial support.| @@ -53,11 +55,9 @@ Implement the following: - companion_proxy - diagnostics -- house_arrest - mobilebackup2 - notification_proxy - screenshot -- syslog_relay - webinspector As this project is done in my free time within my busy schedule, there diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 7c0fc65..e6f0dfd 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -2,7 +2,7 @@ name = "idevice" description = "A Rust library to interact with services on iOS devices." authors = ["Jackson Coxson"] -version = "0.1.30" +version = "0.1.31" edition = "2021" license = "MIT" documentation = "https://docs.rs/idevice" @@ -32,6 +32,7 @@ chrono = { version = "0.4.40", optional = true, default-features = false } serde_json = { version = "1", optional = true } json = { version = "0.12", optional = true } byteorder = { version = "1.5", optional = true } +bytes = { version = "1.10", optional = true } reqwest = { version = "0.12", features = [ "json", @@ -68,6 +69,7 @@ misagent = [] mobile_image_mounter = ["dep:sha2"] location_simulation = [] pair = ["chrono/default", "dep:sha2", "dep:rsa", "dep:x509-cert"] +syslog_relay = ["dep:bytes"] tcp = ["tokio/net"] tunnel_tcp_stack = ["dep:rand", "dep:futures", "tokio/fs", "tokio/sync"] tss = ["dep:uuid", "dep:reqwest"] @@ -101,6 +103,7 @@ full = [ "tss", "tunneld", "springboardservices", + "syslog_relay", ] [package.metadata.docs.rs] diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 7b95b0a..4bbfa29 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -295,6 +295,38 @@ impl Idevice { } } + #[cfg(feature = "syslog_relay")] + async fn read_until_delim( + &mut self, + delimiter: &[u8], + ) -> Result, IdeviceError> { + if let Some(socket) = &mut self.socket { + let mut buffer = bytes::BytesMut::with_capacity(1024); + let mut temp = [0u8; 1024]; + + loop { + let n = socket.read(&mut temp).await?; + if n == 0 { + if buffer.is_empty() { + return Ok(None); // EOF and no data + } else { + return Ok(Some(buffer)); // EOF but return partial data + } + } + + buffer.extend_from_slice(&temp[..n]); + + if let Some(pos) = buffer.windows(delimiter.len()).position(|w| w == delimiter) { + let mut line = buffer.split_to(pos + delimiter.len()); + line.truncate(line.len() - delimiter.len()); // remove delimiter + return Ok(Some(line)); + } + } + } else { + Err(IdeviceError::NoEstablishedConnection) + } + } + /// Upgrades the connection to TLS using device pairing credentials /// /// # Arguments diff --git a/idevice/src/services/mod.rs b/idevice/src/services/mod.rs index 20f95b6..3f0fab3 100644 --- a/idevice/src/services/mod.rs +++ b/idevice/src/services/mod.rs @@ -23,5 +23,7 @@ pub mod misagent; pub mod mobile_image_mounter; #[cfg(feature = "springboardservices")] pub mod springboardservices; +#[cfg(feature = "syslog_relay")] +pub mod syslog_relay; #[cfg(feature = "xpc")] pub mod xpc; diff --git a/idevice/src/services/syslog_relay.rs b/idevice/src/services/syslog_relay.rs new file mode 100644 index 0000000..461d81a --- /dev/null +++ b/idevice/src/services/syslog_relay.rs @@ -0,0 +1,78 @@ +//! iOS Device SyslogRelay Service Abstraction + +use crate::{lockdown::LockdownClient, Idevice, IdeviceError, IdeviceService}; + +/// Client for interacting with the iOS device SyslogRelay service +pub struct SyslogRelayClient { + /// The underlying device connection with established SyslogRelay service + pub idevice: Idevice, +} + +impl IdeviceService for SyslogRelayClient { + /// Returns the SyslogRelay service name as registered with lockdownd + fn service_name() -> &'static str { + "com.apple.syslog_relay" + } + + /// Establishes a connection to the SyslogRelay service + /// + /// # Arguments + /// * `provider` - Device connection provider + /// + /// # Returns + /// A connected `SyslogRelayClient` 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 SyslogRelay service port + /// 4. Establishes connection to the SyslogRelay port + /// 5. Optionally starts TLS if required by service + async fn connect( + provider: &dyn crate::provider::IdeviceProvider, + ) -> Result { + 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 }) + } +} + +impl SyslogRelayClient { + /// Creates a new SyslogRelay client from an existing device connection + /// + /// # Arguments + /// * `idevice` - Pre-established device connection + pub fn new(idevice: Idevice) -> Self { + Self { idevice } + } + + /// 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 { + let res = self.idevice.read_until_delim(b"\n\x00").await?; + match res { + Some(res) => Ok(String::from_utf8_lossy(&res).to_string()), + None => Err(IdeviceError::UnexpectedResponse), + } + } +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index b854807..9927c1e 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -73,6 +73,10 @@ path = "src/amfi.rs" name = "pair" path = "src/pair.rs" +[[bin]] +name = "syslog_relay" +path = "src/syslog_relay.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"] } tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] } diff --git a/tools/src/syslog_relay.rs b/tools/src/syslog_relay.rs new file mode 100644 index 0000000..0ebf483 --- /dev/null +++ b/tools/src/syslog_relay.rs @@ -0,0 +1,66 @@ +// Jackson Coxson + +use clap::{Arg, Command}; +use idevice::{syslog_relay::SyslogRelayClient, IdeviceService}; + +mod common; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let matches = Command::new("syslog_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::("udid"); + let host = matches.get_one::("host"); + let pairing_file = matches.get_one::("pairing_file"); + + let provider = match common::get_provider(udid, host, pairing_file, "misagent-jkcoxson").await { + Ok(p) => p, + Err(e) => { + eprintln!("{e}"); + return; + } + }; + let mut log_client = SyslogRelayClient::connect(&*provider) + .await + .expect("Unable to connect to misagent"); + + loop { + println!( + "{}", + log_client.next().await.expect("Failed to read next log") + ); + } +}