Merge branch 'master' into rppairing

a
This commit is contained in:
Jackson Coxson
2025-09-05 11:58:05 -06:00
166 changed files with 13521 additions and 2544 deletions

View File

@@ -3,7 +3,7 @@ name = "idevice-tools"
description = "Rust binary tools to interact with services on iOS devices."
authors = ["Jackson Coxson"]
version = "0.1.0"
edition = "2021"
edition = "2024"
license = "MIT"
documentation = "https://docs.rs/idevice"
repository = "https://github.com/jkcoxson/idevice"
@@ -21,13 +21,17 @@ path = "src/heartbeat_client.rs"
name = "instproxy"
path = "src/instproxy.rs"
[[bin]]
name = "ideviceinstaller"
path = "src/ideviceinstaller.rs"
[[bin]]
name = "mounter"
path = "src/mounter.rs"
[[bin]]
name = "core_device_proxy_tun"
path = "src/core_device_proxy_tun.rs"
# [[bin]]
# name = "core_device_proxy_tun"
# path = "src/core_device_proxy_tun.rs"
[[bin]]
name = "idevice_id"
@@ -97,15 +101,49 @@ path = "src/restore_service.rs"
name = "remote_pairing"
path = "src/remote_pairing.rs"
[[bin]]
name = "companion_proxy"
path = "src/companion_proxy.rs"
[[bin]]
name = "diagnostics"
path = "src/diagnostics.rs"
[[bin]]
name = "mobilebackup2"
path = "src/mobilebackup2.rs"
[[bin]]
name = "diagnosticsservice"
path = "src/diagnosticsservice.rs"
[[bin]]
name = "bt_packet_logger"
path = "src/bt_packet_logger.rs"
[[bin]]
name = "pcapd"
path = "src/pcapd.rs"
[[bin]]
name = "preboard"
path = "src/preboard.rs"
[dependencies]
idevice = { path = "../idevice", features = ["full"] }
tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] }
idevice = { path = "../idevice", features = ["full"], default-features = false }
tokio = { version = "1.43", features = ["full"] }
log = { version = "0.4" }
env_logger = { version = "0.11" }
tun-rs = { version = "1.5", features = ["async"] }
# tun-rs = { version = "1.5", features = ["async"] }
sha2 = { version = "0.10" }
ureq = { version = "3" }
clap = { version = "4.5" }
plist = { version = "1.7" }
ns-keyed-archive = "0.1.2"
uuid = "1.16"
futures-util = { version = "0.3" }
[features]
default = ["aws-lc"]
aws-lc = ["idevice/aws-lc"]
ring = ["idevice/ring"]

View File

@@ -2,11 +2,11 @@
use std::path::PathBuf;
use clap::{value_parser, Arg, Command};
use clap::{Arg, Command, value_parser};
use idevice::{
afc::{opcode::AfcFopenMode, AfcClient},
house_arrest::HouseArrestClient,
IdeviceService,
afc::{AfcClient, opcode::AfcFopenMode},
house_arrest::HouseArrestClient,
};
mod common;

View File

@@ -1,7 +1,7 @@
// Jackson Coxson
use clap::{Arg, Command};
use idevice::{amfi::AmfiClient, IdeviceService};
use idevice::{IdeviceService, amfi::AmfiClient};
mod common;

View File

