Initial afc support

This commit is contained in:
Jackson Coxson
2025-04-05 01:25:56 -06:00
parent fef30f3783
commit 88d031edd7
8 changed files with 498 additions and 1 deletions

View File

@@ -43,6 +43,7 @@ tun-rs = { version = "2.0.8", features = ["async_tokio"] }
bytes = "1.10.1" bytes = "1.10.1"
[features] [features]
afc = []
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
debug_proxy = [] debug_proxy = []
dvt = ["dep:byteorder", "dep:ns-keyed-archive"] dvt = ["dep:byteorder", "dep:ns-keyed-archive"]
@@ -66,6 +67,7 @@ xpc = [
"dep:json", "dep:json",
] ]
full = [ full = [
"afc",
"core_device_proxy", "core_device_proxy",
"debug_proxy", "debug_proxy",
"dvt", "dvt",
@@ -80,7 +82,7 @@ full = [
"tunnel_tcp_stack", "tunnel_tcp_stack",
"tss", "tss",
"tunneld", "tunneld",
"sbservices" "sbservices",
] ]
# Why: https://github.com/rust-lang/cargo/issues/1197 # Why: https://github.com/rust-lang/cargo/issues/1197

107
idevice/src/afc/errors.rs Normal file
View File

@@ -0,0 +1,107 @@
// Jackson Coxson
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
#[repr(C)]
pub enum AfcError {
Success = 0,
UnknownError = 1,
OpHeaderInvalid = 2,
NoResources = 3,
ReadError = 4,
WriteError = 5,
UnknownPacketType = 6,
InvalidArg = 7,
ObjectNotFound = 8,
ObjectIsDir = 9,
PermDenied = 10,
ServiceNotConnected = 11,
OpTimeout = 12,
TooMuchData = 13,
EndOfData = 14,
OpNotSupported = 15,
ObjectExists = 16,
ObjectBusy = 17,
NoSpaceLeft = 18,
OpWouldBlock = 19,
IoError = 20,
OpInterrupted = 21,
OpInProgress = 22,
InternalError = 23,
MuxError = 30,
NoMem = 31,
NotEnoughData = 32,
DirNotEmpty = 33,
}
impl std::fmt::Display for AfcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let description = match self {
AfcError::Success => "Success",
AfcError::UnknownError => "Unknown error",
AfcError::OpHeaderInvalid => "Operation header invalid",
AfcError::NoResources => "No resources available",
AfcError::ReadError => "Read error",
AfcError::WriteError => "Write error",
AfcError::UnknownPacketType => "Unknown packet type",
AfcError::InvalidArg => "Invalid argument",
AfcError::ObjectNotFound => "Object not found",
AfcError::ObjectIsDir => "Object is a directory",
AfcError::PermDenied => "Permission denied",
AfcError::ServiceNotConnected => "Service not connected",
AfcError::OpTimeout => "Operation timed out",
AfcError::TooMuchData => "Too much data",
AfcError::EndOfData => "End of data",
AfcError::OpNotSupported => "Operation not supported",
AfcError::ObjectExists => "Object already exists",
AfcError::ObjectBusy => "Object is busy",
AfcError::NoSpaceLeft => "No space left",
AfcError::OpWouldBlock => "Operation would block",
AfcError::IoError => "I/O error",
AfcError::OpInterrupted => "Operation interrupted",
AfcError::OpInProgress => "Operation in progress",
AfcError::InternalError => "Internal error",
AfcError::MuxError => "Multiplexer error",
AfcError::NoMem => "Out of memory",
AfcError::NotEnoughData => "Not enough data",
AfcError::DirNotEmpty => "Directory not empty",
};
write!(f, "{}", description)
}
}
impl From<u64> for AfcError {
fn from(value: u64) -> Self {
match value {
0 => Self::Success,
1 => Self::UnknownError,
2 => Self::OpHeaderInvalid,
3 => Self::NoResources,
4 => Self::ReadError,
5 => Self::WriteError,
6 => Self::UnknownPacketType,
7 => Self::InvalidArg,
8 => Self::ObjectNotFound,
9 => Self::ObjectIsDir,
10 => Self::PermDenied,
11 => Self::ServiceNotConnected,
12 => Self::OpTimeout,
13 => Self::TooMuchData,
14 => Self::EndOfData,
15 => Self::OpNotSupported,
16 => Self::ObjectExists,
17 => Self::ObjectBusy,
18 => Self::NoSpaceLeft,
19 => Self::OpWouldBlock,
20 => Self::IoError,
21 => Self::OpInterrupted,
22 => Self::OpInProgress,
23 => Self::InternalError,
30 => Self::MuxError,
31 => Self::NoMem,
32 => Self::NotEnoughData,
33 => Self::DirNotEmpty,
_ => Self::UnknownError, // fallback for unknown codes
}
}
}

