mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 14:36:16 +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:
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