@@ -1,12 +1,11 @@
// Jackson Coxson
use std::io::Write;
use clap::{Arg, Command};
use idevice::{
core_device::AppServiceClient, core_device_proxy::CoreDeviceProxy,
debug_proxy::DebugProxyClient, rsd::RsdHandshake, tcp::stream::AdapterStream, IdeviceService,
RsdService,
IdeviceService, RsdService,
core_device::{AppServiceClient, OpenStdioSocketClient},
core_device_proxy::CoreDeviceProxy,
rsd::RsdHandshake,
};
mod common;
@@ -100,7 +99,7 @@ async fn main() {
let host = matches.get_one::<String>("host");
let provider =
match common::get_provider(udid, host, pairing_file, "debug-proxy-jkcoxson").await {
match common::get_provider(udid, host, pairing_file, "app_service-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
@@ -112,15 +111,10 @@ async fn main() {
.expect("no core proxy");
let rsd_port = proxy.handshake.server_rsd_port;
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
adapter
.pcap("/Users/jacksoncoxson/Desktop/rs_xpc.pcap")
.await
.unwrap();
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
let mut adapter = adapter.to_async_handle();
let stream = AdapterStream::connect(&mut adapter, rsd_port)
.await
.expect("no RSD connect");
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
// Make the connection to RemoteXPC
let mut handshake = RsdHandshake::new(stream).await.unwrap();
@@ -144,12 +138,42 @@ async fn main() {
}
};
let mut stdio_conn = OpenStdioSocketClient::connect_rsd(&mut adapter, &mut handshake)
.await
.expect("no stdio");
let stdio_uuid = stdio_conn.read_uuid().await.expect("no uuid");
println!("stdio uuid: {stdio_uuid:?}");
let res = asc
.launch_application(bundle_id, &[], false, false, None, None)
.launch_application(bundle_id, &[], true, false, None, None, Some(stdio_uuid))
.await
.expect("no launch");
println!("{res:#?}");
println!("Launch response {res:#?}");
let (mut remote_reader, mut remote_writer) = tokio::io::split(stdio_conn.inner);
let mut local_stdin = tokio::io::stdin();
let mut local_stdout = tokio::io::stdout();
tokio::select! {
// Task 1: Copy data from the remote process to local stdout
res = tokio::io::copy(&mut remote_reader, &mut local_stdout) => {
if let Err(e) = res {
eprintln!("Error copying from remote to local: {}", e);
}
println!("\nRemote connection closed.");
return;
}
// Task 2: Copy data from local stdin to the remote process
res = tokio::io::copy(&mut local_stdin, &mut remote_writer) => {
if let Err(e) = res {
eprintln!("Error copying from local to remote: {}", e);
}
println!("\nLocal stdin closed.");
return;
}
}
} else if matches.subcommand_matches("processes").is_some() {
let p = asc.list_processes().await.expect("no processes?");
println!("{p:#?}");

View File

@@ -0,0 +1,104 @@
// Jackson Coxson
use clap::{Arg, Command};
use futures_util::StreamExt;
use idevice::{IdeviceService, bt_packet_logger::BtPacketLoggerClient};
use tokio::io::AsyncWrite;
use crate::pcap::{write_pcap_header, write_pcap_record};
mod common;
mod pcap;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("amfi")
.about("Capture Bluetooth packets")
.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)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("out")
.long("out")
.value_name("PCAP")
.help("Write PCAP to this file (use '-' for stdout)"),
)
.get_matches();
if matches.get_flag("about") {
println!("bt_packet_logger - capture bluetooth packets");
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 out = matches.get_one::<String>("out").map(String::to_owned);
let provider = match common::get_provider(udid, host, pairing_file, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let logger_client = BtPacketLoggerClient::connect(&*provider)
.await
.expect("Failed to connect to amfi");
let mut s = logger_client.into_stream();
// Open output (default to stdout if --out omitted)
let mut out_writer: Box<dyn AsyncWrite + Unpin + Send> = match out.as_deref() {
Some("-") | None => Box::new(tokio::io::stdout()),
Some(path) => Box::new(tokio::fs::File::create(path).await.expect("open pcap")),
};
// Write global header
write_pcap_header(&mut out_writer)
.await
.expect("pcap header");
// Drain stream to PCAP
while let Some(res) = s.next().await {
match res {
Ok(frame) => {
write_pcap_record(
&mut out_writer,
frame.hdr.ts_secs,
frame.hdr.ts_usecs,
frame.kind,
&frame.h4,
)
.await
.unwrap_or_else(|e| eprintln!("pcap write error: {e}"));
}
Err(e) => eprintln!("Failed to get next packet: {e:?}"),
}
}
}

View File

@@ -18,9 +18,7 @@ pub async fn get_provider(
pairing_file: Option<&String>,
label: &str,
) -> Result<Box<dyn IdeviceProvider>, String> {
let provider: Box<dyn IdeviceProvider> = if udid.is_some() {
let udid = udid.unwrap();
let provider: Box<dyn IdeviceProvider> = if let Some(udid) = udid {
let mut usbmuxd = if let Ok(var) = std::env::var("USBMUXD_SOCKET_ADDRESS") {
let socket = SocketAddr::from_str(&var).expect("Bad USBMUXD_SOCKET_ADDRESS");
let socket = tokio::net::TcpStream::connect(socket)
@@ -40,14 +38,16 @@ pub async fn get_provider(
}
};
Box::new(dev.to_provider(UsbmuxdAddr::from_env_var().unwrap(), label))
} else if host.is_some() && pairing_file.is_some() {
let host = match IpAddr::from_str(host.unwrap()) {
} else if let Some(host) = host
&& let Some(pairing_file) = pairing_file
{
let host = match IpAddr::from_str(host) {
Ok(h) => h,
Err(e) => {
return Err(format!("Invalid host: {e:?}"));
}
};
let pairing_file = match PairingFile::read_from_file(pairing_file.unwrap()) {
let pairing_file = match PairingFile::read_from_file(pairing_file) {
Ok(p) => p,
Err(e) => {
return Err(format!("Unable to read pairing file: {e:?}"));

View File

@@ -0,0 +1,151 @@
// Jackson Coxson
use clap::{Arg, Command, arg};
use idevice::{
IdeviceService, RsdService, companion_proxy::CompanionProxy,
core_device_proxy::CoreDeviceProxy, pretty_print_dictionary, pretty_print_plist,
rsd::RsdHandshake,
};
mod common;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("companion_proxy")
.about("Apple Watch things")
.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)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(Command::new("list").about("List the companions on the device"))
.subcommand(Command::new("listen").about("Listen for devices"))
.subcommand(
Command::new("get")
.about("Gets a value")
.arg(arg!(-d --device_udid <STRING> "the device udid to get from").required(true))
.arg(arg!(-v --value <STRING> "the value to get").required(true)),
)
.subcommand(
Command::new("start")
.about("Starts a service")
.arg(arg!(-p --port <PORT> "the port").required(true))
.arg(arg!(-n --name <STRING> "the optional service name").required(false)),
)
.subcommand(
Command::new("stop")
.about("Starts a service")
.arg(arg!(-p --port <PORT> "the port").required(true)),
)
.get_matches();
if matches.get_flag("about") {
println!("companion_proxy");
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, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider)
.await
.expect("no core_device_proxy");
let rsd_port = proxy.handshake.server_rsd_port;
let mut provider = proxy
.create_software_tunnel()
.expect("no tunnel")
.to_async_handle();
let mut handshake = RsdHandshake::new(provider.connect(rsd_port).await.unwrap())
.await
.unwrap();
let mut proxy = CompanionProxy::connect_rsd(&mut provider, &mut handshake)
.await
.expect("no companion proxy connect");
// let mut proxy = CompanionProxy::connect(&*provider)
// .await
// .expect("Failed to connect to companion proxy");
if matches.subcommand_matches("list").is_some() {
proxy.get_device_registry().await.expect("Failed to show");
} else if matches.subcommand_matches("listen").is_some() {
let mut stream = proxy.listen_for_devices().await.expect("Failed to show");
while let Ok(v) = stream.next().await {
println!("{}", pretty_print_dictionary(&v));
}
} else if let Some(matches) = matches.subcommand_matches("get") {
let key = matches.get_one::<String>("value").expect("no value passed");
let udid = matches
.get_one::<String>("device_udid")
.expect("no AW udid passed");
match proxy.get_value(udid, key).await {
Ok(value) => {
println!("{}", pretty_print_plist(&value));
}
Err(e) => {
eprintln!("Error getting value: {e}");
}
}
} else if let Some(matches) = matches.subcommand_matches("start") {
let port: u16 = matches
.get_one::<String>("port")
.expect("no port passed")
.parse()
.expect("not a number");
let name = matches.get_one::<String>("name").map(|x| x.as_str());
match proxy.start_forwarding_service_port(port, name, None).await {
Ok(value) => {
println!("started on port {value}");
}
Err(e) => {
eprintln!("Error starting: {e}");
}
}
} else if let Some(matches) = matches.subcommand_matches("stop") {
let port: u16 = matches
.get_one::<String>("port")
.expect("no port passed")
.parse()
.expect("not a number");
if let Err(e) = proxy.stop_forwarding_service_port(port).await {
eprintln!("Error starting: {e}");
}
} else {
eprintln!("Invalid usage, pass -h for help");
}
return;
}

View File

@@ -2,8 +2,8 @@
use clap::{Arg, Command};
use idevice::{
crashreportcopymobile::{flush_reports, CrashReportCopyMobileClient},
IdeviceService,
crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports},
};
mod common;
@@ -37,7 +37,11 @@ async fn main() {
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(Command::new("list").about("Lists the items in the directory"))
.subcommand(
Command::new("list")
.about("Lists the items in the directory")
.arg(Arg::new("dir").required(false).index(1)),
)
.subcommand(Command::new("flush").about("Flushes reports to the directory"))
.subcommand(
Command::new("pull")

View File

@@ -4,8 +4,8 @@ use std::io::Write;
use clap::{Arg, Command};
use idevice::{
core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient, rsd::RsdHandshake,
tcp::stream::AdapterStream, IdeviceService, RsdService,
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, debug_proxy::DebugProxyClient,
rsd::RsdHandshake,
};
mod common;
@@ -71,10 +71,9 @@ async fn main() {
.expect("no core proxy");
let rsd_port = proxy.handshake.server_rsd_port;
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
let stream = AdapterStream::connect(&mut adapter, rsd_port)
.await
.expect("no RSD connect");
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
let mut adapter = adapter.to_async_handle();
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
// Make the connection to RemoteXPC
let mut handshake = RsdHandshake::new(stream).await.unwrap();

279
tools/src/diagnostics.rs Normal file
View File

@@ -0,0 +1,279 @@
// Jackson Coxson
// idevice Rust implementation of libimobiledevice's idevicediagnostics
use clap::{Arg, ArgMatches, Command};
use idevice::{IdeviceService, services::diagnostics_relay::DiagnosticsRelayClient};
mod common;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("idevicediagnostics")
.about("Interact with the diagnostics interface of a device")
.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)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(
Command::new("ioregistry")
.about("Print IORegistry information")
.arg(
Arg::new("plane")
.long("plane")
.value_name("PLANE")
.help("IORegistry plane to query (e.g., IODeviceTree, IOService)"),
)
.arg(
Arg::new("name")
.long("name")
.value_name("NAME")
.help("Entry name to filter by"),
)
.arg(
Arg::new("class")
.long("class")
.value_name("CLASS")
.help("Entry class to filter by"),
),
)
.subcommand(
Command::new("mobilegestalt")
.about("Print MobileGestalt information")
.arg(
Arg::new("keys")
.long("keys")
.value_name("KEYS")
.help("Comma-separated list of keys to query")
.value_delimiter(',')
.num_args(1..),
),
)
.subcommand(Command::new("gasguage").about("Print gas gauge (battery) information"))
.subcommand(Command::new("nand").about("Print NAND flash information"))
.subcommand(Command::new("all").about("Print all available diagnostics information"))
.subcommand(Command::new("wifi").about("Print WiFi diagnostics information"))
.subcommand(Command::new("goodbye").about("Send Goodbye to diagnostics relay"))
.subcommand(Command::new("restart").about("Restart the device"))
.subcommand(Command::new("shutdown").about("Shutdown the device"))
.subcommand(Command::new("sleep").about("Put the device to sleep"))
.get_matches();
if matches.get_flag("about") {
println!(
"idevicediagnostics - interact with the diagnostics interface of a device. Reimplementation of libimobiledevice's binary."
);
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, "idevicediagnostics-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut diagnostics_client = match DiagnosticsRelayClient::connect(&*provider).await {
Ok(client) => client,
Err(e) => {
eprintln!("Unable to connect to diagnostics relay: {e:?}");
return;
}
};
match matches.subcommand() {
Some(("ioregistry", sub_matches)) => {
handle_ioregistry(&mut diagnostics_client, sub_matches).await;
}
Some(("mobilegestalt", sub_matches)) => {
handle_mobilegestalt(&mut diagnostics_client, sub_matches).await;
}
Some(("gasguage", _)) => {
handle_gasguage(&mut diagnostics_client).await;
}
Some(("nand", _)) => {
handle_nand(&mut diagnostics_client).await;
}
Some(("all", _)) => {
handle_all(&mut diagnostics_client).await;
}
Some(("wifi", _)) => {
handle_wifi(&mut diagnostics_client).await;
}
Some(("restart", _)) => {
handle_restart(&mut diagnostics_client).await;
}
Some(("shutdown", _)) => {
handle_shutdown(&mut diagnostics_client).await;
}
Some(("sleep", _)) => {
handle_sleep(&mut diagnostics_client).await;
}
Some(("goodbye", _)) => {
handle_goodbye(&mut diagnostics_client).await;
}
_ => {
eprintln!("No subcommand specified. Use --help for usage information.");
}
}
}
async fn handle_ioregistry(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) {
let plane = matches.get_one::<String>("plane").map(|s| s.as_str());
let name = matches.get_one::<String>("name").map(|s| s.as_str());
let class = matches.get_one::<String>("class").map(|s| s.as_str());
match client.ioregistry(plane, name, class).await {
Ok(Some(data)) => {
println!("{data:#?}");
}
Ok(None) => {
println!("No IORegistry data returned");
}
Err(e) => {
eprintln!("Failed to get IORegistry data: {e:?}");
}
}
}
async fn handle_mobilegestalt(client: &mut DiagnosticsRelayClient, matches: &ArgMatches) {
let keys = matches
.get_many::<String>("keys")
.map(|values| values.map(|s| s.to_string()).collect::<Vec<_>>());
match client.mobilegestalt(keys).await {
Ok(Some(data)) => {
println!("{data:#?}");
}
Ok(None) => {
println!("No MobileGestalt data returned");
}
Err(e) => {
eprintln!("Failed to get MobileGestalt data: {e:?}");
}
}
}
async fn handle_gasguage(client: &mut DiagnosticsRelayClient) {
match client.gasguage().await {
Ok(Some(data)) => {
println!("{data:#?}");
}
Ok(None) => {
println!("No gas gauge data returned");
}
Err(e) => {
eprintln!("Failed to get gas gauge data: {e:?}");
}
}
}
async fn handle_nand(client: &mut DiagnosticsRelayClient) {
match client.nand().await {
Ok(Some(data)) => {
println!("{data:#?}");
}
Ok(None) => {
println!("No NAND data returned");
}
Err(e) => {
eprintln!("Failed to get NAND data: {e:?}");
}
}
}
async fn handle_all(client: &mut DiagnosticsRelayClient) {
match client.all().await {
Ok(Some(data)) => {
println!("{data:#?}");
}
Ok(None) => {
println!("No diagnostics data returned");
}
Err(e) => {
eprintln!("Failed to get all diagnostics data: {e:?}");
}
}
}
async fn handle_wifi(client: &mut DiagnosticsRelayClient) {
match client.wifi().await {
Ok(Some(data)) => {
println!("{data:#?}");
}
Ok(None) => {
println!("No WiFi diagnostics returned");
}
Err(e) => {
eprintln!("Failed to get WiFi diagnostics: {e:?}");
}
}
}
async fn handle_restart(client: &mut DiagnosticsRelayClient) {
match client.restart().await {
Ok(()) => {
println!("Device restart command sent successfully");
}
Err(e) => {
eprintln!("Failed to restart device: {e:?}");
}
}
}
async fn handle_shutdown(client: &mut DiagnosticsRelayClient) {
match client.shutdown().await {
Ok(()) => {
println!("Device shutdown command sent successfully");
}
Err(e) => {
eprintln!("Failed to shutdown device: {e:?}");
}
}
}
async fn handle_sleep(client: &mut DiagnosticsRelayClient) {
match client.sleep().await {
Ok(()) => {
println!("Device sleep command sent successfully");
}
Err(e) => {
eprintln!("Failed to put device to sleep: {e:?}");
}
}
}
async fn handle_goodbye(client: &mut DiagnosticsRelayClient) {
match client.goodbye().await {
Ok(()) => println!("Goodbye acknowledged by device"),
Err(e) => eprintln!("Goodbye failed: {e:?}"),
}
}

View File

@@ -0,0 +1,106 @@
// Jackson Coxson
use clap::{Arg, Command};
use futures_util::StreamExt;
use idevice::{
IdeviceService, RsdService, core_device::DiagnostisServiceClient,
core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake,
};
use tokio::io::AsyncWriteExt;
mod common;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("remotexpc")
.about("Gets a sysdiagnose")
.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)")
.index(1),
)
.arg(
Arg::new("tunneld")
.long("tunneld")
.help("Use tunneld")
.action(clap::ArgAction::SetTrue),
)
.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 pairing_file = matches.get_one::<String>("pairing_file");
let host = matches.get_one::<String>("host");
let provider =
match common::get_provider(udid, host, pairing_file, "diagnosticsservice-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let proxy = CoreDeviceProxy::connect(&*provider)
.await
.expect("no core proxy");
let rsd_port = proxy.handshake.server_rsd_port;
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
let mut adapter = adapter.to_async_handle();
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
// Make the connection to RemoteXPC
let mut handshake = RsdHandshake::new(stream).await.unwrap();
let mut dsc = DiagnostisServiceClient::connect_rsd(&mut adapter, &mut handshake)
.await
.expect("no connect");
println!("Getting sysdiagnose, this takes a while! iOS is slow...");
let mut res = dsc
.capture_sysdiagnose(false)
.await
.expect("no sysdiagnose");
println!("Got sysdaignose! Saving to file");
let mut written = 0usize;
let mut out = tokio::fs::File::create(&res.preferred_filename)
.await
.expect("no file?");
while let Some(chunk) = res.stream.next().await {
let buf = chunk.expect("stream stopped?");
if !buf.is_empty() {
out.write_all(&buf).await.expect("no write all?");
written += buf.len();
}
println!("wrote {written}/{} bytes", res.expected_length);
}
println!("Done! Saved to {}", res.preferred_filename);
}

View File

@@ -2,7 +2,7 @@
// Heartbeat client
use clap::{Arg, Command};
use idevice::{heartbeat::HeartbeatClient, IdeviceService};
use idevice::{IdeviceService, heartbeat::HeartbeatClient};
mod common;

View File

@@ -2,7 +2,7 @@
// idevice Rust implementation of libimobiledevice's ideviceinfo
use clap::{Arg, Command};
use idevice::{lockdown::LockdownClient, IdeviceService};
use idevice::{IdeviceService, lockdown::LockdownClient};
mod common;
@@ -39,7 +39,9 @@ async fn main() {
.get_matches();
if matches.get_flag("about") {
println!("ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary.");
println!(
"ideviceinfo - get information from the idevice. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
@@ -67,7 +69,9 @@ async fn main() {
println!(
"{:?}",
lockdown_client.get_value("ProductVersion", None).await
lockdown_client
.get_value(Some("ProductVersion"), None)
.await
);
println!(
@@ -82,5 +86,5 @@ async fn main() {
.await
);
println!("{:?}", lockdown_client.idevice.get_type().await.unwrap());
println!("{:#?}", lockdown_client.get_all_values(None).await);
println!("{:#?}", lockdown_client.get_value(None, None).await);
}

View File

@@ -0,0 +1,103 @@
// A minimal ideviceinstaller-like CLI to install/upgrade apps
use clap::{Arg, ArgAction, Command};
use idevice::utils::installation;
mod common;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("ideviceinstaller")
.about("Install/upgrade apps on an iOS device (AFC + InstallationProxy)")
.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)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(ArgAction::SetTrue),
)
.subcommand(
Command::new("install")
.about("Install a local .ipa or directory")
.arg(Arg::new("path").required(true).value_name("PATH")),
)
.subcommand(
Command::new("upgrade")
.about("Upgrade from a local .ipa or directory")
.arg(Arg::new("path").required(true).value_name("PATH")),
)
.get_matches();
if matches.get_flag("about") {
println!("ideviceinstaller - install/upgrade apps using AFC + InstallationProxy (Rust)");
println!("Copyright (c) 2025");
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, "ideviceinstaller").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
if let Some(matches) = matches.subcommand_matches("install") {
let path: &String = matches.get_one("path").expect("required");
match installation::install_package_with_callback(
&*provider,
path,
None,
|(percentage, _)| async move {
println!("Installing: {percentage}%");
},
(),
)
.await
{
Ok(()) => println!("install success"),
Err(e) => eprintln!("Install failed: {e}"),
}
} else if let Some(matches) = matches.subcommand_matches("upgrade") {
let path: &String = matches.get_one("path").expect("required");
match installation::upgrade_package_with_callback(
&*provider,
path,
None,
|(percentage, _)| async move {
println!("Upgrading: {percentage}%");
},
(),
)
.await
{
Ok(()) => println!("upgrade success"),
Err(e) => eprintln!("Upgrade failed: {e}"),
}
} else {
eprintln!("Invalid usage, pass -h for help");
}
}

View File

@@ -2,7 +2,7 @@
// Just lists apps for now
use clap::{Arg, Command};
use idevice::{installation_proxy::InstallationProxyClient, IdeviceService};
use idevice::{IdeviceService, installation_proxy::InstallationProxyClient};
mod common;
@@ -47,7 +47,9 @@ async fn main() {
.get_matches();
if matches.get_flag("about") {
println!("instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary.");
println!(
"instproxy - query and manage apps installed on a device. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
@@ -69,10 +71,7 @@ async fn main() {
.await
.expect("Unable to connect to instproxy");
if matches.subcommand_matches("lookup").is_some() {
let apps = instproxy_client
.get_apps(Some("User".to_string()), None)
.await
.unwrap();
let apps = instproxy_client.get_apps(Some("User"), None).await.unwrap();
for app in apps.keys() {
println!("{app}");
}

View File

@@ -2,10 +2,7 @@
// Just lists apps for now
use clap::{Arg, Command};
use idevice::{
core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, tcp::stream::AdapterStream,
IdeviceService, RsdService,
};
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
mod common;
@@ -71,10 +68,9 @@ async fn main() {
.expect("no core proxy");
let rsd_port = proxy.handshake.server_rsd_port;
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
let stream = AdapterStream::connect(&mut adapter, rsd_port)
.await
.expect("no RSD connect");
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
let mut adapter = adapter.to_async_handle();
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
// Make the connection to RemoteXPC
let mut handshake = RsdHandshake::new(stream).await.unwrap();
@@ -118,7 +114,11 @@ async fn main() {
println!("Location set!");
println!("Press ctrl-c to stop");
loop {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
ls_client
.set(latitude, longitude)
.await
.expect("Failed to set location");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
} else {
eprintln!("Invalid usage, pass -h for help");

View File

@@ -1,7 +1,7 @@
// Jackson Coxson
use clap::{arg, Arg, Command};
use idevice::{lockdown::LockdownClient, pretty_print_plist, IdeviceService};
use clap::{Arg, Command, arg};
use idevice::{IdeviceService, lockdown::LockdownClient, pretty_print_plist};
use plist::Value;
mod common;
@@ -39,12 +39,7 @@ async fn main() {
.subcommand(
Command::new("get")
.about("Gets a value")
.arg(arg!(-v --value <STRING> "the value to get").required(true))
.arg(arg!(-d --domain <STRING> "the domain to get in").required(false)),
)
.subcommand(
Command::new("get_all")
.about("Gets all")
.arg(arg!(-v --value <STRING> "the value to get").required(false))
.arg(arg!(-d --domain <STRING> "the domain to get in").required(false)),
)
.subcommand(
@@ -57,7 +52,9 @@ async fn main() {
.get_matches();
if matches.get_flag("about") {
println!("lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary.");
println!(
"lockdown - query and manage values on a device. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
@@ -86,8 +83,8 @@ async fn main() {
match matches.subcommand() {
Some(("get", sub_m)) => {
let key = sub_m.get_one::<String>("value").unwrap();
let domain = sub_m.get_one::<String>("domain").cloned();
let key = sub_m.get_one::<String>("value").map(|x| x.as_str());
let domain = sub_m.get_one::<String>("domain").map(|x| x.as_str());
match lockdown_client.get_value(key, domain).await {
Ok(value) => {
@@ -98,27 +95,18 @@ async fn main() {
}
}
}
Some(("get_all", sub_m)) => {
let domain = sub_m.get_one::<String>("domain").cloned();
match lockdown_client.get_all_values(domain).await {
Ok(value) => {
println!("{}", pretty_print_plist(&plist::Value::Dictionary(value)));
}
Err(e) => {
eprintln!("Error getting value: {e}");
}
}
}
Some(("set", sub_m)) => {
let key = sub_m.get_one::<String>("key").unwrap();
let value_str = sub_m.get_one::<String>("value").unwrap();
let domain = sub_m.get_one::<String>("domain").cloned();
let domain = sub_m.get_one::<String>("domain");
let value = Value::String(value_str.clone());
match lockdown_client.set_value(key, value, domain).await {
match lockdown_client
.set_value(key, value, domain.map(|x| x.as_str()))
.await
{
Ok(()) => println!("Successfully set"),
Err(e) => eprintln!("Error setting value: {e}"),
}

View File

@@ -2,8 +2,8 @@
use std::path::PathBuf;
use clap::{arg, value_parser, Arg, Command};
use idevice::{misagent::MisagentClient, IdeviceService};
use clap::{Arg, Command, arg, value_parser};
use idevice::{IdeviceService, misagent::MisagentClient};
mod common;
@@ -52,7 +52,9 @@ async fn main() {
.get_matches();
if matches.get_flag("about") {
println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary.");
println!(
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}

668
tools/src/mobilebackup2.rs Normal file
View File

@@ -0,0 +1,668 @@
// Jackson Coxson
// Mobile Backup 2 tool for iOS devices
use clap::{Arg, Command};
use idevice::{
IdeviceService,
mobilebackup2::{MobileBackup2Client, RestoreOptions},
};
use plist::Dictionary;
use std::fs;
use std::io::{Read, Write};
use std::path::Path;
mod common;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("mobilebackup2")
.about("Mobile Backup 2 tool for iOS devices")
.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)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(
Command::new("info")
.about("Get backup information from a local backup directory")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(
Arg::new("source")
.long("source")
.value_name("SOURCE")
.help("Source identifier (defaults to current UDID)"),
),
)
.subcommand(
Command::new("list")
.about("List files of the last backup from a local backup directory")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(Arg::new("source").long("source").value_name("SOURCE")),
)
.subcommand(
Command::new("backup")
.about("Start a backup operation")
.arg(
Arg::new("dir")
.long("dir")
.value_name("DIR")
.help("Backup directory on host")
.required(true),
)
.arg(
Arg::new("target")
.long("target")
.value_name("TARGET")
.help("Target identifier for the backup"),
)
.arg(
Arg::new("source")
.long("source")
.value_name("SOURCE")
.help("Source identifier for the backup"),
),
)
.subcommand(
Command::new("restore")
.about("Restore from a local backup directory (DeviceLink)")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(
Arg::new("source")
.long("source")
.value_name("SOURCE")
.help("Source UDID; defaults to current device UDID"),
)
.arg(
Arg::new("password")
.long("password")
.value_name("PWD")
.help("Backup password if encrypted"),
)
.arg(
Arg::new("no-reboot")
.long("no-reboot")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("no-copy")
.long("no-copy")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("no-settings")
.long("no-settings")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("system")
.long("system")
.action(clap::ArgAction::SetTrue),
)
.arg(
Arg::new("remove")
.long("remove")
.action(clap::ArgAction::SetTrue),
),
)
.subcommand(
Command::new("unback")
.about("Unpack a complete backup to device hierarchy")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(Arg::new("source").long("source").value_name("SOURCE"))
.arg(Arg::new("password").long("password").value_name("PWD")),
)
.subcommand(
Command::new("extract")
.about("Extract a file from a previous backup")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(Arg::new("source").long("source").value_name("SOURCE"))
.arg(
Arg::new("domain")
.long("domain")
.value_name("DOMAIN")
.required(true),
)
.arg(
Arg::new("path")
.long("path")
.value_name("REL_PATH")
.required(true),
)
.arg(Arg::new("password").long("password").value_name("PWD")),
)
.subcommand(
Command::new("change-password")
.about("Change backup password")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true))
.arg(Arg::new("old").long("old").value_name("OLD"))
.arg(Arg::new("new").long("new").value_name("NEW")),
)
.subcommand(
Command::new("erase-device")
.about("Erase the device via mobilebackup2")
.arg(Arg::new("dir").long("dir").value_name("DIR").required(true)),
)
.subcommand(Command::new("freespace").about("Get free space information"))
.subcommand(Command::new("encryption").about("Check backup encryption status"))
.get_matches();
if matches.get_flag("about") {
println!("mobilebackup2 - manage device backups using Mobile Backup 2 service");
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, "mobilebackup2-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("Error creating provider: {e}");
return;
}
};
let mut backup_client = match MobileBackup2Client::connect(&*provider).await {
Ok(client) => client,
Err(e) => {
eprintln!("Unable to connect to mobilebackup2 service: {e}");
return;
}
};
match matches.subcommand() {
Some(("info", sub)) => {
let dir = sub.get_one::<String>("dir").unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str());
match backup_client.info_from_path(Path::new(dir), source).await {
Ok(dict) => {
println!("Backup Information:");
for (k, v) in dict {
println!(" {k}: {v:?}");
}
}
Err(e) => eprintln!("Failed to get info: {e}"),
}
}
Some(("list", sub)) => {
let dir = sub.get_one::<String>("dir").unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str());
match backup_client.list_from_path(Path::new(dir), source).await {
Ok(dict) => {
println!("List Response:");
for (k, v) in dict {
println!(" {k}: {v:?}");
}
}
Err(e) => eprintln!("Failed to list: {e}"),
}
}
Some(("backup", sub_matches)) => {
let target = sub_matches.get_one::<String>("target").map(|s| s.as_str());
let source = sub_matches.get_one::<String>("source").map(|s| s.as_str());
let dir = sub_matches
.get_one::<String>("dir")
.expect("dir is required");
println!("Starting backup operation...");
let res = backup_client
.send_request("Backup", target, source, None::<Dictionary>)
.await;
if let Err(e) = res {
eprintln!("Failed to send backup request: {e}");
} else if let Err(e) = process_dl_loop(&mut backup_client, Path::new(dir)).await {
eprintln!("Backup failed during DL loop: {e}");
} else {
println!("Backup flow finished");
}
}
Some(("restore", sub)) => {
let dir = sub.get_one::<String>("dir").unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str());
let mut ropts = RestoreOptions::new();
if sub.get_flag("no-reboot") {
ropts = ropts.with_reboot(false);
}
if sub.get_flag("no-copy") {
ropts = ropts.with_copy(false);
}
if sub.get_flag("no-settings") {
ropts = ropts.with_preserve_settings(false);
}
if sub.get_flag("system") {
ropts = ropts.with_system_files(true);
}
if sub.get_flag("remove") {
ropts = ropts.with_remove_items_not_restored(true);
}
if let Some(pw) = sub.get_one::<String>("password") {
ropts = ropts.with_password(pw);
}
match backup_client
.restore_from_path(Path::new(dir), source, Some(ropts))
.await
{
Ok(_) => println!("Restore flow finished"),
Err(e) => eprintln!("Restore failed: {e}"),
}
}
Some(("unback", sub)) => {
let dir = sub.get_one::<String>("dir").unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str());
let password = sub.get_one::<String>("password").map(|s| s.as_str());
match backup_client
.unback_from_path(Path::new(dir), password, source)
.await
{
Ok(_) => println!("Unback finished"),
Err(e) => eprintln!("Unback failed: {e}"),
}
}
Some(("extract", sub)) => {
let dir = sub.get_one::<String>("dir").unwrap();
let source = sub.get_one::<String>("source").map(|s| s.as_str());
let domain = sub.get_one::<String>("domain").unwrap();
let rel = sub.get_one::<String>("path").unwrap();
let password = sub.get_one::<String>("password").map(|s| s.as_str());
match backup_client
.extract_from_path(domain, rel, Path::new(dir), password, source)
.await
{
Ok(_) => println!("Extract finished"),
Err(e) => eprintln!("Extract failed: {e}"),
}
}
Some(("change-password", sub)) => {
let dir = sub.get_one::<String>("dir").unwrap();
let old = sub.get_one::<String>("old").map(|s| s.as_str());
let newv = sub.get_one::<String>("new").map(|s| s.as_str());
match backup_client
.change_password_from_path(Path::new(dir), old, newv)
.await
{
Ok(_) => println!("Change password finished"),
Err(e) => eprintln!("Change password failed: {e}"),
}
}
Some(("erase-device", sub)) => {
let dir = sub.get_one::<String>("dir").unwrap();
match backup_client.erase_device_from_path(Path::new(dir)).await {
Ok(_) => println!("Erase device command sent"),
Err(e) => eprintln!("Erase device failed: {e}"),
}
}
Some(("freespace", _)) => match backup_client.get_freespace().await {
Ok(freespace) => {
let freespace_gb = freespace as f64 / (1024.0 * 1024.0 * 1024.0);
println!("Free space: {freespace} bytes ({freespace_gb:.2} GB)");
}
Err(e) => eprintln!("Failed to get free space: {e}"),
},
Some(("encryption", _)) => match backup_client.check_backup_encryption().await {
Ok(is_encrypted) => {
println!(
"Backup encryption: {}",
if is_encrypted { "Enabled" } else { "Disabled" }
);
}
Err(e) => eprintln!("Failed to check backup encryption: {e}"),
},
_ => {
println!("No subcommand provided. Use --help for available commands.");
}
}
// Disconnect from the service
if let Err(e) = backup_client.disconnect().await {
eprintln!("Warning: Failed to disconnect cleanly: {e}");
}
}
use idevice::services::mobilebackup2::{
DL_CODE_ERROR_LOCAL as CODE_ERROR_LOCAL, DL_CODE_FILE_DATA as CODE_FILE_DATA,
DL_CODE_SUCCESS as CODE_SUCCESS,
};
async fn process_dl_loop(
client: &mut MobileBackup2Client,
host_dir: &Path,
) -> Result<Option<Dictionary>, idevice::IdeviceError> {
loop {
let (tag, value) = client.receive_dl_message().await?;
match tag.as_str() {
"DLMessageDownloadFiles" => {
handle_download_files(client, &value, host_dir).await?;
}
"DLMessageUploadFiles" => {
handle_upload_files(client, &value, host_dir).await?;
}
"DLMessageGetFreeDiskSpace" => {
// Minimal implementation: report unknown/zero with success
client
.send_status_response(0, None, Some(plist::Value::Integer(0u64.into())))
.await?;
}
"DLContentsOfDirectory" => {
// Minimal: return empty listing
let empty = plist::Value::Dictionary(Dictionary::new());
client.send_status_response(0, None, Some(empty)).await?;
}
"DLMessageCreateDirectory" => {
let status = create_directory_from_message(&value, host_dir);
client.send_status_response(status, None, None).await?;
}
"DLMessageMoveFiles" | "DLMessageMoveItems" => {
let status = move_files_from_message(&value, host_dir);
client
.send_status_response(
status,
None,
Some(plist::Value::Dictionary(Dictionary::new())),
)
.await?;
}
"DLMessageRemoveFiles" | "DLMessageRemoveItems" => {
let status = remove_files_from_message(&value, host_dir);
client
.send_status_response(
status,
None,
Some(plist::Value::Dictionary(Dictionary::new())),
)
.await?;
}
"DLMessageCopyItem" => {
let status = copy_item_from_message(&value, host_dir);
client
.send_status_response(
status,
None,
Some(plist::Value::Dictionary(Dictionary::new())),
)
.await?;
}
"DLMessageProcessMessage" => {
// Final status/content: return inner dict
if let plist::Value::Array(arr) = value
&& let Some(plist::Value::Dictionary(dict)) = arr.get(1)
{
return Ok(Some(dict.clone()));
}
return Ok(None);
}
"DLMessageDisconnect" => {
return Ok(None);
}
other => {
eprintln!("Unsupported DL message: {other}");
client
.send_status_response(-1, Some("Operation not supported"), None)
.await?;
}
}
}
}
async fn handle_download_files(
client: &mut MobileBackup2Client,
dl_value: &plist::Value,
host_dir: &Path,
) -> Result<(), idevice::IdeviceError> {
// dl_value is an array: ["DLMessageDownloadFiles", [paths...], progress?]
let mut err_any = false;
if let plist::Value::Array(arr) = dl_value
&& arr.len() >= 2
&& let Some(plist::Value::Array(files)) = arr.get(1)
{
for pv in files {
if let Some(path) = pv.as_string()
&& let Err(e) = send_single_file(client, host_dir, path).await
{
eprintln!("Failed to send file {path}: {e}");
err_any = true;
}
}
}
// terminating zero dword
client.idevice.send_raw(&0u32.to_be_bytes()).await?;
// status response
if err_any {
client
.send_status_response(
-13,
Some("Multi status"),
Some(plist::Value::Dictionary(Dictionary::new())),
)
.await
} else {
client
.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new())))
.await
}
}
async fn send_single_file(
client: &mut MobileBackup2Client,
host_dir: &Path,
rel_path: &str,
) -> Result<(), idevice::IdeviceError> {
let full = host_dir.join(rel_path);
let path_bytes = rel_path.as_bytes().to_vec();
let nlen = (path_bytes.len() as u32).to_be_bytes();
client.idevice.send_raw(&nlen).await?;
client.idevice.send_raw(&path_bytes).await?;
let mut f = match std::fs::File::open(&full) {
Ok(f) => f,
Err(e) => {
// send error
let desc = e.to_string();
let size = (desc.len() as u32 + 1).to_be_bytes();
let mut hdr = Vec::with_capacity(5);
hdr.extend_from_slice(&size);
hdr.push(CODE_ERROR_LOCAL);
client.idevice.send_raw(&hdr).await?;
client.idevice.send_raw(desc.as_bytes()).await?;
return Ok(());
}
};
let mut buf = [0u8; 32768];
loop {
let read = f.read(&mut buf).unwrap_or(0);
if read == 0 {
break;
}
let size = ((read as u32) + 1).to_be_bytes();
let mut hdr = Vec::with_capacity(5);
hdr.extend_from_slice(&size);
hdr.push(CODE_FILE_DATA);
client.idevice.send_raw(&hdr).await?;
client.idevice.send_raw(&buf[..read]).await?;
}
// success trailer
let mut ok = [0u8; 5];
ok[..4].copy_from_slice(&1u32.to_be_bytes());
ok[4] = CODE_SUCCESS;
client.idevice.send_raw(&ok).await?;
Ok(())
}
async fn handle_upload_files(
client: &mut MobileBackup2Client,
_dl_value: &plist::Value,
host_dir: &Path,
) -> Result<(), idevice::IdeviceError> {
// Minimal receiver: read pairs of (dir, filename) and block stream
// Receive dir name
loop {
let dlen = read_be_u32(client).await?;
if dlen == 0 {
break;
}
let dname = read_exact_string(client, dlen as usize).await?;
let flen = read_be_u32(client).await?;
if flen == 0 {
break;
}
let fname = read_exact_string(client, flen as usize).await?;
let dst = host_dir.join(&fname);
if let Some(parent) = dst.parent() {
let _ = fs::create_dir_all(parent);
}
let mut file = std::fs::File::create(&dst)
.map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?;
loop {
let nlen = read_be_u32(client).await?;
if nlen == 0 {
break;
}
let code = read_one(client).await?;
if code == CODE_FILE_DATA {
let size = (nlen - 1) as usize;
let data = read_exact(client, size).await?;
file.write_all(&data)
.map_err(|e| idevice::IdeviceError::InternalError(e.to_string()))?;
} else {
let _ = read_exact(client, (nlen - 1) as usize).await?;
}
}
let _ = dname; // not used
}
client
.send_status_response(0, None, Some(plist::Value::Dictionary(Dictionary::new())))
.await
}
async fn read_be_u32(client: &mut MobileBackup2Client) -> Result<u32, idevice::IdeviceError> {
let buf = client.idevice.read_raw(4).await?;
Ok(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]))
}
async fn read_one(client: &mut MobileBackup2Client) -> Result<u8, idevice::IdeviceError> {
let buf = client.idevice.read_raw(1).await?;
Ok(buf[0])
}
async fn read_exact(
client: &mut MobileBackup2Client,
size: usize,
) -> Result<Vec<u8>, idevice::IdeviceError> {
client.idevice.read_raw(size).await
}
async fn read_exact_string(
client: &mut MobileBackup2Client,
size: usize,
) -> Result<String, idevice::IdeviceError> {
let buf = client.idevice.read_raw(size).await?;
Ok(String::from_utf8_lossy(&buf).to_string())
}
fn create_directory_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
if let plist::Value::Array(arr) = dl_value
&& arr.len() >= 2
&& let Some(plist::Value::String(dir)) = arr.get(1)
{
let path = host_dir.join(dir);
return match fs::create_dir_all(&path) {
Ok(_) => 0,
Err(_) => -1,
};
}
-1
}
fn move_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
if let plist::Value::Array(arr) = dl_value
&& arr.len() >= 2
&& let Some(plist::Value::Dictionary(map)) = arr.get(1)
{
for (from, to_v) in map.iter() {
if let Some(to) = to_v.as_string() {
let old = host_dir.join(from);
let newp = host_dir.join(to);
if let Some(parent) = newp.parent() {
let _ = fs::create_dir_all(parent);
}
if fs::rename(&old, &newp).is_err() {
return -1;
}
}
}
return 0;
}
-1
}
fn remove_files_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
if let plist::Value::Array(arr) = dl_value
&& arr.len() >= 2
&& let Some(plist::Value::Array(items)) = arr.get(1)
{
for it in items {
if let Some(p) = it.as_string() {
let path = host_dir.join(p);
if path.is_dir() {
if fs::remove_dir_all(&path).is_err() {
return -1;
}
} else if path.exists() && fs::remove_file(&path).is_err() {
return -1;
}
}
}
return 0;
}
-1
}
fn copy_item_from_message(dl_value: &plist::Value, host_dir: &Path) -> i64 {
if let plist::Value::Array(arr) = dl_value
&& arr.len() >= 3
&& let (Some(plist::Value::String(src)), Some(plist::Value::String(dst))) =
(arr.get(1), arr.get(2))
{
let from = host_dir.join(src);
let to = host_dir.join(dst);
if let Some(parent) = to.parent() {
let _ = fs::create_dir_all(parent);
}
if from.is_dir() {
// shallow copy: create dir
return match fs::create_dir_all(&to) {
Ok(_) => 0,
Err(_) => -1,
};
} else {
return match fs::copy(&from, &to) {
Ok(_) => 0,
Err(_) => -1,
};
}
}
-1
}

View File

@@ -3,10 +3,10 @@
use std::{io::Write, path::PathBuf};
use clap::{arg, value_parser, Arg, Command};
use clap::{Arg, Command, arg, value_parser};
use idevice::{
lockdown::LockdownClient, mobile_image_mounter::ImageMounter, pretty_print_plist,
IdeviceService,
IdeviceService, lockdown::LockdownClient, mobile_image_mounter::ImageMounter,
pretty_print_plist,
};
mod common;
@@ -67,7 +67,9 @@ async fn main() {
.get_matches();
if matches.get_flag("about") {
println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary.");
println!(
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
@@ -89,7 +91,10 @@ async fn main() {
.await
.expect("Unable to connect to lockdown");
let product_version = match lockdown_client.get_value("ProductVersion", None).await {
let product_version = match lockdown_client
.get_value(Some("ProductVersion"), None)
.await
{
Ok(p) => p,
Err(_) => {
lockdown_client
@@ -97,7 +102,7 @@ async fn main() {
.await
.unwrap();
lockdown_client
.get_value("ProductVersion", None)
.get_value(Some("ProductVersion"), None)
.await
.unwrap()
}
@@ -182,21 +187,22 @@ async fn main() {
.await
.expect("Unable to read signature");
let unique_chip_id = match lockdown_client.get_value("UniqueChipID", None).await {
Ok(u) => u,
Err(_) => {
lockdown_client
.start_session(&provider.get_pairing_file().await.unwrap())
.await
.expect("Unable to start session");
lockdown_client
.get_value("UniqueChipID", None)
.await
.expect("Unable to get UniqueChipID")
let unique_chip_id =
match lockdown_client.get_value(Some("UniqueChipID"), None).await {
Ok(u) => u,
Err(_) => {
lockdown_client
.start_session(&provider.get_pairing_file().await.unwrap())
.await
.expect("Unable to start session");
lockdown_client
.get_value(Some("UniqueChipID"), None)
.await
.expect("Unable to get UniqueChipID")
}
}
}
.as_unsigned_integer()
.expect("Unexpected value for chip IP");
.as_unsigned_integer()
.expect("Unexpected value for chip IP");
mounter_client
.mount_personalized_with_callback(
@@ -208,7 +214,7 @@ async fn main() {
unique_chip_id,
async |((n, d), _)| {
let percent = (n as f64 / d as f64) * 100.0;
print!("\rProgress: {:.2}%", percent);
print!("\rProgress: {percent:.2}%");
std::io::stdout().flush().unwrap(); // Make sure it prints immediately
if n == d {
println!();

View File

@@ -1,7 +1,7 @@
// Jackson Coxson
use clap::{Arg, Command};
use idevice::{os_trace_relay::OsTraceRelayClient, IdeviceService};
use idevice::{IdeviceService, os_trace_relay::OsTraceRelayClient};
mod common;

View File

@@ -2,9 +2,9 @@
use clap::{Arg, Command};
use idevice::{
IdeviceService,
lockdown::LockdownClient,
usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdConnection},
IdeviceService,
};
#[tokio::main]
@@ -74,10 +74,13 @@ async fn main() {
.expect("Pairing file test failed");
// Add the UDID (jitterbug spec)
pairing_file.udid = Some(dev.udid);
pairing_file.udid = Some(dev.udid.clone());
let pairing_file = pairing_file.serialize().expect("failed to serialize");
println!(
"{}",
String::from_utf8(pairing_file.serialize().unwrap()).unwrap()
);
println!("{}", String::from_utf8(pairing_file.clone()).unwrap());
// Save with usbmuxd
u.save_pair_record(dev.device_id, &dev.udid, pairing_file)
.await
.expect("no save");
}

60
tools/src/pcap.rs Normal file
View File

@@ -0,0 +1,60 @@
use idevice::bt_packet_logger::BtPacketKind;
use tokio::io::{AsyncWrite, AsyncWriteExt};
// Classic PCAP (big-endian) global header for DLT_BLUETOOTH_HCI_H4_WITH_PHDR (201)
const PCAP_GLOBAL_HEADER_BE: [u8; 24] = [
0xA1, 0xB2, 0xC3, 0xD4, // magic (big-endian stream)
0x00, 0x02, // version maj
0x00, 0x04, // version min
0x00, 0x00, 0x00, 0x00, // thiszone
0x00, 0x00, 0x00, 0x00, // sigfigs
0x00, 0x00, 0x08, 0x00, // snaplen = 2048
0x00, 0x00, 0x00, 201, // network = 201 (HCI_H4_WITH_PHDR)
];
#[inline]
fn be32(x: u32) -> [u8; 4] {
[(x >> 24) as u8, (x >> 16) as u8, (x >> 8) as u8, x as u8]
}
#[inline]
fn dir_flag(kind: BtPacketKind) -> Option<u32> {
use BtPacketKind::*;
Some(match kind {
HciCmd | AclSent | ScoSent => 0,
HciEvt | AclRecv | ScoRecv => 1,
_ => return None,
})
}
pub async fn write_pcap_header<W: AsyncWrite + Unpin>(w: &mut W) -> std::io::Result<()> {
w.write_all(&PCAP_GLOBAL_HEADER_BE).await
}
pub async fn write_pcap_record<W: AsyncWrite + Unpin>(
w: &mut W,
ts_sec: u32,
ts_usec: u32,
kind: BtPacketKind,
h4_payload: &[u8], // starts with H4 type followed by HCI bytes
) -> std::io::Result<()> {
// Prepend 4-byte direction flag to the packet body
let Some(dir) = dir_flag(kind) else {
return Ok(());
};
let cap_len = 4u32 + h4_payload.len() as u32;
// PCAP record header (big-endian fields to match magic above)
// ts_sec, ts_usec, incl_len, orig_len
let mut rec = [0u8; 16];
rec[0..4].copy_from_slice(&be32(ts_sec));
rec[4..8].copy_from_slice(&be32(ts_usec));
rec[8..12].copy_from_slice(&be32(cap_len));
rec[12..16].copy_from_slice(&be32(cap_len));
// Write: rec hdr, dir flag (as 4 BE bytes), then H4 bytes
w.write_all(&rec).await?;
w.write_all(&be32(dir)).await?;
w.write_all(h4_payload).await?;
Ok(())
}

83
tools/src/pcapd.rs Normal file
View File

@@ -0,0 +1,83 @@
// Jackson Coxson
use clap::{Arg, Command};
use idevice::{
IdeviceService,
pcapd::{PcapFileWriter, PcapdClient},
};
mod common;
mod pcap;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("pcapd")
.about("Capture IP packets")
.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),
)
.arg(
Arg::new("out")
.long("out")
.value_name("PCAP")
.help("Write PCAP to this file (use '-' for stdout)"),
)
.get_matches();
if matches.get_flag("about") {
println!("bt_packet_logger - capture bluetooth packets");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let udid = matches.get_one::<String>("udid");
let out = matches.get_one::<String>("out").map(String::to_owned);
let provider = match common::get_provider(udid, None, None, "pcapd-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut logger_client = PcapdClient::connect(&*provider)
.await
.expect("Failed to connect to pcapd! This service is only available over USB!");
logger_client.next_packet().await.unwrap();
// Open output (default to stdout if --out omitted)
let mut out_writer = match out.as_deref() {
Some(path) => Some(
PcapFileWriter::new(tokio::fs::File::create(path).await.expect("open pcap"))
.await
.expect("write header"),
),
_ => None,
};
println!("Starting packet stream");
loop {
let packet = logger_client
.next_packet()
.await
.expect("failed to read next packet");
if let Some(writer) = &mut out_writer {
writer.write_packet(&packet).await.expect("write packet");
} else {
println!("{packet:?}");
}
}
}

76
tools/src/preboard.rs Normal file
View File

@@ -0,0 +1,76 @@
// Jackson Coxson
use clap::{Arg, Command};
use idevice::{IdeviceService, preboard_service::PreboardServiceClient};
mod common;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("preboard")
.about("Mess with developer mode")
.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)")
.index(1),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(Command::new("create").about("Create a stashbag??"))
.subcommand(Command::new("commit").about("Commit a stashbag??"))
.get_matches();
if matches.get_flag("about") {
println!("preboard - no idea what this does");
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, "amfi-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut pc = PreboardServiceClient::connect(&*provider)
.await
.expect("Failed to connect to Preboard");
if matches.subcommand_matches("create").is_some() {
pc.create_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
.await
.expect("Failed to create");
} else if matches.subcommand_matches("commit").is_some() {
pc.commit_stashbag(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
.await
.expect("Failed to create");
} else {
eprintln!("Invalid usage, pass -h for help");
}
return;
}

View File

@@ -1,10 +1,8 @@
// Jackson Coxson
use clap::{Arg, Command};
use idevice::{
core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake, tcp::stream::AdapterStream,
IdeviceService, RsdService,
};
use idevice::services::lockdown::LockdownClient;
use idevice::{IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake};
mod common;
@@ -74,30 +72,70 @@ async fn main() {
}
};
let proxy = CoreDeviceProxy::connect(&*provider)
let mut rs_client_opt: Option<
idevice::dvt::remote_server::RemoteServerClient<Box<dyn idevice::ReadWrite>>,
> = None;
if let Ok(proxy) = CoreDeviceProxy::connect(&*provider).await {
let rsd_port = proxy.handshake.server_rsd_port;
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
let mut adapter = adapter.to_async_handle();
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
// Make the connection to RemoteXPC (iOS 17+)
let mut handshake = RsdHandshake::new(stream).await.unwrap();
let mut rs_client = idevice::dvt::remote_server::RemoteServerClient::connect_rsd(
&mut adapter,
&mut handshake,
)
.await
.expect("no core proxy");
let rsd_port = proxy.handshake.server_rsd_port;
.expect("no connect");
rs_client.read_message(0).await.expect("no read??");
rs_client_opt = Some(rs_client);
}
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
let stream = AdapterStream::connect(&mut adapter, rsd_port)
.await
.expect("no RSD connect");
// Make the connection to RemoteXPC
let mut handshake = RsdHandshake::new(stream).await.unwrap();
let mut rs_client =
idevice::dvt::remote_server::RemoteServerClient::connect_rsd(&mut adapter, &mut handshake)
let mut rs_client = if let Some(c) = rs_client_opt {
c
} else {
// Read iOS version to decide whether we can fallback to remoteserver
let mut lockdown = LockdownClient::connect(&*provider)
.await
.expect("no connect");
.expect("lockdown connect failed");
lockdown
.start_session(&provider.get_pairing_file().await.expect("pairing file"))
.await
.expect("lockdown start_session failed");
let pv = lockdown
.get_value(Some("ProductVersion"), None)
.await
.ok()
.and_then(|v| v.as_string().map(|s| s.to_string()))
.unwrap_or_default();
let major: u32 = pv
.split('.')
.next()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if major >= 17 {
// iOS 17+ with no CoreDeviceProxy: do not attempt remoteserver (would return InvalidService)
panic!("iOS {pv} detected and CoreDeviceProxy unavailable. RemoteXPC tunnel required.");
}
// iOS 16 and earlier: fallback to Lockdown remoteserver (or DVTSecureSocketProxy)
idevice::dvt::remote_server::RemoteServerClient::connect(&*provider)
.await
.expect("failed to connect to Instruments Remote Server over Lockdown (iOS16-). Ensure Developer Disk Image is mounted.")
};
// Note: On both transports, protocol requires reading the initial message on root channel (0)
rs_client.read_message(0).await.expect("no read??");
let mut pc_client = idevice::dvt::process_control::ProcessControlClient::new(&mut rs_client)
.await
.unwrap();
let pid = pc_client
.launch_app(bundle_id, None, None, true, false)
.launch_app(bundle_id, None, None, false, false)
.await
.expect("no launch??");
pc_client

View File

@@ -3,8 +3,8 @@
use clap::{Arg, Command};
use idevice::{
core_device_proxy::CoreDeviceProxy, tcp::stream::AdapterStream, xpc::RemoteXpcClient,
IdeviceService,
IdeviceService, core_device_proxy::CoreDeviceProxy, rsd::RsdHandshake,
tcp::stream::AdapterStream,
};
mod common;
@@ -66,13 +66,12 @@ async fn main() {
let rsd_port = proxy.handshake.server_rsd_port;
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
adapter.pcap("new_xpc.pcap").await.unwrap();
let conn = AdapterStream::connect(&mut adapter, rsd_port)
let stream = AdapterStream::connect(&mut adapter, rsd_port)
.await
.expect("no RSD connect");
// Make the connection to RemoteXPC
let mut client = RemoteXpcClient::new(Box::new(conn)).await.unwrap();
println!("{:#?}", client.do_handshake().await);
let handshake = RsdHandshake::new(stream).await.unwrap();
println!("{:#?}", handshake.services);
}

View File

@@ -2,9 +2,8 @@
use clap::{Arg, Command};
use idevice::{
core_device_proxy::CoreDeviceProxy, pretty_print_dictionary,
restore_service::RestoreServiceClient, rsd::RsdHandshake, tcp::stream::AdapterStream,
IdeviceService, RsdService,
IdeviceService, RsdService, core_device_proxy::CoreDeviceProxy, pretty_print_dictionary,
restore_service::RestoreServiceClient, rsd::RsdHandshake,
};
mod common;
@@ -52,7 +51,9 @@ async fn main() {
.get_matches();
if matches.get_flag("about") {
println!("mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary.");
println!(
"mounter - query and manage images mounted on a device. Reimplementation of libimobiledevice's binary."
);
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
@@ -75,11 +76,9 @@ async fn main() {
.expect("no core proxy");
let rsd_port = proxy.handshake.server_rsd_port;
let mut adapter = proxy.create_software_tunnel().expect("no software tunnel");
let stream = AdapterStream::connect(&mut adapter, rsd_port)
.await
.expect("no RSD connect");
let adapter = proxy.create_software_tunnel().expect("no software tunnel");
let mut adapter = adapter.to_async_handle();
let stream = adapter.connect(rsd_port).await.expect("no RSD connect");
// Make the connection to RemoteXPC
let mut handshake = RsdHandshake::new(stream).await.unwrap();

View File

@@ -1,7 +1,7 @@
// Jackson Coxson
use clap::{Arg, Command};
use idevice::{syslog_relay::SyslogRelayClient, IdeviceService};
use idevice::{IdeviceService, syslog_relay::SyslogRelayClient};
mod common;