107
idevice/src/afc/mod.rs Normal file
View File

@@ -0,0 +1,107 @@
// Jackson Coxson
use errors::AfcError;
use opcode::AfcOpcode;
use packet::{AfcPacket, AfcPacketHeader};
use crate::{lockdownd::LockdowndClient, Idevice, IdeviceError, IdeviceService};
pub mod errors;
pub mod opcode;
pub mod packet;
pub const MAGIC: u64 = 0x4141504c36414643;
pub struct AfcClient {
pub idevice: Idevice,
package_number: u64,
}
impl IdeviceService for AfcClient {
fn service_name() -> &'static str {
"com.apple.afc"
}
async fn connect(
provider: &dyn crate::provider::IdeviceProvider,
) -> Result<Self, IdeviceError> {
let mut lockdown = LockdowndClient::connect(provider).await?;
lockdown
.start_session(&provider.get_pairing_file().await?)
.await?;
let (port, ssl) = lockdown.start_service(Self::service_name()).await?;
let mut idevice = provider.connect(port).await?;
if ssl {
idevice
.start_session(&provider.get_pairing_file().await?)
.await?;
}
Ok(Self {
idevice,
package_number: 0,
})
}
}
impl AfcClient {
pub fn new(idevice: Idevice) -> Self {
Self {
idevice,
package_number: 0,
}
}
pub async fn list(&mut self, path: impl Into<String>) -> Result<Vec<String>, IdeviceError> {
let path = path.into();
let header_payload = path.as_bytes().to_vec();
let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN;
let header = AfcPacketHeader {
magic: MAGIC,
entire_len: header_len, // it's the same since the payload is empty for this
header_payload_len: header_len,
packet_num: self.package_number,
operation: AfcOpcode::ReadDir,
};
self.package_number += 1;
let packet = AfcPacket {
header,
header_payload,
payload: Vec::new(),
};
self.send(packet).await?;
let res = self.read().await?;
let strings: Vec<String> = res
.payload
.split(|b| *b == 0)
.filter(|s| !s.is_empty())
.map(|s| String::from_utf8_lossy(s).into_owned())
.collect();
Ok(strings)
}
pub async fn read(&mut self) -> Result<AfcPacket, IdeviceError> {
let res = AfcPacket::read(&mut self.idevice).await?;
if res.header.operation == AfcOpcode::Status {
if res.header_payload.len() < 8 {
log::error!("AFC returned error opcode, but not a code");
return Err(IdeviceError::UnexpectedResponse);
}
let code = u64::from_le_bytes(res.header_payload[..8].try_into().unwrap());
return Err(IdeviceError::Afc(AfcError::from(code)));
}
Ok(res)
}
pub async fn send(&mut self, packet: AfcPacket) -> Result<(), IdeviceError> {
let packet = packet.serialize();
self.idevice.send_raw(&packet).await?;
Ok(())
}
}

83
idevice/src/afc/opcode.rs Normal file
View File

