diff --git a/Cargo.lock b/Cargo.lock index 5f0b8d4..3fa22e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1060,7 +1060,7 @@ dependencies = [ [[package]] name = "idevice" -version = "0.1.48" +version = "0.1.49" dependencies = [ "async-stream", "async_zip", diff --git a/tools/Cargo.toml b/tools/Cargo.toml index 90713e9..9beaa5d 100644 --- a/tools/Cargo.toml +++ b/tools/Cargo.toml @@ -143,6 +143,10 @@ path = "src/notifications.rs" name = "installcoordination_proxy" path = "src/installcoordination_proxy.rs" +[[bin]] +name = "iproxy" +path = "src/iproxy.rs" + [dependencies] idevice = { path = "../idevice", features = ["full"], default-features = false } tokio = { version = "1.43", features = ["full"] } diff --git a/tools/src/iproxy.rs b/tools/src/iproxy.rs new file mode 100644 index 0000000..feddce4 --- /dev/null +++ b/tools/src/iproxy.rs @@ -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, + /// 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, +) -> Result<(), Box> { + 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> { + 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> { + 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) + } + 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 { + 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::() + .map_err(|_| format!("Invalid local port: {}", parts[0]))?; + let device_port = parts[1] + .parse::() + .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, + client_data: Arc, +) -> Result<(), Box> { + 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::("udid").cloned(); + + // Parse source address + let source_addr = matches.get_one::("source").map(|addr_str| { + addr_str + .parse::() + .unwrap_or_else(|_| panic!("Invalid source address: {}", addr_str)) + }); + + // Parse port pairs + let port_pairs_args: Vec<&String> = matches.get_many::("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::() + .unwrap_or_else(|_| panic!("Invalid local port: {}", port_pairs_args[0])); + let device_port = port_pairs_args[1] + .parse::() + .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); + } + } +}