mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
tools: add iproxy (#37)
* tools: add iproxy * cargofmt and clippy cleanup --------- Co-authored-by: Jackson Coxson <jkcoxson@gmail.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1060,7 +1060,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idevice"
|
name = "idevice"
|
||||||
version = "0.1.48"
|
version = "0.1.49"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async_zip",
|
"async_zip",
|
||||||
|
|||||||
@@ -143,6 +143,10 @@ path = "src/notifications.rs"
|
|||||||
name = "installcoordination_proxy"
|
name = "installcoordination_proxy"
|
||||||
path = "src/installcoordination_proxy.rs"
|
path = "src/installcoordination_proxy.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "iproxy"
|
||||||
|
path = "src/iproxy.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idevice = { path = "../idevice", features = ["full"], default-features = false }
|
idevice = { path = "../idevice", features = ["full"], default-features = false }
|
||||||
tokio = { version = "1.43", features = ["full"] }
|
tokio = { version = "1.43", features = ["full"] }
|
||||||
|
|||||||
408
tools/src/iproxy.rs
Normal file
408
tools/src/iproxy.rs
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
// iproxy - Proxy tool to forward local TCP ports to specified ports on iOS devices
|
||||||
|
// Based on libusbmuxd/tools/iproxy.c implementation
|
||||||
|
|
||||||
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use clap::{Arg, Command};
|
||||||
|
use idevice::{
|
||||||
|
ReadWrite,
|
||||||
|
usbmuxd::{Connection, UsbmuxdAddr, UsbmuxdDevice},
|
||||||
|
};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
const BUFFER_SIZE: usize = 32768;
|
||||||
|
|
||||||
|
/// Port pair configuration
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct PortPair {
|
||||||
|
/// Local listening port
|
||||||
|
local_port: u16,
|
||||||
|
/// Device port
|
||||||
|
device_port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lookup options
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct LookupOptions {
|
||||||
|
/// Whether to lookup USB devices
|
||||||
|
usb: bool,
|
||||||
|
/// Whether to lookup network devices
|
||||||
|
network: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LookupOptions {
|
||||||
|
fn new(usb: bool, network: bool) -> Self {
|
||||||
|
if !usb && !network {
|
||||||
|
// Default to USB
|
||||||
|
Self {
|
||||||
|
usb: true,
|
||||||
|
network: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self { usb, network }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client connection data
|
||||||
|
struct ClientData {
|
||||||
|
/// Device UDID (optional)
|
||||||
|
udid: Option<String>,
|
||||||
|
/// Device port
|
||||||
|
device_port: u16,
|
||||||
|
/// Lookup options
|
||||||
|
lookup_opts: LookupOptions,
|
||||||
|
/// usbmuxd address
|
||||||
|
usbmuxd_addr: UsbmuxdAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a single client connection
|
||||||
|
async fn handle_client(
|
||||||
|
mut client_stream: TcpStream,
|
||||||
|
client_data: Arc<ClientData>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let client_addr = client_stream.peer_addr()?;
|
||||||
|
info!("Accepted new connection from {}", client_addr);
|
||||||
|
|
||||||
|
// Get device
|
||||||
|
let device = get_device(&client_data).await?;
|
||||||
|
|
||||||
|
// Connect to device
|
||||||
|
let device_stream =
|
||||||
|
connect_to_device(&device, client_data.device_port, &client_data.usbmuxd_addr).await?;
|
||||||
|
|
||||||
|
// Bidirectional data forwarding
|
||||||
|
let (mut client_read, mut client_write) = client_stream.split();
|
||||||
|
let (mut device_read, mut device_write) = tokio::io::split(device_stream);
|
||||||
|
|
||||||
|
let client_to_device = async {
|
||||||
|
let mut buffer = vec![0u8; BUFFER_SIZE];
|
||||||
|
loop {
|
||||||
|
match client_read.read(&mut buffer).await {
|
||||||
|
Ok(0) => {
|
||||||
|
debug!("Client connection closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(n) => {
|
||||||
|
if let Err(e) = device_write.write_all(&buffer[..n]).await {
|
||||||
|
error!("Failed to write to device: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to read from client: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let device_to_client = async {
|
||||||
|
let mut buffer = vec![0u8; BUFFER_SIZE];
|
||||||
|
loop {
|
||||||
|
match device_read.read(&mut buffer).await {
|
||||||
|
Ok(0) => {
|
||||||
|
debug!("Device connection closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(n) => {
|
||||||
|
if let Err(e) = client_write.write_all(&buffer[..n]).await {
|
||||||
|
error!("Failed to write to client: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to read from device: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for either direction to finish
|
||||||
|
tokio::select! {
|
||||||
|
_ = client_to_device => {},
|
||||||
|
_ = device_to_client => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Connection {} closed", client_addr);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get device
|
||||||
|
async fn get_device(
|
||||||
|
client_data: &ClientData,
|
||||||
|
) -> Result<UsbmuxdDevice, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let mut usbmuxd = client_data.usbmuxd_addr.connect(1).await?;
|
||||||
|
|
||||||
|
if let Some(udid) = &client_data.udid {
|
||||||
|
// Find device by UDID
|
||||||
|
let device = usbmuxd.get_device(udid).await?;
|
||||||
|
Ok(device)
|
||||||
|
} else {
|
||||||
|
// Get all devices and filter by lookup options
|
||||||
|
let devices = usbmuxd.get_devices().await?;
|
||||||
|
|
||||||
|
if devices.is_empty() {
|
||||||
|
return Err("No connected devices found".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer USB devices (if allowed), otherwise select network devices
|
||||||
|
for device in &devices {
|
||||||
|
if client_data.lookup_opts.usb && device.connection_type == Connection::Usb {
|
||||||
|
return Ok(device.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for device in &devices {
|
||||||
|
if client_data.lookup_opts.network
|
||||||
|
&& let Connection::Network(_) = device.connection_type
|
||||||
|
{
|
||||||
|
return Ok(device.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no matching device found, return first device
|
||||||
|
Ok(devices[0].clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connect to device
|
||||||
|
async fn connect_to_device(
|
||||||
|
device: &UsbmuxdDevice,
|
||||||
|
port: u16,
|
||||||
|
usbmuxd_addr: &UsbmuxdAddr,
|
||||||
|
) -> Result<Box<dyn ReadWrite>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
match &device.connection_type {
|
||||||
|
Connection::Network(ip_addr) => {
|
||||||
|
info!(
|
||||||
|
"Requesting connection to NETWORK device {} (serial: {}), port {}",
|
||||||
|
ip_addr, device.udid, port
|
||||||
|
);
|
||||||
|
let socket_addr = SocketAddr::new(*ip_addr, port);
|
||||||
|
let stream = TcpStream::connect(socket_addr).await?;
|
||||||
|
Ok(Box::new(stream) as Box<dyn ReadWrite>)
|
||||||
|
}
|
||||||
|
Connection::Usb => {
|
||||||
|
info!(
|
||||||
|
"Requesting connection to USB device handle {} (serial: {}), port {}",
|
||||||
|
device.device_id, device.udid, port
|
||||||
|
);
|
||||||
|
let conn = usbmuxd_addr.connect(device.device_id).await?;
|
||||||
|
let idevice = conn
|
||||||
|
.connect_to_device(device.device_id, port, "iproxy")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Extract underlying socket from Idevice
|
||||||
|
match idevice.get_socket() {
|
||||||
|
Some(socket) => Ok(socket),
|
||||||
|
None => Err("Unable to get device socket".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Connection::Unknown(desc) => Err(format!("Unsupported connection type: {}", desc).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse port pair
|
||||||
|
fn parse_port_pair(arg: &str) -> Result<PortPair, String> {
|
||||||
|
let parts: Vec<&str> = arg.split(':').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(format!("Invalid port pair format: {}", arg));
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_port = parts[0]
|
||||||
|
.parse::<u16>()
|
||||||
|
.map_err(|_| format!("Invalid local port: {}", parts[0]))?;
|
||||||
|
let device_port = parts[1]
|
||||||
|
.parse::<u16>()
|
||||||
|
.map_err(|_| format!("Invalid device port: {}", parts[1]))?;
|
||||||
|
|
||||||
|
if local_port == 0 {
|
||||||
|
return Err("Local port cannot be 0".into());
|
||||||
|
}
|
||||||
|
if device_port == 0 {
|
||||||
|
return Err("Device port cannot be 0".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PortPair {
|
||||||
|
local_port,
|
||||||
|
device_port,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start listener
|
||||||
|
async fn start_listener(
|
||||||
|
port_pair: PortPair,
|
||||||
|
source_addr: Option<IpAddr>,
|
||||||
|
client_data: Arc<ClientData>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let bind_addr = SocketAddr::new(
|
||||||
|
source_addr.unwrap_or_else(|| "127.0.0.1".parse().unwrap()),
|
||||||
|
port_pair.local_port,
|
||||||
|
);
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(bind_addr).await?;
|
||||||
|
info!(
|
||||||
|
"Creating listening port {} for device port {}",
|
||||||
|
port_pair.local_port, port_pair.device_port
|
||||||
|
);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match listener.accept().await {
|
||||||
|
Ok((stream, _addr)) => {
|
||||||
|
debug!(
|
||||||
|
"New connection: {} -> {}",
|
||||||
|
port_pair.local_port, port_pair.device_port
|
||||||
|
);
|
||||||
|
let client_data = Arc::clone(&client_data);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = handle_client(stream, client_data).await {
|
||||||
|
error!("Failed to handle client connection: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to accept connection: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let matches = Command::new("iproxy")
|
||||||
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
|
.about("Proxy that binds local TCP ports to be forwarded to the specified ports on a usbmux device")
|
||||||
|
.arg(
|
||||||
|
Arg::new("udid")
|
||||||
|
.short('u')
|
||||||
|
.long("udid")
|
||||||
|
.value_name("UDID")
|
||||||
|
.help("Target specific device by UDID"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("network")
|
||||||
|
.short('n')
|
||||||
|
.long("network")
|
||||||
|
.action(clap::ArgAction::SetTrue)
|
||||||
|
.help("Connect to network device"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("local")
|
||||||
|
.short('l')
|
||||||
|
.long("local")
|
||||||
|
.action(clap::ArgAction::SetTrue)
|
||||||
|
.help("Connect to USB device (default)"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("source")
|
||||||
|
.short('s')
|
||||||
|
.long("source")
|
||||||
|
.value_name("ADDR")
|
||||||
|
.help("Source address for listening socket (default 127.0.0.1)"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("port_pairs")
|
||||||
|
.value_name("LOCAL_PORT:DEVICE_PORT")
|
||||||
|
.help("Port pairs in LOCAL_PORT:DEVICE_PORT format")
|
||||||
|
.required(true)
|
||||||
|
.num_args(1..),
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
// Parse lookup options
|
||||||
|
let usb = matches.get_flag("local");
|
||||||
|
let network = matches.get_flag("network");
|
||||||
|
let lookup_opts = LookupOptions::new(usb, network);
|
||||||
|
|
||||||
|
// Parse UDID
|
||||||
|
let udid = matches.get_one::<String>("udid").cloned();
|
||||||
|
|
||||||
|
// Parse source address
|
||||||
|
let source_addr = matches.get_one::<String>("source").map(|addr_str| {
|
||||||
|
addr_str
|
||||||
|
.parse::<IpAddr>()
|
||||||
|
.unwrap_or_else(|_| panic!("Invalid source address: {}", addr_str))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse port pairs
|
||||||
|
let port_pairs_args: Vec<&String> = matches.get_many::<String>("port_pairs").unwrap().collect();
|
||||||
|
|
||||||
|
let mut port_pairs = Vec::new();
|
||||||
|
|
||||||
|
// Support old format: two separate arguments for port pair
|
||||||
|
if port_pairs_args.len() == 2
|
||||||
|
&& !port_pairs_args[0].contains(':')
|
||||||
|
&& !port_pairs_args[1].contains(':')
|
||||||
|
{
|
||||||
|
let local_port = port_pairs_args[0]
|
||||||
|
.parse::<u16>()
|
||||||
|
.unwrap_or_else(|_| panic!("Invalid local port: {}", port_pairs_args[0]));
|
||||||
|
let device_port = port_pairs_args[1]
|
||||||
|
.parse::<u16>()
|
||||||
|
.unwrap_or_else(|_| panic!("Invalid device port: {}", port_pairs_args[1]));
|
||||||
|
|
||||||
|
if local_port == 0 {
|
||||||
|
eprintln!("ERROR: Local port cannot be 0");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
if device_port == 0 {
|
||||||
|
eprintln!("ERROR: Device port cannot be 0");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
port_pairs.push(PortPair {
|
||||||
|
local_port,
|
||||||
|
device_port,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// New format: colon-separated port pairs
|
||||||
|
for arg in port_pairs_args {
|
||||||
|
match parse_port_pair(arg) {
|
||||||
|
Ok(pair) => port_pairs.push(pair),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("ERROR: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if port_pairs.len() > 16 {
|
||||||
|
eprintln!("ERROR: Too many port pairs, maximum is 16");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get usbmuxd address
|
||||||
|
let usbmuxd_addr = UsbmuxdAddr::from_env_var().unwrap_or_default();
|
||||||
|
|
||||||
|
// Start listener for each port pair
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
|
for port_pair in port_pairs {
|
||||||
|
let client_data = Arc::new(ClientData {
|
||||||
|
udid: udid.clone(),
|
||||||
|
device_port: port_pair.device_port,
|
||||||
|
lookup_opts,
|
||||||
|
usbmuxd_addr: usbmuxd_addr.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let task = tokio::spawn(start_listener(port_pair, source_addr, client_data));
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Waiting for connection...");
|
||||||
|
|
||||||
|
// Wait for all tasks to complete (they will run indefinitely)
|
||||||
|
for task in tasks {
|
||||||
|
if let Err(e) = task.await {
|
||||||
|
error!("Task failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user