@@ -0,0 +1,83 @@
// Jackson Coxson
#[derive(Clone, Debug, PartialEq, Eq)]
#[repr(u64)]
pub enum AfcOpcode {
Status = 0x00000001,
Data = 0x00000002, // Data
ReadDir = 0x00000003, // ReadDir
ReadFile = 0x00000004, // ReadFile
WriteFile = 0x00000005, // WriteFile
WritePart = 0x00000006, // WritePart
Truncate = 0x00000007, // TruncateFile
RemovePath = 0x00000008, // RemovePath
MakeDir = 0x00000009, // MakeDir
GetFileInfo = 0x0000000a, // GetFileInfo
GetDevInfo = 0x0000000b, // GetDeviceInfo
WriteFileAtom = 0x0000000c, // WriteFileAtomic (tmp file+rename)
FileOpen = 0x0000000d, // FileRefOpen
FileOpenRes = 0x0000000e, // FileRefOpenResult
Read = 0x0000000f, // FileRefRead
Write = 0x00000010, // FileRefWrite
FileSeek = 0x00000011, // FileRefSeek
FileTell = 0x00000012, // FileRefTell
FileTellRes = 0x00000013, // FileRefTellResult
FileClose = 0x00000014, // FileRefClose
FileSetSize = 0x00000015, // FileRefSetFileSize (ftruncate)
GetConInfo = 0x00000016, // GetConnectionInfo
SetConOptions = 0x00000017, // SetConnectionOptions
RenamePath = 0x00000018, // RenamePath
SetFsBs = 0x00000019, // SetFSBlockSize (0x800000)
SetSocketBs = 0x0000001A, // SetSocketBlockSize (0x800000)
FileLock = 0x0000001B, // FileRefLock
MakeLink = 0x0000001C, // MakeLink
SetFileTime = 0x0000001E, // Set st_mtime
}
pub enum AfcFopenMode {
RdOnly = 0x00000001, // r O_RDONLY
Rw = 0x00000002, // r+ O_RDWR | O_CREAT
WrOnly = 0x00000003, // w O_WRONLY | O_CREAT | O_TRUNC
Wr = 0x00000004, // w+ O_RDWR | O_CREAT | O_TRUNC
Append = 0x00000005, // a O_WRONLY | O_APPEND | O_CREAT
RdAppend = 0x00000006, // a+ O_RDWR | O_APPEND | O_CREAT
}
impl TryFrom<u64> for AfcOpcode {
type Error = ();
fn try_from(value: u64) -> Result<Self, Self::Error> {
match value {
0x00000001 => Ok(Self::Status),
0x00000002 => Ok(Self::Data),
0x00000003 => Ok(Self::ReadDir),
0x00000004 => Ok(Self::ReadFile),
0x00000005 => Ok(Self::WriteFile),
0x00000006 => Ok(Self::WritePart),
0x00000007 => Ok(Self::Truncate),
0x00000008 => Ok(Self::RemovePath),
0x00000009 => Ok(Self::MakeDir),
0x0000000a => Ok(Self::GetFileInfo),
0x0000000b => Ok(Self::GetDevInfo),
0x0000000c => Ok(Self::WriteFileAtom),
0x0000000d => Ok(Self::FileOpen),
0x0000000e => Ok(Self::FileOpenRes),
0x0000000f => Ok(Self::Read),
0x00000010 => Ok(Self::Write),
0x00000011 => Ok(Self::FileSeek),
0x00000012 => Ok(Self::FileTell),
0x00000013 => Ok(Self::FileTellRes),
0x00000014 => Ok(Self::FileClose),
0x00000015 => Ok(Self::FileSetSize),
0x00000016 => Ok(Self::GetConInfo),
0x00000017 => Ok(Self::SetConOptions),
0x00000018 => Ok(Self::RenamePath),
0x00000019 => Ok(Self::SetFsBs),
0x0000001A => Ok(Self::SetSocketBs),
0x0000001B => Ok(Self::FileLock),
0x0000001C => Ok(Self::MakeLink),
0x0000001E => Ok(Self::SetFileTime),
_ => Err(()),
}
}
}

98
idevice/src/afc/packet.rs Normal file
View File

