From c84cae6f5b3c8876e3dbc3aecf855df6017ce10a Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sun, 26 Jan 2025 19:30:08 -0700 Subject: [PATCH] Implement usbmuxd protocol --- idevice/Cargo.toml | 2 + idevice/src/lib.rs | 2 + idevice/src/pairing_file.rs | 6 + idevice/src/usbmuxd/mod.rs | 227 ++++++++++++++++++++++++++++++ idevice/src/usbmuxd/raw_packet.rs | 123 ++++++++++++++++ tools/Cargo.toml | 4 + tools/src/idevice_id.rs | 13 ++ 7 files changed, 377 insertions(+) create mode 100644 idevice/src/usbmuxd/mod.rs create mode 100644 idevice/src/usbmuxd/raw_packet.rs create mode 100644 tools/src/idevice_id.rs diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 9a37087..b9e537b 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -37,6 +37,7 @@ core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] heartbeat = [] installation_proxy = [] mounter = [] +usbmuxd = [] xpc = [ "tokio/full", "dep:indexmap", @@ -49,5 +50,6 @@ full = [ "heartbeat", "installation_proxy", "mounter", + "usbmuxd", "xpc", ] diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index dea849f..441c4d9 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -12,6 +12,8 @@ pub mod lockdownd; #[cfg(feature = "mounter")] pub mod mounter; pub mod pairing_file; +#[cfg(feature = "usbmuxd")] +pub mod usbmuxd; #[cfg(feature = "xpc")] pub mod xpc; diff --git a/idevice/src/pairing_file.rs b/idevice/src/pairing_file.rs index 8861167..fdd6d50 100644 --- a/idevice/src/pairing_file.rs +++ b/idevice/src/pairing_file.rs @@ -66,6 +66,12 @@ impl PairingFile { } } } + + pub fn from_value(v: &plist::Value) -> Result { + let raw: RawPairingFile = plist::from_value(v)?; + let p = raw.try_into()?; + Ok(p) + } } impl TryFrom for PairingFile { diff --git a/idevice/src/usbmuxd/mod.rs b/idevice/src/usbmuxd/mod.rs new file mode 100644 index 0000000..9df6ad6 --- /dev/null +++ b/idevice/src/usbmuxd/mod.rs @@ -0,0 +1,227 @@ +// Jackson Coxson + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[cfg(target_os = "windows")] +use std::net::{Ipv4Addr, SocketAddrV4}; + +use log::debug; +use serde::Deserialize; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::{pairing_file::PairingFile, IdeviceError, ReadWrite}; + +mod raw_packet; + +#[derive(Debug, Clone)] +pub enum Connection { + Usb, + Network(IpAddr), + Unknown(String), +} + +#[derive(Debug, Clone)] +pub struct UsbmuxdDevice { + pub connection_type: Connection, + pub udid: String, + pub device_id: u32, +} + +pub struct UsbmuxdConnection { + socket: Box, + tag: u32, +} + +#[derive(Deserialize)] +struct ListDevicesResponse { + #[serde(rename = "DeviceList")] + device_list: Vec, +} + +#[derive(Deserialize)] +struct DeviceListResponse { + #[serde(rename = "DeviceID")] + device_id: u32, + #[serde(rename = "Properties")] + properties: DevicePropertiesResponse, +} + +#[derive(Deserialize)] +struct DevicePropertiesResponse { + #[serde(rename = "ConnectionType")] + connection_type: String, + #[serde(rename = "NetworkAddress")] + network_address: Option, + #[serde(rename = "SerialNumber")] + serial_number: String, +} + +impl UsbmuxdConnection { + pub const DEFAULT_PORT: u16 = 27015; + pub const SOCKET_FILE: &str = "/var/run/usbmuxd"; + + pub const BINARY_PLIST_VERSION: u32 = 0; + pub const XML_PLIST_VERSION: u32 = 1; + + pub const RESULT_MESSAGE_TYPE: u32 = 1; + pub const PLIST_MESSAGE_TYPE: u32 = 8; + + pub async fn default() -> Result { + #[cfg(target_os = "windows")] + let socket = tokio::net::TcpStream::connect(SocketAddrV4::new( + Ipv4Addr::new(127, 0, 0, 1), + Self::DEFAULT_PORT, + )) + .await?; + + #[cfg(any(target_os = "macos", target_os = "linux"))] + let socket = tokio::net::UnixStream::connect(Self::SOCKET_FILE).await?; + + Ok(Self { + socket: Box::new(socket), + tag: 0, + }) + } + + pub async fn new(socket: Box, tag: u32) -> Self { + Self { socket, tag } + } + + pub async fn get_devices(&mut self) -> Result, IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("MessageType".into(), "ListDevices".into()); + req.insert("ClientVersionString".into(), "idevice-rs".into()); + req.insert("kLibUSBMuxVersion".into(), 3.into()); + self.write_plist(req).await?; + let res = self.read_plist().await?; + let res = plist::to_value(&res)?; + let res = plist::from_value::(&res)?; + + let mut devs = Vec::new(); + for dev in res.device_list { + let connection_type = match dev.properties.connection_type.as_str() { + "Network" => { + if let Some(addr) = dev.properties.network_address { + let addr = &Into::>::into(addr); + if addr.len() < 8 { + return Err(IdeviceError::UnexpectedResponse); + } + + let addr = match addr[0] { + 0x02 => { + // ipv4 + IpAddr::V4(Ipv4Addr::new(addr[4], addr[5], addr[6], addr[7])) + } + 0x1E => { + // ipv6 + if addr.len() < 24 { + return Err(IdeviceError::UnexpectedResponse); + } + + IpAddr::V6(Ipv6Addr::new( + u16::from_le_bytes([addr[8], addr[9]]), + u16::from_le_bytes([addr[10], addr[11]]), + u16::from_le_bytes([addr[12], addr[13]]), + u16::from_le_bytes([addr[14], addr[15]]), + u16::from_le_bytes([addr[16], addr[17]]), + u16::from_le_bytes([addr[18], addr[19]]), + u16::from_le_bytes([addr[20], addr[21]]), + u16::from_le_bytes([addr[22], addr[23]]), + )) + } + _ => { + return Err(IdeviceError::UnexpectedResponse); + } + }; + Connection::Network(addr) + } else { + return Err(IdeviceError::UnexpectedResponse); + } + } + "USB" => Connection::Usb, + _ => Connection::Unknown(dev.properties.connection_type), + }; + devs.push(UsbmuxdDevice { + connection_type, + udid: dev.properties.serial_number, + device_id: dev.device_id, + }) + } + + Ok(devs) + } + + pub async fn get_pair_record(&mut self, udid: &str) -> Result { + let mut req = plist::Dictionary::new(); + req.insert("MessageType".into(), "ReadPairRecord".into()); + req.insert("PairRecordID".into(), udid.into()); + self.write_plist(req).await?; + let res = self.read_plist().await?; + + match res.get("PairRecordData") { + Some(plist::Value::Data(d)) => PairingFile::from_bytes(d), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + pub async fn get_buid(&mut self) -> Result { + let mut req = plist::Dictionary::new(); + req.insert("MessageType".into(), "ReadBUID".into()); + self.write_plist(req).await?; + let mut res = self.read_plist().await?; + + match res.remove("BUID") { + Some(plist::Value::String(s)) => Ok(s), + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + pub async fn connect_to_device( + mut self, + device_id: u32, + port: u16, + ) -> Result, IdeviceError> { + let mut req = plist::Dictionary::new(); + req.insert("MessageType".into(), "Connect".into()); + req.insert("DeviceID".into(), device_id.into()); + req.insert("PortNumber".into(), port.into()); + self.write_plist(req).await?; + match self.read_plist().await?.get("Number") { + Some(plist::Value::Integer(i)) => match i.as_unsigned() { + Some(0) => Ok(self.socket), + _ => Err(IdeviceError::UnexpectedResponse), + }, + _ => Err(IdeviceError::UnexpectedResponse), + } + } + + async fn write_plist(&mut self, req: plist::Dictionary) -> Result<(), IdeviceError> { + let raw = raw_packet::RawPacket::new( + req, + Self::XML_PLIST_VERSION, + Self::PLIST_MESSAGE_TYPE, + self.tag, + ); + + let raw: Vec = raw.into(); + self.socket.write_all(&raw).await?; + + Ok(()) + } + + async fn read_plist(&mut self) -> Result { + let mut header_buffer = [0; 16]; + self.socket.read_exact(&mut header_buffer).await?; + + // We are safe to unwrap as it only panics if the buffer isn't 4 + let packet_size = u32::from_le_bytes(header_buffer[..4].try_into().unwrap()) - 16; + debug!("Reading {packet_size} bytes from muxer"); + + let mut body_buffer = vec![0; packet_size as usize]; + self.socket.read_exact(&mut body_buffer).await?; + + let res = plist::from_bytes(&body_buffer)?; + + Ok(res) + } +} diff --git a/idevice/src/usbmuxd/raw_packet.rs b/idevice/src/usbmuxd/raw_packet.rs new file mode 100644 index 0000000..4feb392 --- /dev/null +++ b/idevice/src/usbmuxd/raw_packet.rs @@ -0,0 +1,123 @@ +// Jackson Coxson + +use log::warn; + +#[derive(Debug)] +pub struct RawPacket { + pub size: u32, + pub version: u32, + pub message: u32, + pub tag: u32, + pub plist: plist::Dictionary, +} + +fn plist_to_bytes(p: &plist::Dictionary) -> Vec { + let buf = Vec::new(); + let mut writer = std::io::BufWriter::new(buf); + plist::to_writer_xml(&mut writer, &p).unwrap(); + + writer.into_inner().unwrap() +} + +impl RawPacket { + pub fn new(plist: plist::Dictionary, version: u32, message: u32, tag: u32) -> RawPacket { + let plist_bytes = plist_to_bytes(&plist); + let size = plist_bytes.len() as u32 + 16; + RawPacket { + size, + version, + message, + tag, + plist, + } + } +} + +impl From for Vec { + fn from(raw_packet: RawPacket) -> Vec { + let mut packet = vec![]; + packet.extend_from_slice(&raw_packet.size.to_le_bytes()); + packet.extend_from_slice(&raw_packet.version.to_le_bytes()); + packet.extend_from_slice(&raw_packet.message.to_le_bytes()); + packet.extend_from_slice(&raw_packet.tag.to_le_bytes()); + packet.extend_from_slice(&plist_to_bytes(&raw_packet.plist)); + packet + } +} + +impl TryFrom<&mut Vec> for RawPacket { + type Error = (); + fn try_from(packet: &mut Vec) -> Result { + let packet: &[u8] = packet; + packet.try_into() + } +} + +impl TryFrom<&[u8]> for RawPacket { + type Error = (); + fn try_from(packet: &[u8]) -> Result { + // Determine if we have enough data to parse + if packet.len() < 16 { + warn!("Not enough data to parse a raw packet header"); + return Err(()); + } + + let packet_size = &packet[0..4]; + let packet_size = u32::from_le_bytes(match packet_size.try_into() { + Ok(packet_size) => packet_size, + Err(_) => { + warn!("Failed to parse packet size"); + return Err(()); + } + }); + + // Determine if we have enough data to parse + if packet.len() < packet_size as usize { + warn!("Not enough data to parse a raw packet body"); + return Err(()); + } + + let packet_version = &packet[4..8]; + let packet_version = u32::from_le_bytes(match packet_version.try_into() { + Ok(packet_version) => packet_version, + Err(_) => { + warn!("Failed to parse packet version"); + return Err(()); + } + }); + + let message = &packet[8..12]; + let message = u32::from_le_bytes(match message.try_into() { + Ok(message) => message, + Err(_) => { + warn!("Failed to parse packet message"); + return Err(()); + } + }); + + let packet_tag = &packet[12..16]; + let packet_tag = u32::from_le_bytes(match packet_tag.try_into() { + Ok(packet_tag) => packet_tag, + Err(_) => { + warn!("Failed to parse packet tag"); + return Err(()); + } + }); + + let plist = &packet[16..packet_size as usize]; + let plist = if let Ok(p) = plist::from_bytes(plist) { + p + } else { + warn!("Failed to parse packet plist"); + return Err(()); + }; + + Ok(RawPacket { + size: packet_size, + version: packet_version, + message, + tag: packet_tag, + plist, + }) + } +} diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 486c2c1..30eacbf 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -29,6 +29,10 @@ path = "src/mounter.rs" name = "core_device_proxy_tun" path = "src/core_device_proxy_tun.rs" +[[bin]] +name = "idevice_id" +path = "src/idevice_id.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"] } diff --git a/tools/src/idevice_id.rs b/tools/src/idevice_id.rs new file mode 100644 index 0000000..1791e10 --- /dev/null +++ b/tools/src/idevice_id.rs @@ -0,0 +1,13 @@ +// Jackson Coxson +// Gets the devices from the muxer + +use idevice::usbmuxd::UsbmuxdConnection; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let mut muxer = UsbmuxdConnection::default().await.unwrap(); + let res = muxer.get_devices().await.unwrap(); + println!("{res:#?}"); +}