mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
Implement basic debug proxy support
This commit is contained in:
@@ -37,6 +37,7 @@ sha2 = { version = "0.10", optional = true }
|
||||
|
||||
[features]
|
||||
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
|
||||
debug_proxy = []
|
||||
heartbeat = []
|
||||
installation_proxy = []
|
||||
mounter = ["dep:sha2"]
|
||||
@@ -54,6 +55,7 @@ xpc = [
|
||||
]
|
||||
full = [
|
||||
"core_device_proxy",
|
||||
"debug_proxy",
|
||||
"heartbeat",
|
||||
"installation_proxy",
|
||||
"mounter",
|
||||
|
||||
196
idevice/src/debug_proxy.rs
Normal file
196
idevice/src/debug_proxy.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
// Jackson Coxson
|
||||
// https://sourceware.org/gdb/current/onlinedocs/gdb.html/Packets.html#Packets
|
||||
|
||||
use log::debug;
|
||||
use std::fmt::Write;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::{IdeviceError, ReadWrite};
|
||||
|
||||
pub const SERVICE_NAME: &str = "com.apple.internal.dt.remote.debugproxy";
|
||||
|
||||
pub struct DebugProxyClient {
|
||||
pub socket: Box<dyn ReadWrite>,
|
||||
pub noack_mode: bool,
|
||||
}
|
||||
|
||||
pub struct DebugserverCommand {
|
||||
pub name: String,
|
||||
pub argv: Vec<String>,
|
||||
}
|
||||
|
||||
impl DebugserverCommand {
|
||||
pub fn new(name: String, argv: Vec<String>) -> Self {
|
||||
Self { name, argv }
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugProxyClient {
|
||||
pub fn new(socket: Box<dyn ReadWrite>) -> Self {
|
||||
Self {
|
||||
socket,
|
||||
noack_mode: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_command(
|
||||
&mut self,
|
||||
command: DebugserverCommand,
|
||||
) -> Result<Option<String>, IdeviceError> {
|
||||
// Hex-encode the arguments
|
||||
let hex_args = command
|
||||
.argv
|
||||
.iter()
|
||||
.map(|arg| hex_encode(arg.as_bytes()))
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
|
||||
// Construct the packet data (command + hex-encoded arguments)
|
||||
let packet_data = format!("{}{}", command.name, hex_args);
|
||||
|
||||
// Calculate the checksum
|
||||
let checksum = calculate_checksum(&packet_data);
|
||||
|
||||
// Construct the full packet
|
||||
let packet = format!("${}#{}", packet_data, checksum);
|
||||
|
||||
// Log the packet for debugging
|
||||
debug!("Sending packet: {}", packet);
|
||||
|
||||
// Send the packet
|
||||
self.socket.write_all(packet.as_bytes()).await?;
|
||||
|
||||
// Read the response
|
||||
let response = self.read_response().await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn read_response(&mut self) -> Result<Option<String>, IdeviceError> {
|
||||
let mut buffer = Vec::new();
|
||||
let mut received_char = [0u8; 1];
|
||||
|
||||
if !self.noack_mode {
|
||||
self.socket.read_exact(&mut received_char).await?;
|
||||
if received_char[0] != b'+' {
|
||||
debug!("No + ack");
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
self.socket.read_exact(&mut received_char).await?;
|
||||
if received_char[0] != b'$' {
|
||||
debug!("No $ response");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
loop {
|
||||
self.socket.read_exact(&mut received_char).await?;
|
||||
if received_char[0] == b'#' {
|
||||
break;
|
||||
}
|
||||
buffer.push(received_char[0]);
|
||||
}
|
||||
|
||||
if !self.noack_mode {
|
||||
self.send_ack().await?;
|
||||
}
|
||||
|
||||
let response = String::from_utf8(buffer)?;
|
||||
Ok(Some(response))
|
||||
}
|
||||
|
||||
pub async fn send_raw(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> {
|
||||
self.socket.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read(&mut self, len: usize) -> Result<String, IdeviceError> {
|
||||
let mut buf = vec![0; len];
|
||||
let r = self.socket.read(&mut buf).await?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&buf[..r]).to_string())
|
||||
}
|
||||
|
||||
pub async fn set_argv(&mut self, argv: Vec<String>) -> Result<String, IdeviceError> {
|
||||
if argv.is_empty() {
|
||||
return Err(IdeviceError::InvalidArgument);
|
||||
}
|
||||
|
||||
// Calculate the total length of the packet
|
||||
let mut pkt_len = 0;
|
||||
for (i, arg) in argv.iter().enumerate() {
|
||||
let prefix = format!(",{},{},", arg.len() * 2, i);
|
||||
pkt_len += prefix.len() + arg.len() * 2;
|
||||
}
|
||||
|
||||
// Allocate and initialize the packet
|
||||
let mut pkt = vec![0u8; pkt_len + 1];
|
||||
let mut pktp = 0;
|
||||
|
||||
for (i, arg) in argv.iter().enumerate() {
|
||||
let prefix = format!(",{},{},", arg.len() * 2, i);
|
||||
let prefix_bytes = prefix.as_bytes();
|
||||
|
||||
// Copy prefix to the packet
|
||||
pkt[pktp..pktp + prefix_bytes.len()].copy_from_slice(prefix_bytes);
|
||||
pktp += prefix_bytes.len();
|
||||
|
||||
// Hex encode the argument
|
||||
for byte in arg.bytes() {
|
||||
let hex = format!("{:02X}", byte);
|
||||
pkt[pktp..pktp + 2].copy_from_slice(hex.as_bytes());
|
||||
pktp += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the first byte of the packet
|
||||
pkt[0] = b'A';
|
||||
|
||||
// Simulate sending the command and receiving a response
|
||||
self.send_raw(&pkt).await?;
|
||||
let response = self.read(16).await?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn send_ack(&mut self) -> Result<(), IdeviceError> {
|
||||
self.socket.write_all(b"+").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_noack(&mut self) -> Result<(), IdeviceError> {
|
||||
self.socket.write_all(b"-").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_ack_mode(&mut self, enabled: bool) {
|
||||
self.noack_mode = !enabled;
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_checksum(data: &str) -> String {
|
||||
let checksum = data.bytes().fold(0u8, |acc, byte| acc.wrapping_add(byte));
|
||||
format!("{:02x}", checksum)
|
||||
}
|
||||
|
||||
fn hex_encode(bytes: &[u8]) -> String {
|
||||
bytes.iter().fold(String::new(), |mut output, b| {
|
||||
let _ = write!(output, "{b:02X}");
|
||||
output
|
||||
})
|
||||
}
|
||||
|
||||
impl From<String> for DebugserverCommand {
|
||||
fn from(s: String) -> Self {
|
||||
// Split string into command and arguments
|
||||
let mut split = s.split_whitespace();
|
||||
let command = split.next().unwrap_or("").to_string();
|
||||
let arguments: Vec<String> = split.map(|s| s.to_string()).collect();
|
||||
Self::new(command, arguments)
|
||||
}
|
||||
}
|
||||
impl From<&str> for DebugserverCommand {
|
||||
fn from(s: &str) -> DebugserverCommand {
|
||||
s.to_string().into()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#[cfg(feature = "core_device_proxy")]
|
||||
pub mod core_device_proxy;
|
||||
#[cfg(feature = "debug_proxy")]
|
||||
pub mod debug_proxy;
|
||||
#[cfg(feature = "heartbeat")]
|
||||
pub mod heartbeat;
|
||||
#[cfg(feature = "xpc")]
|
||||
@@ -294,6 +296,10 @@ pub enum IdeviceError {
|
||||
#[error("xpc message failed")]
|
||||
Xpc(#[from] xpc::error::XPCError),
|
||||
|
||||
#[cfg(feature = "debug_proxy")]
|
||||
#[error("invalid argument passed")]
|
||||
InvalidArgument,
|
||||
|
||||
#[error("unknown error `{0}` returned from device")]
|
||||
UnknownErrorType(String),
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ impl XPCConnection {
|
||||
return Ok(decoded);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Error decoding message: {:?}", err);
|
||||
log::warn!("Error decoding message: {:?}", err);
|
||||
buf.extend_from_slice(&self.inner.read_streamid(stream_id).await?);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ path = "src/idevice_id.rs"
|
||||
name = "remotexpc"
|
||||
path = "src/remotexpc.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "debug_proxy"
|
||||
path = "src/debug_proxy.rs"
|
||||
|
||||
[dependencies]
|
||||
idevice = { path = "../idevice", features = ["full"] }
|
||||
|
||||
102
tools/src/debug_proxy.rs
Normal file
102
tools/src/debug_proxy.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
// Jackson Coxson
|
||||
|
||||
use std::{
|
||||
io::Write,
|
||||
net::{IpAddr, SocketAddr},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use idevice::{debug_proxy::DebugProxyClient, tunneld::get_tunneld_devices, xpc::XPCDevice};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let matches = Command::new("remotexpc")
|
||||
.about("Get services from RemoteXPC")
|
||||
.arg(
|
||||
Arg::new("udid")
|
||||
.value_name("UDID")
|
||||
.help("UDID of the device (overrides host/pairing file)")
|
||||
.index(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("about")
|
||||
.long("about")
|
||||
.help("Show about information")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
if matches.get_flag("about") {
|
||||
println!("debug_proxy - connect to the debug proxy and run commands");
|
||||
println!("Copyright (c) 2025 Jackson Coxson");
|
||||
return;
|
||||
}
|
||||
|
||||
let udid = matches.get_one::<String>("udid");
|
||||
|
||||
let socket = SocketAddr::new(
|
||||
IpAddr::from_str("127.0.0.1").unwrap(),
|
||||
idevice::tunneld::DEFAULT_PORT,
|
||||
);
|
||||
let mut devices = get_tunneld_devices(socket)
|
||||
.await
|
||||
.expect("Failed to get tunneld devices");
|
||||
|
||||
let (_udid, device) = match udid {
|
||||
Some(u) => (
|
||||
u.to_owned(),
|
||||
devices.remove(u).expect("Device not in tunneld"),
|
||||
),
|
||||
None => devices.into_iter().next().expect("No devices"),
|
||||
};
|
||||
|
||||
// Make the connection to RemoteXPC
|
||||
let client = XPCDevice::new(Box::new(
|
||||
TcpStream::connect((device.tunnel_address.as_str(), device.tunnel_port))
|
||||
.await
|
||||
.unwrap(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Get the debug proxy
|
||||
let service = client
|
||||
.services
|
||||
.get(idevice::debug_proxy::SERVICE_NAME)
|
||||
.expect("Client did not contain debug proxy service");
|
||||
|
||||
let stream = TcpStream::connect(SocketAddr::new(
|
||||
IpAddr::from_str(&device.tunnel_address).unwrap(),
|
||||
service.port,
|
||||
))
|
||||
.await
|
||||
.expect("Failed to connect");
|
||||
|
||||
let mut dp = DebugProxyClient::new(Box::new(stream));
|
||||
|
||||
println!("Shell connected!");
|
||||
loop {
|
||||
print!("> ");
|
||||
std::io::stdout().flush().unwrap();
|
||||
|
||||
let mut buf = String::new();
|
||||
std::io::stdin().read_line(&mut buf).unwrap();
|
||||
|
||||
let buf = buf.trim();
|
||||
|
||||
if buf == "exit" {
|
||||
break;
|
||||
}
|
||||
|
||||
let res = dp.send_command(buf.into()).await.expect("Failed to send");
|
||||
if let Some(res) = res {
|
||||
println!("{res}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user