@@ -0,0 +1,98 @@
// Jackson Coxson
use log::debug;
use crate::{Idevice, IdeviceError};
use super::opcode::AfcOpcode;
#[derive(Clone, Debug)]
pub struct AfcPacketHeader {
pub magic: u64,
pub entire_len: u64,
pub header_payload_len: u64,
pub packet_num: u64,
pub operation: AfcOpcode,
}
#[derive(Clone, Debug)]
pub struct AfcPacket {
pub header: AfcPacketHeader,
pub header_payload: Vec<u8>,
pub payload: Vec<u8>,
}
impl AfcPacketHeader {
pub const LEN: u64 = 40;
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(Self::LEN as usize);
res.extend_from_slice(&self.magic.to_le_bytes());
res.extend_from_slice(&self.entire_len.to_le_bytes());
res.extend_from_slice(&self.header_payload_len.to_le_bytes());
res.extend_from_slice(&self.packet_num.to_le_bytes());
res.extend_from_slice(&(self.operation.clone() as u64).to_le_bytes());
res
}
pub async fn read(reader: &mut Idevice) -> Result<Self, IdeviceError> {
let header_bytes = reader.read_raw(Self::LEN as usize).await?;
let mut chunks = header_bytes.chunks_exact(8);
let res = Self {
magic: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()),
entire_len: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()),
header_payload_len: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()),
packet_num: u64::from_le_bytes(chunks.next().unwrap().try_into().unwrap()),
operation: match AfcOpcode::try_from(u64::from_le_bytes(
chunks.next().unwrap().try_into().unwrap(),
)) {
Ok(o) => o,
Err(_) => {
return Err(IdeviceError::UnknownAfcOpcode);
}
},
};
if res.magic != super::MAGIC {
return Err(IdeviceError::InvalidAfcMagic);
}
Ok(res)
}
}
impl AfcPacket {
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::new();
res.extend_from_slice(&self.header.serialize());
res.extend_from_slice(&self.header_payload);
res.extend_from_slice(&self.payload);
res
}
pub async fn read(reader: &mut Idevice) -> Result<Self, IdeviceError> {
let header = AfcPacketHeader::read(reader).await?;
debug!("afc header: {header:?}");
let header_payload = reader
.read_raw((header.header_payload_len - AfcPacketHeader::LEN) as usize)
.await?;
let payload = if header.header_payload_len == header.entire_len {
Vec::new() // no payload
} else {
reader
.read_raw((header.entire_len - header.header_payload_len) as usize)
.await?
};
let res = Self {
header,
header_payload,
payload,
};
debug!("Recv afc: {res:?}");
Ok(res)
}
}

View File

@@ -1,5 +1,7 @@
// Jackson Coxson // Jackson Coxson
#[cfg(feature = "afc")]
pub mod afc;
#[cfg(feature = "core_device_proxy")] #[cfg(feature = "core_device_proxy")]
pub mod core_device_proxy; pub mod core_device_proxy;
#[cfg(feature = "debug_proxy")] #[cfg(feature = "debug_proxy")]
@@ -298,6 +300,18 @@ pub enum IdeviceError {
#[error("misagent operation failed")] #[error("misagent operation failed")]
MisagentFailure, MisagentFailure,
#[cfg(feature = "afc")]
#[error("afc error")]
Afc(#[from] afc::errors::AfcError),
#[cfg(feature = "afc")]
#[error("unknown afc opcode")]
UnknownAfcOpcode,
#[cfg(feature = "afc")]
#[error("invalid afc magic")]
InvalidAfcMagic,
#[cfg(any(feature = "tss", feature = "tunneld"))] #[cfg(any(feature = "tss", feature = "tunneld"))]
#[error("http reqwest error")] #[error("http reqwest error")]
Reqwest(#[from] reqwest::Error), Reqwest(#[from] reqwest::Error),

View File

@@ -57,6 +57,11 @@ path = "src/misagent.rs"
name = "location_simulation" name = "location_simulation"
path = "src/location_simulation.rs" path = "src/location_simulation.rs"
[[bin]]
name = "afc"
path = "src/afc.rs"
[dependencies] [dependencies]
idevice = { path = "../idevice", features = ["full"] } idevice = { path = "../idevice", features = ["full"] }
tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] } tokio = { version = "1.43", features = ["io-util", "macros", "time", "full"] }

81
tools/src/afc.rs Normal file
View File

@@ -0,0 +1,81 @@
// Jackson Coxson
use std::path::PathBuf;
use clap::{arg, value_parser, Arg, Command};
use idevice::{afc::AfcClient, misagent::MisagentClient, IdeviceService};
mod common;
#[tokio::main]
async fn main() {
env_logger::init();
let matches = Command::new("afc")
.about("Start a tunnel")
.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)"),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.subcommand(
Command::new("list")
.about("Lists the items in the directory")
.arg(Arg::new("path").required(true).index(1)),
)
.subcommand(
Command::new("remove")
.about("Remove a provisioning profile")
.arg(Arg::new("path").required(true).index(1)),
)
.get_matches();
if matches.get_flag("about") {
println!("afc");
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, "afc-jkcoxson").await {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return;
}
};
let mut afc_client = AfcClient::connect(&*provider)
.await
.expect("Unable to connect to misagent");
if let Some(matches) = matches.subcommand_matches("list") {
let path = matches.get_one::<String>("path").expect("No path passed");
let res = afc_client.list(path).await.expect("Failed to read dir");
println!("{path}\n{res:#?}");
} else if let Some(matches) = matches.subcommand_matches("remove") {
let path = matches.get_one::<String>("id").expect("No path passed");
} else {
eprintln!("Invalid usage, pass -h for help");
}
}