10 Commits

Author SHA1 Message Date
se2crid
e79d1c0498 fix(pair_rsd_ios): handle Linux mDNS link-local fallback (#73) 2026-02-18 12:18:40 -07:00
Jackson Coxson
0944708a8f Allow rppairing with underlying RemoteXPC connection 2026-02-18 09:09:43 -07:00
Jackson Coxson
7077e70e24 Send weird flags after opening reply stream 2026-02-17 07:41:17 -07:00
Jackson Coxson
7782df8bd9 Set RemoteXPC initial root ID to 0 2026-02-17 07:33:51 -07:00
Jackson Coxson
93d2f1b28c Merge master into rppairing-try2 2026-02-14 13:32:14 -07:00
Jackson Coxson
f8c5010b34 Start work on iOS rppairing 2025-12-31 16:12:47 -07:00
Jackson Coxson
637758ad7f Create pair_rsd_ios tool 2025-12-23 07:23:26 -07:00
Jackson Coxson
f5be1a000a Add more debug logging to rppairing 2025-12-19 00:01:28 -07:00
Jackson Coxson
d6e7b9aef4 Use idevice-srp crate 2025-12-18 21:32:40 -07:00
Jackson Coxson
4bea784260 Initial rppairing support 2025-12-18 21:21:30 -07:00
18 changed files with 2649 additions and 869 deletions

1264
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -76,7 +76,6 @@ To keep dependency bloat and compile time down, everything is contained in featu
| `tunneld` | Interface with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3)'s tunneld. | | `tunneld` | Interface with [pymobiledevice3](https://github.com/doronz88/pymobiledevice3)'s tunneld. |
| `usbmuxd` | Connect using the usbmuxd daemon.| | `usbmuxd` | Connect using the usbmuxd daemon.|
| `xpc` | Access protected services via XPC over RSD. | | `xpc` | Access protected services via XPC over RSD. |
| `notification_proxy` | Post and observe iOS notifications. |
### Planned/TODO ### Planned/TODO
@@ -86,7 +85,7 @@ Finish the following:
Implement the following: Implement the following:
- file_relay - notification_proxy
As this project is done in my free time within my busy schedule, there As this project is done in my free time within my busy schedule, there
is no ETA for any of these. Feel free to contribute or donate! is no ETA for any of these. Feel free to contribute or donate!

View File

@@ -1,50 +0,0 @@
// Jackson Coxson
#pragma once
#include <idevice++/bindings.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
#include <memory>
#include <string>
#include <vector>
namespace IdeviceFFI {
using CrashReportCopyMobilePtr =
std::unique_ptr<CrashReportCopyMobileHandle,
FnDeleter<CrashReportCopyMobileHandle, crash_report_client_free>>;
class CrashReportCopyMobile {
public:
// Factory: connect via Provider
static Result<CrashReportCopyMobile, FfiError> connect(Provider& provider);
// Factory: wrap an existing Idevice socket (consumes it on success)
static Result<CrashReportCopyMobile, FfiError> from_socket(Idevice&& socket);
// Static: flush crash reports from system storage
static Result<void, FfiError> flush(Provider& provider);
// Ops
Result<std::vector<std::string>, FfiError> ls(const char* dir_path = nullptr);
Result<std::vector<char>, FfiError> pull(const std::string& log_name);
Result<void, FfiError> remove(const std::string& log_name);
// RAII / moves
~CrashReportCopyMobile() noexcept = default;
CrashReportCopyMobile(CrashReportCopyMobile&&) noexcept = default;
CrashReportCopyMobile& operator=(CrashReportCopyMobile&&) noexcept = default;
CrashReportCopyMobile(const CrashReportCopyMobile&) = delete;
CrashReportCopyMobile& operator=(const CrashReportCopyMobile&) = delete;
CrashReportCopyMobileHandle* raw() const noexcept { return handle_.get(); }
static CrashReportCopyMobile adopt(CrashReportCopyMobileHandle* h) noexcept {
return CrashReportCopyMobile(h);
}
private:
explicit CrashReportCopyMobile(CrashReportCopyMobileHandle* h) noexcept : handle_(h) {}
CrashReportCopyMobilePtr handle_{};
};
} // namespace IdeviceFFI

View File

@@ -1,94 +0,0 @@
// Jackson Coxson
#include <cstring>
#include <idevice++/bindings.hpp>
#include <idevice++/crashreportcopymobile.hpp>
#include <idevice++/ffi.hpp>
#include <idevice++/provider.hpp>
namespace IdeviceFFI {
// -------- Factory Methods --------
Result<CrashReportCopyMobile, FfiError> CrashReportCopyMobile::connect(Provider& provider) {
CrashReportCopyMobileHandle* out = nullptr;
FfiError e(::crash_report_client_connect(provider.raw(), &out));
if (e) {
return Err(e);
}
return Ok(CrashReportCopyMobile::adopt(out));
}
Result<CrashReportCopyMobile, FfiError> CrashReportCopyMobile::from_socket(Idevice&& socket) {
CrashReportCopyMobileHandle* out = nullptr;
FfiError e(::crash_report_client_new(socket.raw(), &out));
if (e) {
return Err(e);
}
socket.release();
return Ok(CrashReportCopyMobile::adopt(out));
}
Result<void, FfiError> CrashReportCopyMobile::flush(Provider& provider) {
FfiError e(::crash_report_flush(provider.raw()));
if (e) {
return Err(e);
}
return Ok();
}
// -------- Ops --------
Result<std::vector<std::string>, FfiError>
CrashReportCopyMobile::ls(const char* dir_path) {
char** entries_raw = nullptr;
size_t count = 0;
FfiError e(::crash_report_client_ls(handle_.get(), dir_path, &entries_raw, &count));
if (e) {
return Err(e);
}
std::vector<std::string> result;
if (entries_raw) {
result.reserve(count);
for (size_t i = 0; i < count; ++i) {
if (entries_raw[i]) {
result.emplace_back(entries_raw[i]);
::idevice_string_free(entries_raw[i]);
}
}
std::free(entries_raw);
}
return Ok(std::move(result));
}
Result<std::vector<char>, FfiError>
CrashReportCopyMobile::pull(const std::string& log_name) {
uint8_t* data = nullptr;
size_t length = 0;
FfiError e(::crash_report_client_pull(handle_.get(), log_name.c_str(), &data, &length));
if (e) {
return Err(e);
}
std::vector<char> result;
if (data && length > 0) {
result.assign(reinterpret_cast<char*>(data), reinterpret_cast<char*>(data) + length);
::idevice_data_free(data, length);
}
return Ok(std::move(result));
}
Result<void, FfiError> CrashReportCopyMobile::remove(const std::string& log_name) {
FfiError e(::crash_report_client_remove(handle_.get(), log_name.c_str()));
if (e) {
return Err(e);
}
return Ok();
}
} // namespace IdeviceFFI

View File

@@ -1,325 +0,0 @@
// Jackson Coxson
use std::{
ffi::{CStr, c_char},
ptr::null_mut,
};
use idevice::{
IdeviceError, IdeviceService,
provider::IdeviceProvider,
services::crashreportcopymobile::{CrashReportCopyMobileClient, flush_reports},
};
use crate::{
IdeviceFfiError, IdeviceHandle, afc::AfcClientHandle, ffi_err, provider::IdeviceProviderHandle,
run_sync_local,
};
pub struct CrashReportCopyMobileHandle(pub CrashReportCopyMobileClient);
/// Automatically creates and connects to the crash report copy mobile service,
/// returning a client handle
///
/// # Arguments
/// * [`provider`] - An IdeviceProvider
/// * [`client`] - On success, will be set to point to a newly allocated handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `provider` must be a valid pointer to a handle allocated by this library
/// `client` must be a valid, non-null pointer to a location where the handle will be stored
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_connect(
provider: *mut IdeviceProviderHandle,
client: *mut *mut CrashReportCopyMobileHandle,
) -> *mut IdeviceFfiError {
if provider.is_null() || client.is_null() {
tracing::error!("Null pointer provided");
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res: Result<CrashReportCopyMobileClient, IdeviceError> = run_sync_local(async move {
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
CrashReportCopyMobileClient::connect(provider_ref).await
});
match res {
Ok(r) => {
let boxed = Box::new(CrashReportCopyMobileHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Creates a new CrashReportCopyMobile client from an existing Idevice connection
///
/// # Arguments
/// * [`socket`] - An IdeviceSocket handle
/// * [`client`] - On success, will be set to point to a newly allocated handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `socket` must be a valid pointer to a handle allocated by this library
/// `client` must be a valid, non-null pointer to a location where the handle will be stored
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_new(
socket: *mut IdeviceHandle,
client: *mut *mut CrashReportCopyMobileHandle,
) -> *mut IdeviceFfiError {
if socket.is_null() || client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let socket = unsafe { Box::from_raw(socket) }.0;
let r = CrashReportCopyMobileClient::new(socket);
let boxed = Box::new(CrashReportCopyMobileHandle(r));
unsafe { *client = Box::into_raw(boxed) };
null_mut()
}
/// Lists crash report files in the specified directory
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle
/// * [`dir_path`] - Optional directory path (NULL for root "/")
/// * [`entries`] - Will be set to point to an array of C strings
/// * [`count`] - Will be set to the number of entries
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// All pointers must be valid and non-null
/// `dir_path` may be NULL (defaults to root)
/// Caller must free the returned array with `afc_free_directory_entries`
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_ls(
client: *mut CrashReportCopyMobileHandle,
dir_path: *const c_char,
entries: *mut *mut *mut c_char,
count: *mut libc::size_t,
) -> *mut IdeviceFfiError {
if client.is_null() || entries.is_null() || count.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let path = if dir_path.is_null() {
None
} else {
match unsafe { CStr::from_ptr(dir_path) }.to_str() {
Ok(s) => Some(s),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
}
};
let res: Result<Vec<String>, IdeviceError> = run_sync_local(async {
let client_ref = unsafe { &mut (*client).0 };
client_ref.ls(path).await
});
match res {
Ok(items) => {
let c_strings = items
.into_iter()
.filter_map(|s| std::ffi::CString::new(s).ok())
.collect::<Vec<_>>();
let string_count = c_strings.len();
// Allocate array for char pointers (with NULL terminator)
let layout = std::alloc::Layout::array::<*mut c_char>(string_count + 1).unwrap();
let ptr = unsafe { std::alloc::alloc(layout) as *mut *mut c_char };
if ptr.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
for (i, cstring) in c_strings.into_iter().enumerate() {
let string_ptr = cstring.into_raw();
unsafe { *ptr.add(i) = string_ptr };
}
// NULL terminator
unsafe { *ptr.add(string_count) = std::ptr::null_mut() };
unsafe {
*entries = ptr;
*count = string_count;
}
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Downloads a crash report file from the device
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle
/// * [`log_name`] - Name of the log file to download (C string)
/// * [`data`] - Will be set to point to the file contents
/// * [`length`] - Will be set to the size of the data
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// All pointers must be valid and non-null
/// `log_name` must be a valid C string
/// Caller must free the returned data with `idevice_data_free`
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_pull(
client: *mut CrashReportCopyMobileHandle,
log_name: *const c_char,
data: *mut *mut u8,
length: *mut libc::size_t,
) -> *mut IdeviceFfiError {
if client.is_null() || log_name.is_null() || data.is_null() || length.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let name = match unsafe { CStr::from_ptr(log_name) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
};
let res: Result<Vec<u8>, IdeviceError> = run_sync_local(async {
let client_ref = unsafe { &mut (*client).0 };
client_ref.pull(name).await
});
match res {
Ok(file_data) => {
let len = file_data.len();
let mut boxed = file_data.into_boxed_slice();
unsafe {
*data = boxed.as_mut_ptr();
*length = len;
}
std::mem::forget(boxed);
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Removes a crash report file from the device
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle
/// * [`log_name`] - Name of the log file to remove (C string)
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `log_name` must be a valid C string
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_remove(
client: *mut CrashReportCopyMobileHandle,
log_name: *const c_char,
) -> *mut IdeviceFfiError {
if client.is_null() || log_name.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let name = match unsafe { CStr::from_ptr(log_name) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return ffi_err!(IdeviceError::FfiInvalidString),
};
let res = run_sync_local(async {
let client_ref = unsafe { &mut (*client).0 };
client_ref.remove(name).await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Converts this client to an AFC client for advanced file operations
///
/// # Arguments
/// * [`client`] - A valid CrashReportCopyMobile handle (will be consumed)
/// * [`afc_client`] - On success, will be set to an AFC client handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer (will be freed after this call)
/// `afc_client` must be a valid, non-null pointer where the new AFC client will be stored
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_to_afc(
client: *mut CrashReportCopyMobileHandle,
afc_client: *mut *mut AfcClientHandle,
) -> *mut IdeviceFfiError {
if client.is_null() || afc_client.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let crash_client = unsafe { Box::from_raw(client) }.0;
let afc = crash_client.to_afc_client();
let a = Box::into_raw(Box::new(AfcClientHandle(afc)));
unsafe { *afc_client = a };
null_mut()
}
/// Triggers a flush of crash logs from system storage
///
/// This connects to the crashreportmover service to move crash logs
/// into the AFC-accessible directory. Should be called before listing logs.
///
/// # Arguments
/// * [`provider`] - An IdeviceProvider
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `provider` must be a valid pointer to a handle allocated by this library
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_flush(
provider: *mut IdeviceProviderHandle,
) -> *mut IdeviceFfiError {
if provider.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let res = run_sync_local(async {
let provider_ref: &dyn IdeviceProvider = unsafe { &*(*provider).0 };
flush_reports(provider_ref).await
});
match res {
Ok(_) => null_mut(),
Err(e) => ffi_err!(e),
}
}
/// Frees a CrashReportCopyMobile client handle
///
/// # Arguments
/// * [`handle`] - The handle to free
///
/// # Safety
/// `handle` must be a valid pointer to the handle that was allocated by this library,
/// or NULL (in which case this function does nothing)
#[unsafe(no_mangle)]
pub unsafe extern "C" fn crash_report_client_free(handle: *mut CrashReportCopyMobileHandle) {
if !handle.is_null() {
tracing::debug!("Freeing crash_report_client");
let _ = unsafe { Box::from_raw(handle) };
}
}

View File

@@ -10,8 +10,6 @@ pub mod amfi;
pub mod core_device; pub mod core_device;
#[cfg(feature = "core_device_proxy")] #[cfg(feature = "core_device_proxy")]
pub mod core_device_proxy; pub mod core_device_proxy;
#[cfg(feature = "crashreportcopymobile")]
pub mod crashreportcopymobile;
#[cfg(feature = "debug_proxy")] #[cfg(feature = "debug_proxy")]
pub mod debug_proxy; pub mod debug_proxy;
#[cfg(feature = "diagnostics_relay")] #[cfg(feature = "diagnostics_relay")]

View File

@@ -157,74 +157,6 @@ pub unsafe extern "C" fn lockdownd_start_service(
} }
} }
/// Pairs with the device using lockdownd
///
/// # Arguments
/// * `client` - A valid LockdowndClient handle
/// * `host_id` - The host ID (null-terminated string)
/// * `system_buid` - The system BUID (null-terminated string)
/// * `pairing_file` - On success, will be set to point to a newly allocated IdevicePairingFile handle
///
/// # Returns
/// An IdeviceFfiError on error, null on success
///
/// # Safety
/// `client` must be a valid pointer to a handle allocated by this library
/// `host_id` must be a valid null-terminated string
/// `system_buid` must be a valid null-terminated string
/// `pairing_file` must be a valid, non-null pointer to a location where the handle will be stored
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lockdownd_pair(
client: *mut LockdowndClientHandle,
host_id: *const libc::c_char,
system_buid: *const libc::c_char,
host_name: *const libc::c_char,
pairing_file: *mut *mut IdevicePairingFile,
) -> *mut IdeviceFfiError {
if client.is_null() || host_id.is_null() || system_buid.is_null() {
return ffi_err!(IdeviceError::FfiInvalidArg);
}
let host_id = unsafe {
std::ffi::CStr::from_ptr(host_id)
.to_string_lossy()
.into_owned()
};
let system_buid = unsafe {
std::ffi::CStr::from_ptr(system_buid)
.to_string_lossy()
.into_owned()
};
let host_name = if host_name.is_null() {
None
} else {
Some(
match unsafe { std::ffi::CStr::from_ptr(host_name) }.to_str() {
Ok(v) => v,
Err(_) => {
return ffi_err!(IdeviceError::InvalidCString);
}
},
)
};
let res = run_sync_local(async move {
let client_ref = unsafe { &mut (*client).0 };
client_ref.pair(host_id, system_buid, host_name).await
});
match res {
Ok(pairing_file_res) => {
let boxed_pairing_file = Box::new(IdevicePairingFile(pairing_file_res));
unsafe { *pairing_file = Box::into_raw(boxed_pairing_file) };
null_mut()
}
Err(e) => ffi_err!(e),
}
}
/// Gets a value from lockdownd /// Gets a value from lockdownd
/// ///
/// # Arguments /// # Arguments

View File

@@ -21,7 +21,7 @@ tokio-openssl = { version = "0.6", optional = true }
openssl = { version = "0.10", optional = true } openssl = { version = "0.10", optional = true }
plist = { version = "1.8" } plist = { version = "1.8" }
plist-macro = { version = "0.1.3" } plist-macro = { version = "0.1.6" }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
ns-keyed-archive = { version = "0.1.4", optional = true } ns-keyed-archive = { version = "0.1.4", optional = true }
crossfire = { version = "2.1", optional = true } crossfire = { version = "2.1", optional = true }
@@ -31,7 +31,7 @@ tracing = { version = "0.1.41" }
base64 = { version = "0.22" } base64 = { version = "0.22" }
indexmap = { version = "2.11", features = ["serde"], optional = true } indexmap = { version = "2.11", features = ["serde"], optional = true }
uuid = { version = "1.18", features = ["serde", "v4"], optional = true } uuid = { version = "1.18", features = ["serde", "v3", "v4"], optional = true }
chrono = { version = "0.4", optional = true, default-features = false, features = [ chrono = { version = "0.4", optional = true, default-features = false, features = [
"serde", "serde",
] } ] }
@@ -55,6 +55,11 @@ x509-cert = { version = "0.2", optional = true, features = [
"builder", "builder",
"pem", "pem",
], default-features = false } ], default-features = false }
x25519-dalek = { version = "2", optional = true }
ed25519-dalek = { version = "2", features = ["rand_core"], optional = true }
hkdf = { version = "0.12", optional = true }
chacha20poly1305 = { version = "0.10", optional = true }
idevice-srp = { version = "0.6", optional = true }
obfstr = { version = "0.4", optional = true } obfstr = { version = "0.4", optional = true }
@@ -97,12 +102,27 @@ misagent = []
mobile_image_mounter = ["dep:sha2"] mobile_image_mounter = ["dep:sha2"]
mobileactivationd = ["dep:reqwest"] mobileactivationd = ["dep:reqwest"]
mobilebackup2 = [] mobilebackup2 = []
notification_proxy = ["tokio/macros", "tokio/time", "dep:async-stream", "dep:futures"] notification_proxy = [
"tokio/macros",
"tokio/time",
"dep:async-stream",
"dep:futures",
]
location_simulation = [] location_simulation = []
pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"] pair = ["chrono/default", "tokio/time", "dep:sha2", "dep:rsa", "dep:x509-cert"]
pcapd = [] pcapd = []
preboard_service = [] preboard_service = []
obfuscate = ["dep:obfstr"] obfuscate = ["dep:obfstr"]
remote_pairing = [
"dep:serde_json",
"dep:json",
"dep:x25519-dalek",
"dep:ed25519-dalek",
"dep:hkdf",
"dep:chacha20poly1305",
"dep:idevice-srp",
"dep:uuid",
]
restore_service = [] restore_service = []
rsd = ["xpc"] rsd = ["xpc"]
screenshotr = [] screenshotr = []
@@ -143,6 +163,7 @@ full = [
"pair", "pair",
"pcapd", "pcapd",
"preboard_service", "preboard_service",
"remote_pairing",
"restore_service", "restore_service",
"rsd", "rsd",
"screenshotr", "screenshotr",

View File

@@ -9,6 +9,8 @@ pub mod cursor;
mod obfuscation; mod obfuscation;
pub mod pairing_file; pub mod pairing_file;
pub mod provider; pub mod provider;
#[cfg(feature = "remote_pairing")]
pub mod remote_pairing;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
mod sni; mod sni;
#[cfg(feature = "tunnel_tcp_stack")] #[cfg(feature = "tunnel_tcp_stack")]
@@ -866,13 +868,25 @@ pub enum IdeviceError {
#[error("Developer mode is not enabled")] #[error("Developer mode is not enabled")]
DeveloperModeNotEnabled = -68, DeveloperModeNotEnabled = -68,
#[error("Unknown TLV {0}")]
UnknownTlv(u8) = -69,
#[error("Malformed TLV")]
MalformedTlv = -70,
#[error("Pairing rejected: {0}")]
PairingRejected(String) = -71,
#[cfg(feature = "remote_pairing")]
#[error("Base64 decode error")]
Base64DecodeError(#[from] base64::DecodeError) = -72,
#[error("Pair verified failed")]
PairVerifyFailed = -73,
#[error("SRP auth failed")]
SrpAuthFailed = -74,
#[cfg(feature = "remote_pairing")]
#[error("Chacha encryption error")]
ChachaEncryption(chacha20poly1305::Error) = -75,
#[cfg(feature = "notification_proxy")] #[cfg(feature = "notification_proxy")]
#[error("notification proxy died")] #[error("notification proxy died")]
NotificationProxyDeath = -69, NotificationProxyDeath = -76,
#[cfg(feature = "installation_proxy")]
#[error("Application verification failed: {0}")]
ApplicationVerificationFailed(String) = -70,
} }
impl IdeviceError { impl IdeviceError {
@@ -916,15 +930,6 @@ impl IdeviceError {
Some(Self::InternalError(detailed_error)) Some(Self::InternalError(detailed_error))
} }
} }
#[cfg(feature = "installation_proxy")]
"ApplicationVerificationFailed" => {
let msg = context
.get("ErrorDescription")
.and_then(|x| x.as_string())
.unwrap_or("No context")
.to_string();
Some(Self::ApplicationVerificationFailed(msg))
}
_ => None, _ => None,
} }
} }
@@ -1047,12 +1052,17 @@ impl IdeviceError {
#[cfg(feature = "installation_proxy")] #[cfg(feature = "installation_proxy")]
IdeviceError::MalformedPackageArchive(_) => -67, IdeviceError::MalformedPackageArchive(_) => -67,
IdeviceError::DeveloperModeNotEnabled => -68, IdeviceError::DeveloperModeNotEnabled => -68,
IdeviceError::UnknownTlv(_) => -69,
IdeviceError::MalformedTlv => -70,
IdeviceError::PairingRejected(_) => -71,
#[cfg(feature = "remote_pairing")]
IdeviceError::Base64DecodeError(_) => -72,
IdeviceError::PairVerifyFailed => -73,
IdeviceError::SrpAuthFailed => -74,
IdeviceError::ChachaEncryption(_) => -75,
#[cfg(feature = "notification_proxy")] #[cfg(feature = "notification_proxy")]
IdeviceError::NotificationProxyDeath => -69, IdeviceError::NotificationProxyDeath => -76,
#[cfg(feature = "installation_proxy")]
IdeviceError::ApplicationVerificationFailed(_) => -70,
} }
} }
} }

View File

@@ -0,0 +1,720 @@
//! Remote Pairing
use crate::IdeviceError;
use chacha20poly1305::{
ChaCha20Poly1305, Key, KeyInit, Nonce,
aead::{Aead, Payload},
};
use ed25519_dalek::Signature;
use hkdf::Hkdf;
use idevice_srp::{client::SrpClient, groups::G_3072};
use plist_macro::plist;
use plist_macro::{PlistConvertible, PlistExt};
use rand::RngCore;
use rsa::{rand_core::OsRng, signature::SignerMut};
use serde::Serialize;
use sha2::Sha512;
use tracing::{debug, warn};
use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey};
mod opack;
mod rp_pairing_file;
mod socket;
mod tlv;
// export
pub use rp_pairing_file::RpPairingFile;
pub use socket::{RpPairingSocket, RpPairingSocketProvider};
const RPPAIRING_MAGIC: &[u8] = b"RPPairing";
const WIRE_PROTOCOL_VERSION: u8 = 19;
pub struct RemotePairingClient<'a, R: RpPairingSocketProvider> {
inner: R,
sequence_number: usize,
pairing_file: &'a mut RpPairingFile,
sending_host: String,
client_cipher: ChaCha20Poly1305,
server_cipher: ChaCha20Poly1305,
}
impl<'a, R: RpPairingSocketProvider> RemotePairingClient<'a, R> {
pub fn new(inner: R, sending_host: &str, pairing_file: &'a mut RpPairingFile) -> Self {
let hk = Hkdf::<sha2::Sha512>::new(None, pairing_file.e_private_key.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"ClientEncrypt-main", &mut okm).unwrap();
let client_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
let hk = Hkdf::<sha2::Sha512>::new(None, pairing_file.e_private_key.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"ServerEncrypt-main", &mut okm).unwrap();
let server_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
Self {
inner,
sequence_number: 0,
pairing_file,
sending_host: sending_host.to_string(),
client_cipher,
server_cipher,
}
}
pub async fn connect<Fut, S>(
&mut self,
pin_callback: impl Fn(S) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = String>,
{
self.attempt_pair_verify().await?;
if self.validate_pairing().await.is_err() {
self.pair(pin_callback, state).await?;
}
Ok(())
}
pub async fn validate_pairing(&mut self) -> Result<(), IdeviceError> {
let x_private_key = EphemeralSecret::random_from_rng(OsRng);
let x_public_key = X25519PublicKey::from(&x_private_key);
let pairing_data = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: vec![0x01],
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::PublicKey,
data: x_public_key.to_bytes().to_vec(),
},
]);
let pairing_data = R::serialize_bytes(&pairing_data);
self.send_pairing_data(plist!({
"data": pairing_data,
"kind": "verifyManualPairing",
"startNewSession": true
}))
.await?;
debug!("Waiting for response from verifyManualPairing");
let pairing_data = self.receive_pairing_data().await?;
let data = match R::deserialize_bytes(pairing_data) {
Some(d) => d,
None => {
return Err(IdeviceError::UnexpectedResponse);
}
};
let data = tlv::deserialize_tlv8(&data)?;
if data
.iter()
.any(|x| x.tlv_type == tlv::PairingDataComponentType::ErrorResponse)
{
self.send_pair_verified_failed().await?;
return Err(IdeviceError::PairVerifyFailed);
}
let device_public_key = match data
.iter()
.find(|x| x.tlv_type == tlv::PairingDataComponentType::PublicKey)
{
Some(d) => d,
None => {
warn!("No public key in TLV data");
return Err(IdeviceError::UnexpectedResponse);
}
};
let peer_pub_bytes: [u8; 32] = match device_public_key.data.as_slice().try_into() {
Ok(d) => d,
Err(_) => {
warn!("Device public key isn't the expected size");
return Err(IdeviceError::NotEnoughBytes(
32,
device_public_key.data.len(),
));
}
};
let device_public_key = x25519_dalek::PublicKey::from(peer_pub_bytes);
let shared_secret = x_private_key.diffie_hellman(&device_public_key);
// Derive encryption key with HKDF-SHA512
let hk =
Hkdf::<sha2::Sha512>::new(Some(b"Pair-Verify-Encrypt-Salt"), shared_secret.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"Pair-Verify-Encrypt-Info", &mut okm).unwrap();
// ChaCha20Poly1305 AEAD cipher
let cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
let ed25519_signing_key = &mut self.pairing_file.e_private_key;
let mut signbuf = Vec::with_capacity(32 + self.pairing_file.identifier.len() + 32);
signbuf.extend_from_slice(x_public_key.as_bytes()); // 32 bytes
signbuf.extend_from_slice(self.pairing_file.identifier.as_bytes()); // variable
signbuf.extend_from_slice(device_public_key.as_bytes()); // 32 bytes
let signature: Signature = ed25519_signing_key.sign(&signbuf);
let plaintext = vec![
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Identifier,
data: self.pairing_file.identifier.as_bytes().to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Signature,
data: signature.to_vec(),
},
];
let plaintext = tlv::serialize_tlv8(&plaintext);
let nonce = Nonce::from_slice(b"\x00\x00\x00\x00PV-Msg03"); // 12-byte nonce
let ciphertext = cipher
.encrypt(
nonce,
Payload {
msg: &plaintext,
aad: &[],
},
)
.expect("encryption should not fail");
let msg = vec![
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: [0x03].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::EncryptedData,
data: ciphertext,
},
];
debug!("Waiting for signbuf response");
self.send_pairing_data(plist! ({
"data": R::serialize_bytes(&tlv::serialize_tlv8(&msg)),
"kind": "verifyManualPairing",
"startNewSession": false
}))
.await?;
let res = self.receive_pairing_data().await?;
let data = match R::deserialize_bytes(res) {
Some(d) => d,
None => return Err(IdeviceError::UnexpectedResponse),
};
let data = tlv::deserialize_tlv8(&data)?;
debug!("Verify TLV: {data:#?}");
// Check if the device responded with an error (which is expected for a new pairing)
if data
.iter()
.any(|x| x.tlv_type == tlv::PairingDataComponentType::ErrorResponse)
{
debug!(
"Verification failed, device reported an error. This is expected for a new pairing."
);
self.send_pair_verified_failed().await?;
// Return a specific error to the caller.
return Err(IdeviceError::PairVerifyFailed);
}
Ok(())
}
pub async fn send_pair_verified_failed(&mut self) -> Result<(), IdeviceError> {
self.inner
.send_plain(
plist!({
"event": {
"_0": {
"pairVerifyFailed": {}
}
}
}),
self.sequence_number,
)
.await?;
self.sequence_number += 1;
Ok(())
}
pub async fn attempt_pair_verify(&mut self) -> Result<plist::Value, IdeviceError> {
debug!("Sending attemptPairVerify");
self.inner
.send_plain(
plist!({
"request": {
"_0": {
"handshake": {
"_0": {
"hostOptions": {
"attemptPairVerify": true
},
"wireProtocolVersion": plist::Value::Integer(WIRE_PROTOCOL_VERSION.into()),
}
}
}
}
}),
self.sequence_number,
)
.await?;
self.sequence_number += 1;
debug!("Waiting for attemptPairVerify response");
let response = self.inner.recv_plain().await?;
let response = response
.as_dictionary()
.and_then(|x| x.get("response"))
.and_then(|x| x.as_dictionary())
.and_then(|x| x.get("_1"))
.and_then(|x| x.as_dictionary())
.and_then(|x| x.get("handshake"))
.and_then(|x| x.as_dictionary())
.and_then(|x| x.get("_0"));
match response {
Some(v) => Ok(v.to_owned()),
None => Err(IdeviceError::UnexpectedResponse),
}
}
pub async fn pair<Fut, S>(
&mut self,
pin_callback: impl Fn(S) -> Fut,
state: S,
) -> Result<(), IdeviceError>
where
Fut: std::future::Future<Output = String>,
{
let (salt, public_key, pin) = self.request_pair_consent(pin_callback, state).await?;
let key = self.init_srp_context(&salt, &public_key, &pin).await?;
self.save_pair_record_on_peer(&key).await?;
Ok(())
}
/// Returns salt and public key and pin
async fn request_pair_consent<Fut, S>(
&mut self,
pin_callback: impl Fn(S) -> Fut,
state: S,
) -> Result<(Vec<u8>, Vec<u8>, String), IdeviceError>
where
Fut: std::future::Future<Output = String>,
{
let tlv = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Method,
data: vec![0x00],
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: vec![0x01],
},
]);
let tlv = R::serialize_bytes(&tlv);
self.send_pairing_data(plist!({
"data": tlv,
"kind": "setupManualPairing",
"sendingHost": &self.sending_host,
"startNewSession": true
}))
.await?;
let response = self.inner.recv_plain().await?;
let response = match response
.get_by("event")
.and_then(|x| x.get_by("_0"))
.and_then(|x| x.as_dictionary())
{
Some(r) => r,
None => {
return Err(IdeviceError::UnexpectedResponse);
}
};
let mut pin = None;
let pairing_data = match if let Some(err) = response.get("pairingRejectedWithError") {
let context = err
.get_by("wrappedError")
.and_then(|x| x.get_by("userInfo"))
.and_then(|x| x.get_by("NSLocalizedDescription"))
.and_then(|x| x.as_string())
.map(|x| x.to_string());
return Err(IdeviceError::PairingRejected(context.unwrap_or_default()));
} else if response.get("awaitingUserConsent").is_some() {
pin = Some("000000".to_string());
Some(self.receive_pairing_data().await?)
} else {
// On Apple TV, we can get the pin now
response
.get_by("pairingData")
.and_then(|x| x.get_by("_0"))
.and_then(|x| x.get_by("data"))
.map(|x| x.to_owned())
} {
Some(p) => p,
None => {
return Err(IdeviceError::UnexpectedResponse);
}
};
let tlv = tlv::deserialize_tlv8(&match R::deserialize_bytes(pairing_data) {
Some(t) => t,
None => return Err(IdeviceError::UnexpectedResponse),
})?;
debug!("Received pairingData response: {tlv:#?}");
let mut salt = Vec::new();
let mut public_key = Vec::new();
for t in tlv {
match t.tlv_type {
tlv::PairingDataComponentType::Salt => {
salt = t.data;
}
tlv::PairingDataComponentType::PublicKey => {
public_key.extend(t.data);
}
tlv::PairingDataComponentType::ErrorResponse => {
warn!("Pairing data contained error response");
return Err(IdeviceError::UnexpectedResponse);
}
_ => {
continue;
}
}
}
let pin = match pin {
Some(p) => p,
None => pin_callback(state).await,
};
if salt.is_empty() || public_key.is_empty() {
warn!("Pairing data did not contain salt or public key");
return Err(IdeviceError::UnexpectedResponse);
}
Ok((salt, public_key, pin))
}
/// Returns the encryption key
async fn init_srp_context(
&mut self,
salt: &[u8],
public_key: &[u8],
pin: &str,
) -> Result<Vec<u8>, IdeviceError> {
let client = SrpClient::<Sha512>::new(
&G_3072, // PRIME_3072 + generator
);
let mut a_private = [0u8; 32];
rand::rng().fill_bytes(&mut a_private);
let a_public = client.compute_public_ephemeral(&a_private);
let verifier = match client.process_reply(
&a_private,
"Pair-Setup".as_bytes(),
&pin.as_bytes()[..6],
salt,
public_key,
false,
) {
Ok(v) => v,
Err(e) => {
warn!("SRP verifier creation failed: {e:?}");
return Err(IdeviceError::SrpAuthFailed);
}
};
let client_proof = verifier.proof();
let tlv = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: vec![0x03],
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::PublicKey,
data: a_public[..254].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::PublicKey,
data: a_public[254..].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Proof,
data: client_proof.to_vec(),
},
]);
let tlv = R::serialize_bytes(&tlv);
self.send_pairing_data(plist!({
"data": tlv,
"kind": "setupManualPairing",
"sendingHost": &self.sending_host,
"startNewSession": false,
}))
.await?;
let response = self.receive_pairing_data().await?;
let response = tlv::deserialize_tlv8(&match R::deserialize_bytes(response.to_owned()) {
Some(r) => r,
None => return Err(IdeviceError::UnexpectedResponse),
})?;
debug!("Proof response: {response:#?}");
let proof = match response
.iter()
.find(|x| x.tlv_type == tlv::PairingDataComponentType::Proof)
{
Some(p) => &p.data,
None => {
warn!("Proof response did not contain server proof");
return Err(IdeviceError::UnexpectedResponse);
}
};
match verifier.verify_server(proof) {
Ok(_) => Ok(verifier.key().to_vec()),
Err(e) => {
warn!("Server auth failed: {e:?}");
Err(IdeviceError::SrpAuthFailed)
}
}
}
async fn save_pair_record_on_peer(
&mut self,
encryption_key: &[u8],
) -> Result<Vec<tlv::TLV8Entry>, IdeviceError> {
let salt = b"Pair-Setup-Encrypt-Salt";
let info = b"Pair-Setup-Encrypt-Info";
let hk = Hkdf::<Sha512>::new(Some(salt), encryption_key);
let mut setup_encryption_key = [0u8; 32];
hk.expand(info, &mut setup_encryption_key)
.expect("HKDF expand failed");
self.pairing_file.recreate_signing_keys();
{
// new scope, update our signing keys
let hk = Hkdf::<sha2::Sha512>::new(None, self.pairing_file.e_private_key.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"ClientEncrypt-main", &mut okm).unwrap();
self.client_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
let hk = Hkdf::<sha2::Sha512>::new(None, self.pairing_file.e_private_key.as_bytes());
let mut okm = [0u8; 32];
hk.expand(b"ServerEncrypt-main", &mut okm).unwrap();
self.server_cipher = ChaCha20Poly1305::new(chacha20poly1305::Key::from_slice(&okm));
}
let hk = Hkdf::<Sha512>::new(Some(b"Pair-Setup-Controller-Sign-Salt"), encryption_key);
let mut signbuf = Vec::with_capacity(32 + self.pairing_file.identifier.len() + 32);
let mut hkdf_out = [0u8; 32];
hk.expand(b"Pair-Setup-Controller-Sign-Info", &mut hkdf_out)
.expect("HKDF expand failed");
signbuf.extend_from_slice(&hkdf_out);
signbuf.extend_from_slice(self.pairing_file.identifier.as_bytes());
signbuf.extend_from_slice(self.pairing_file.e_public_key.as_bytes());
let signature = self.pairing_file.e_private_key.sign(&signbuf);
let device_info = crate::plist!({
"altIRK": b"\xe9\xe8-\xc0jIykVoT\x00\x19\xb1\xc7{".to_vec(),
"btAddr": "11:22:33:44:55:66",
"mac": b"\x11\x22\x33\x44\x55\x66".to_vec(),
"remotepairing_serial_number": "AAAAAAAAAAAA",
"accountID": self.pairing_file.identifier.as_str(),
"model": "computer-model",
"name": self.sending_host.as_str()
});
let device_info = opack::plist_to_opack(&device_info);
let tlv = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Identifier,
data: self.pairing_file.identifier.as_bytes().to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::PublicKey,
data: self.pairing_file.e_public_key.to_bytes().to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Signature,
data: signature.to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::Info,
data: device_info,
},
]);
let key = Key::from_slice(&setup_encryption_key); // 32 bytes
let cipher = ChaCha20Poly1305::new(key);
let nonce = Nonce::from_slice(b"\x00\x00\x00\x00PS-Msg05"); // 12 bytes
let plaintext = &tlv;
let ciphertext = match cipher.encrypt(
nonce,
Payload {
msg: plaintext,
aad: b"",
},
) {
Ok(c) => c,
Err(e) => {
warn!("Chacha encryption failed: {e:?}");
return Err(IdeviceError::ChachaEncryption(e));
}
};
debug!("ciphertext len: {}", ciphertext.len());
let tlv = tlv::serialize_tlv8(&[
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::EncryptedData,
data: ciphertext[..254].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::EncryptedData,
data: ciphertext[254..].to_vec(),
},
tlv::TLV8Entry {
tlv_type: tlv::PairingDataComponentType::State,
data: vec![0x05],
},
]);
let tlv = R::serialize_bytes(&tlv);
debug!("Sending encrypted data");
self.send_pairing_data(plist!({
"data": tlv,
"kind": "setupManualPairing",
"sendingHost": &self.sending_host,
"startNewSession": false,
}))
.await?;
debug!("Waiting for encrypted data");
let response = match R::deserialize_bytes(self.receive_pairing_data().await?) {
Some(r) => r,
None => {
warn!("Pairing data response was not deserializable");
return Err(IdeviceError::UnexpectedResponse);
}
};
let tlv = tlv::deserialize_tlv8(&response)?;
let mut encrypted_data = Vec::new();
for t in tlv {
match t.tlv_type {
tlv::PairingDataComponentType::EncryptedData => encrypted_data.extend(t.data),
tlv::PairingDataComponentType::ErrorResponse => {
warn!("TLV contained error response");
return Err(IdeviceError::UnexpectedResponse);
}
_ => {}
}
}
let nonce = Nonce::from_slice(b"\x00\x00\x00\x00PS-Msg06");
let plaintext = cipher
.decrypt(
nonce,
Payload {
msg: &encrypted_data,
aad: b"",
},
)
.expect("decryption failure!");
let tlv = tlv::deserialize_tlv8(&plaintext)?;
debug!("Decrypted plaintext TLV: {tlv:?}");
Ok(tlv)
}
async fn send_pairing_data(
&mut self,
pairing_data: impl Serialize + PlistConvertible,
) -> Result<(), IdeviceError> {
self.inner
.send_plain(
plist!({
"event": {
"_0": {
"pairingData": {
"_0": pairing_data
}
}
}
}),
self.sequence_number,
)
.await?;
self.sequence_number += 1;
Ok(())
}
async fn receive_pairing_data(&mut self) -> Result<plist::Value, IdeviceError> {
let response = self.inner.recv_plain().await?;
let response = match response.get_by("event").and_then(|x| x.get_by("_0")) {
Some(r) => r,
None => return Err(IdeviceError::UnexpectedResponse),
};
if let Some(data) = response
.get_by("pairingData")
.and_then(|x| x.get_by("_0"))
.and_then(|x| x.get_by("data"))
{
Ok(data.to_owned())
} else if let Some(err) = response.get_by("pairingRejectedWithError") {
let context = err
.get_by("wrappedError")
.and_then(|x| x.get_by("userInfo"))
.and_then(|x| x.get_by("NSLocalizedDescription"))
.and_then(|x| x.as_string())
.map(|x| x.to_string());
Err(IdeviceError::PairingRejected(context.unwrap_or_default()))
} else {
Err(IdeviceError::UnexpectedResponse)
}
}
}
impl<R: RpPairingSocketProvider> std::fmt::Debug for RemotePairingClient<'_, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RemotePairingClient")
.field("inner", &self.inner)
.field("sequence_number", &self.sequence_number)
.field("pairing_file", &self.pairing_file)
.field("sending_host", &self.sending_host)
.finish()
}
}

View File

@@ -0,0 +1,165 @@
// Jackson Coxson
use plist::Value;
pub fn plist_to_opack(value: &Value) -> Vec<u8> {
let mut buf = Vec::new();
plist_to_opack_inner(value, &mut buf);
buf
}
fn plist_to_opack_inner(node: &Value, buf: &mut Vec<u8>) {
match node {
Value::Dictionary(dict) => {
let count = dict.len() as u32;
let blen = if count < 15 {
(count as u8).wrapping_sub(32)
} else {
0xEF
};
buf.push(blen);
for (key, val) in dict {
plist_to_opack_inner(&Value::String(key.clone()), buf);
plist_to_opack_inner(val, buf);
}
if count > 14 {
buf.push(0x03);
}
}
Value::Array(array) => {
let count = array.len() as u32;
let blen = if count < 15 {
(count as u8).wrapping_sub(48)
} else {
0xDF
};
buf.push(blen);
for val in array {
plist_to_opack_inner(val, buf);
}
if count > 14 {
buf.push(0x03); // Terminator
}
}
Value::Boolean(b) => {
let bval = if *b { 1u8 } else { 2u8 };
buf.push(bval);
}
Value::Integer(integer) => {
let u64val = integer.as_unsigned().unwrap_or(0);
if u64val <= u8::MAX as u64 {
let u8val = u64val as u8;
if u8val > 0x27 {
buf.push(0x30);
buf.push(u8val);
} else {
buf.push(u8val + 8);
}
} else if u64val <= u32::MAX as u64 {
buf.push(0x32);
buf.extend_from_slice(&(u64val as u32).to_le_bytes());
} else {
buf.push(0x33);
buf.extend_from_slice(&u64val.to_le_bytes());
}
}
Value::Real(real) => {
let dval = *real;
let fval = dval as f32;
if fval as f64 == dval {
buf.push(0x35);
buf.extend_from_slice(&fval.to_bits().swap_bytes().to_ne_bytes());
} else {
buf.push(0x36);
buf.extend_from_slice(&dval.to_bits().swap_bytes().to_ne_bytes());
}
}
Value::String(s) => {
let bytes = s.as_bytes();
let len = bytes.len();
if len > 0x20 {
if len <= 0xFF {
buf.push(0x61);
buf.push(len as u8);
} else if len <= 0xFFFF {
buf.push(0x62);
buf.extend_from_slice(&(len as u16).to_le_bytes());
} else if len <= 0xFFFFFFFF {
buf.push(0x63);
buf.extend_from_slice(&(len as u32).to_le_bytes());
} else {
buf.push(0x64);
buf.extend_from_slice(&(len as u64).to_le_bytes());
}
} else {
buf.push(0x40 + len as u8);
}
buf.extend_from_slice(bytes);
}
Value::Data(data) => {
let len = data.len();
if len > 0x20 {
if len <= 0xFF {
buf.push(0x91);
buf.push(len as u8);
} else if len <= 0xFFFF {
buf.push(0x92);
buf.extend_from_slice(&(len as u16).to_le_bytes());
} else if len <= 0xFFFFFFFF {
buf.push(0x93);
buf.extend_from_slice(&(len as u32).to_le_bytes());
} else {
buf.push(0x94);
buf.extend_from_slice(&(len as u64).to_le_bytes());
}
} else {
buf.push(0x70 + len as u8);
}
buf.extend_from_slice(data);
}
_ => {}
}
}
#[cfg(test)]
mod tests {
#[test]
fn t1() {
let v = crate::plist!({
"altIRK": b"\xe9\xe8-\xc0jIykVoT\x00\x19\xb1\xc7{".to_vec(),
"btAddr": "11:22:33:44:55:66",
"mac": b"\x11\x22\x33\x44\x55\x66".to_vec(),
"remotepairing_serial_number": "AAAAAAAAAAAA",
"accountID": "lolsssss",
"model": "computer-model",
"name": "reeeee",
});
let res = super::plist_to_opack(&v);
let expected = [
0xe7, 0x46, 0x61, 0x6c, 0x74, 0x49, 0x52, 0x4b, 0x80, 0xe9, 0xe8, 0x2d, 0xc0, 0x6a,
0x49, 0x79, 0x6b, 0x56, 0x6f, 0x54, 0x00, 0x19, 0xb1, 0xc7, 0x7b, 0x46, 0x62, 0x74,
0x41, 0x64, 0x64, 0x72, 0x51, 0x31, 0x31, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x33, 0x3a,
0x34, 0x34, 0x3a, 0x35, 0x35, 0x3a, 0x36, 0x36, 0x43, 0x6d, 0x61, 0x63, 0x76, 0x11,
0x22, 0x33, 0x44, 0x55, 0x66, 0x5b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x70, 0x61,
0x69, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x5f, 0x6e,
0x75, 0x6d, 0x62, 0x65, 0x72, 0x4c, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,
0x41, 0x41, 0x41, 0x41, 0x49, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x44,
0x48, 0x6c, 0x6f, 0x6c, 0x73, 0x73, 0x73, 0x73, 0x73, 0x45, 0x6d, 0x6f, 0x64, 0x65,
0x6c, 0x4e, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x72, 0x2d, 0x6d, 0x6f, 0x64,
0x65, 0x6c, 0x44, 0x6e, 0x61, 0x6d, 0x65, 0x46, 0x72, 0x65, 0x65, 0x65, 0x65, 0x65,
];
println!("{res:02X?}");
assert_eq!(res, expected);
}
}

View File

@@ -0,0 +1,114 @@
// Jackson Coxson
use std::path::Path;
use ed25519_dalek::{SigningKey, VerifyingKey};
use plist::Dictionary;
use plist_macro::plist_to_xml_bytes;
use rsa::rand_core::OsRng;
use serde::de::Error;
use tracing::{debug, warn};
use crate::IdeviceError;
#[derive(Clone)]
pub struct RpPairingFile {
pub(crate) e_private_key: SigningKey,
pub(crate) e_public_key: VerifyingKey,
pub(crate) identifier: String,
}
impl RpPairingFile {
pub fn generate(sending_host: &str) -> Self {
// Ed25519 private key (persistent signing key)
let ed25519_private_key = SigningKey::generate(&mut OsRng);
let ed25519_public_key = VerifyingKey::from(&ed25519_private_key);
let identifier =
uuid::Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, sending_host.as_bytes()).to_string();
Self {
e_private_key: ed25519_private_key,
e_public_key: ed25519_public_key,
identifier,
}
}
pub(crate) fn recreate_signing_keys(&mut self) {
let ed25519_private_key = SigningKey::generate(&mut OsRng);
let ed25519_public_key = VerifyingKey::from(&ed25519_private_key);
self.e_public_key = ed25519_public_key;
self.e_private_key = ed25519_private_key;
}
pub async fn write_to_file(&self, path: impl AsRef<Path>) -> Result<(), IdeviceError> {
let v = crate::plist!(dict {
"public_key": self.e_public_key.to_bytes().to_vec(),
"private_key": self.e_private_key.to_bytes().to_vec(),
"identifier": self.identifier.as_str()
});
tokio::fs::write(path, plist_to_xml_bytes(&v)).await?;
Ok(())
}
pub async fn read_from_file(path: impl AsRef<Path>) -> Result<Self, IdeviceError> {
let s = tokio::fs::read_to_string(path).await?;
let mut p: Dictionary = plist::from_bytes(s.as_bytes())?;
debug!("Read dictionary for rppairingfile: {p:#?}");
let public_key = match p
.remove("public_key")
.and_then(|x| x.into_data())
.filter(|x| x.len() == 32)
.and_then(|x| VerifyingKey::from_bytes(&x[..32].try_into().unwrap()).ok())
{
Some(p) => p,
None => {
warn!("plist did not contain valid public key bytes");
return Err(IdeviceError::Plist(plist::Error::missing_field(
"public_key",
)));
}
};
let private_key = match p
.remove("private_key")
.and_then(|x| x.into_data())
.filter(|x| x.len() == 32)
{
Some(p) => SigningKey::from_bytes(&p.try_into().unwrap()),
None => {
warn!("plist did not contain valid private key bytes");
return Err(IdeviceError::Plist(plist::Error::missing_field(
"private_key",
)));
}
};
let identifier = match p.remove("identifier").and_then(|x| x.into_string()) {
Some(i) => i,
None => {
warn!("plist did not contain identifier");
return Err(IdeviceError::Plist(plist::Error::missing_field(
"identifier",
)));
}
};
Ok(Self {
e_private_key: private_key,
e_public_key: public_key,
identifier,
})
}
}
impl std::fmt::Debug for RpPairingFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RpPairingFile")
.field("e_public_key", &self.e_public_key)
.field("identifier", &self.identifier)
.finish()
}
}

View File

@@ -0,0 +1,173 @@
// Jackson Coxson
use base64::{Engine as _, engine::general_purpose::STANDARD as B64};
use plist_macro::{plist, pretty_print_plist};
use serde::Serialize;
use serde_json::json;
use std::{fmt::Debug, pin::Pin};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tracing::{debug, warn};
use crate::{
IdeviceError, ReadWrite, RemoteXpcClient, remote_pairing::RPPAIRING_MAGIC, xpc::XPCObject,
};
pub trait RpPairingSocketProvider: Debug {
fn send_plain(
&mut self,
value: impl Serialize,
seq: usize,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + '_>>;
fn recv_plain<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<plist::Value, IdeviceError>> + Send + 'a>>;
/// rppairing uses b64, while RemoteXPC uses raw bytes just fine
fn serialize_bytes(b: &[u8]) -> plist::Value;
fn deserialize_bytes(v: plist::Value) -> Option<Vec<u8>>;
}
#[derive(Debug)]
pub struct RpPairingSocket<R: ReadWrite> {
pub inner: R,
}
impl<R: ReadWrite> RpPairingSocket<R> {
pub fn new(socket: R) -> Self {
Self { inner: socket }
}
async fn send_rppairing(&mut self, value: impl Serialize) -> Result<(), IdeviceError> {
let value = serde_json::to_string(&value)?;
let x = value.as_bytes();
self.inner.write_all(RPPAIRING_MAGIC).await?;
self.inner
.write_all(&(x.len() as u16).to_be_bytes())
.await?;
self.inner.write_all(x).await?;
Ok(())
}
}
impl<R: ReadWrite> RpPairingSocketProvider for RpPairingSocket<R> {
fn send_plain(
&mut self,
value: impl Serialize,
seq: usize,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + '_>> {
let v = json!({
"message": {"plain": {"_0": value}},
"originatedBy": "host",
"sequenceNumber": seq
});
Box::pin(async move {
self.send_rppairing(v).await?;
Ok(())
})
}
fn recv_plain<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<plist::Value, IdeviceError>> + Send + 'a>> {
Box::pin(async move {
self.inner
.read_exact(&mut vec![0u8; RPPAIRING_MAGIC.len()])
.await?;
let mut packet_len_bytes = [0u8; 2];
self.inner.read_exact(&mut packet_len_bytes).await?;
let packet_len = u16::from_be_bytes(packet_len_bytes);
let mut value = vec![0u8; packet_len as usize];
self.inner.read_exact(&mut value).await?;
let value: serde_json::Value = serde_json::from_slice(&value)?;
let value = value
.get("message")
.and_then(|x| x.get("plain"))
.and_then(|x| x.get("_0"));
match value {
Some(v) => Ok(plist::to_value(v).unwrap()),
None => Err(IdeviceError::UnexpectedResponse),
}
})
}
fn serialize_bytes(b: &[u8]) -> plist::Value {
plist!(B64.encode(b))
}
fn deserialize_bytes(v: plist::Value) -> Option<Vec<u8>> {
if let plist::Value::String(v) = v {
B64.decode(v).ok()
} else {
None
}
}
}
impl<R: ReadWrite> RpPairingSocketProvider for RemoteXpcClient<R> {
fn send_plain(
&mut self,
value: impl Serialize,
seq: usize,
) -> Pin<Box<dyn Future<Output = Result<(), IdeviceError>> + Send + '_>> {
let value: plist::Value = plist::to_value(&value).expect("plist assert failed");
let value: XPCObject = value.into();
let v = crate::xpc!({
"mangledTypeName": "RemotePairing.ControlChannelMessageEnvelope",
"value": {
"message": {"plain": {"_0": value}},
"originatedBy": "host",
"sequenceNumber": seq as u64
}
});
debug!("Sending XPC: {v:#?}");
Box::pin(async move {
self.send_object(v, true).await?;
Ok(())
})
}
fn recv_plain<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<plist::Value, IdeviceError>> + Send + 'a>> {
Box::pin(async move {
let msg = self.recv_root().await.unwrap();
debug!("Received RemoteXPC {}", pretty_print_plist(&msg));
let value = msg
.into_dictionary()
.and_then(|mut x| x.remove("value"))
.and_then(|x| x.into_dictionary())
.and_then(|mut x| x.remove("message"))
.and_then(|x| x.into_dictionary())
.and_then(|mut x| x.remove("plain"))
.and_then(|x| x.into_dictionary())
.and_then(|mut x| x.remove("_0"));
match value {
Some(v) => Ok(v),
None => Err(IdeviceError::UnexpectedResponse),
}
})
}
fn serialize_bytes(b: &[u8]) -> plist::Value {
plist::Value::Data(b.to_owned())
}
fn deserialize_bytes(v: plist::Value) -> Option<Vec<u8>> {
if let plist::Value::Data(v) = v {
Some(v)
} else {
warn!("Non-data passed to rppairingsocket::deserialize_bytes for RemoteXPC provider");
None
}
}
}

View File

@@ -0,0 +1,123 @@
// Jackson Coxson
use crate::IdeviceError;
// from pym3
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum PairingDataComponentType {
Method = 0x00,
Identifier = 0x01,
Salt = 0x02,
PublicKey = 0x03,
Proof = 0x04,
EncryptedData = 0x05,
State = 0x06,
ErrorResponse = 0x07,
RetryDelay = 0x08,
Certificate = 0x09,
Signature = 0x0a,
Permissions = 0x0b,
FragmentData = 0x0c,
FragmentLast = 0x0d,
SessionId = 0x0e,
Ttl = 0x0f,
ExtraData = 0x10,
Info = 0x11,
Acl = 0x12,
Flags = 0x13,
ValidationData = 0x14,
MfiAuthToken = 0x15,
MfiProductType = 0x16,
SerialNumber = 0x17,
MfiAuthTokenUuid = 0x18,
AppFlags = 0x19,
OwnershipProof = 0x1a,
SetupCodeType = 0x1b,
ProductionData = 0x1c,
AppInfo = 0x1d,
Separator = 0xff,
}
#[derive(Debug, Clone)]
pub struct TLV8Entry {
pub tlv_type: PairingDataComponentType,
pub data: Vec<u8>,
}
pub fn serialize_tlv8(entries: &[TLV8Entry]) -> Vec<u8> {
let mut out = Vec::new();
for entry in entries {
out.push(entry.tlv_type as u8);
out.push(entry.data.len() as u8);
out.extend(&entry.data);
}
out
}
pub fn deserialize_tlv8(input: &[u8]) -> Result<Vec<TLV8Entry>, IdeviceError> {
let mut index = 0;
let mut result = Vec::new();
while index + 2 <= input.len() {
let type_byte = input[index];
let length = input[index + 1] as usize;
index += 2;
if index + length > input.len() {
return Err(IdeviceError::MalformedTlv);
}
let data = input[index..index + length].to_vec();
index += length;
let tlv_type = PairingDataComponentType::try_from(type_byte)
.map_err(|_| IdeviceError::UnknownTlv(type_byte))?;
result.push(TLV8Entry { tlv_type, data });
}
Ok(result)
}
impl TryFrom<u8> for PairingDataComponentType {
type Error = u8;
fn try_from(value: u8) -> Result<Self, Self::Error> {
use PairingDataComponentType::*;
Ok(match value {
0x00 => Method,
0x01 => Identifier,
0x02 => Salt,
0x03 => PublicKey,
0x04 => Proof,
0x05 => EncryptedData,
0x06 => State,
0x07 => ErrorResponse,
0x08 => RetryDelay,
0x09 => Certificate,
0x0a => Signature,
0x0b => Permissions,
0x0c => FragmentData,
0x0d => FragmentLast,
0x0e => SessionId,
0x0f => Ttl,
0x10 => ExtraData,
0x11 => Info,
0x12 => Acl,
0x13 => Flags,
0x14 => ValidationData,
0x15 => MfiAuthToken,
0x16 => MfiProductType,
0x17 => SerialNumber,
0x18 => MfiAuthTokenUuid,
0x19 => AppFlags,
0x1a => OwnershipProof,
0x1b => SetupCodeType,
0x1c => ProductionData,
0x1d => AppInfo,
0xff => Separator,
other => return Err(other),
})
}
}

View File

@@ -53,10 +53,6 @@ impl<R: ReadWrite> RemoteXpcClient<R> {
)) ))
.await?; .await?;
debug!("Sending weird flags");
self.send_root(XPCMessage::new(Some(XPCFlag::Custom(0x201)), None, None))
.await?;
debug!("Opening reply stream"); debug!("Opening reply stream");
self.h2_client.open_stream(REPLY_CHANNEL).await?; self.h2_client.open_stream(REPLY_CHANNEL).await?;
self.send_reply(XPCMessage::new( self.send_reply(XPCMessage::new(
@@ -66,6 +62,10 @@ impl<R: ReadWrite> RemoteXpcClient<R> {
)) ))
.await?; .await?;
debug!("Sending weird flags");
self.send_root(XPCMessage::new(Some(XPCFlag::Custom(0x201)), None, None))
.await?;
Ok(()) Ok(())
} }

View File

@@ -10,6 +10,14 @@ repository = "https://github.com/jkcoxson/idevice"
keywords = ["lockdownd", "ios"] keywords = ["lockdownd", "ios"]
default-run = "idevice-tools" default-run = "idevice-tools"
[[bin]]
name = "pair_apple_tv"
path = "src/pair_apple_tv.rs"
[[bin]]
name = "pair_rsd_ios"
path = "src/pair_rsd_ios.rs"
# [[bin]] # [[bin]]
# name = "core_device_proxy_tun" # name = "core_device_proxy_tun"
# path = "src/core_device_proxy_tun.rs" # path = "src/core_device_proxy_tun.rs"
@@ -37,6 +45,7 @@ plist-macro = { version = "0.1.3" }
ns-keyed-archive = "0.1.2" ns-keyed-archive = "0.1.2"
uuid = "1.16" uuid = "1.16"
futures-util = { version = "0.3" } futures-util = { version = "0.3" }
zeroconf = { version = "0.17" }
[features] [features]
default = ["aws-lc"] default = ["aws-lc"]

View File

@@ -0,0 +1,92 @@
// Jackson Coxson
// A PoC to pair by IP
// Ideally you'd browse by mDNS in production
use std::{io::Write, net::IpAddr, str::FromStr};
use clap::{Arg, Command};
use idevice::remote_pairing::{RemotePairingClient, RpPairingFile, RpPairingSocket};
#[tokio::main]
async fn main() {
// tracing_subscriber::fmt::init();
let matches = Command::new("pair")
.about("Pair with the device")
.arg(
Arg::new("ip")
.value_name("IP")
.help("The IP of the Apple TV")
.required(true)
.index(1),
)
.arg(
Arg::new("port")
.value_name("port")
.help("The port of the Apple TV")
.required(true)
.index(2),
)
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
println!("pair - pair with the Apple TV");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let ip = matches.get_one::<String>("ip").expect("no IP passed");
let port = matches.get_one::<String>("port").expect("no port passed");
let port = port.parse::<u16>().unwrap();
let conn =
tokio::net::TcpStream::connect((IpAddr::from_str(ip).expect("failed to parse IP"), port))
.await
.expect("Failed to connect");
let conn = RpPairingSocket::new(conn);
let host = "idevice-rs-jkcoxson";
let mut rpf = RpPairingFile::generate(host);
let mut rpc = RemotePairingClient::new(conn, host, &mut rpf);
rpc.connect(
async |_| {
let mut buf = String::new();
print!("Enter the Apple TV pin: ");
std::io::stdout().flush().unwrap();
std::io::stdin()
.read_line(&mut buf)
.expect("Failed to read line");
buf.trim_end().to_string()
},
0u8, // we need no state, so pass a single byte that will hopefully get optimized out
)
.await
.expect("no pair");
// now that we are paired, we should be good
println!("Reconnecting...");
let conn =
tokio::net::TcpStream::connect((IpAddr::from_str(ip).expect("failed to parse IP"), port))
.await
.expect("Failed to connect");
let conn = RpPairingSocket::new(conn);
let mut rpc = RemotePairingClient::new(conn, host, &mut rpf);
rpc.connect(
async |_| {
panic!("we tried to pair again :(");
},
0u8,
)
.await
.expect("no reconnect");
rpf.write_to_file("atv_pairing_file.plist").await.unwrap();
println!("Pairing file validated and written to disk. Have a nice day.");
}

235
tools/src/pair_rsd_ios.rs Normal file
View File

@@ -0,0 +1,235 @@
// let ip = Ipv6Addr::new(0xfe80, 0, 0, 0, 0x282a, 0x9aff, 0xfedb, 0x8cbb);
// let addr = SocketAddrV6::new(ip, 60461, 0, 28);
// let conn = tokio::net::TcpStream::connect(addr).await.unwrap();
// Jackson Coxson
use std::{
any::Any,
net::{IpAddr, SocketAddr, SocketAddrV6},
sync::Arc,
time::Duration,
};
#[cfg(target_os = "linux")]
use std::{fs, process::Command as OsCommand};
use clap::{Arg, Command};
use idevice::{
RemoteXpcClient,
remote_pairing::{RemotePairingClient, RpPairingFile},
rsd::RsdHandshake,
};
use tokio::net::TcpStream;
use zeroconf::{
BrowserEvent, MdnsBrowser, ServiceType,
prelude::{TEventLoop, TMdnsBrowser},
};
const SERVICE_NAME: &str = "remoted";
const SERVICE_PROTOCOL: &str = "tcp";
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let matches = Command::new("pair")
.about("Pair with the device")
.arg(
Arg::new("about")
.long("about")
.help("Show about information")
.action(clap::ArgAction::SetTrue),
)
.get_matches();
if matches.get_flag("about") {
println!("pair - pair with the device");
println!("Copyright (c) 2025 Jackson Coxson");
return;
}
let mut browser = MdnsBrowser::new(
ServiceType::new(SERVICE_NAME, SERVICE_PROTOCOL).expect("Unable to start mDNS browse"),
);
browser.set_service_callback(Box::new(on_service_discovered));
let event_loop = browser.browse_services().unwrap();
loop {
// calling `poll()` will keep this browser alive
event_loop.poll(Duration::from_secs(0)).unwrap();
}
}
fn on_service_discovered(
result: zeroconf::Result<BrowserEvent>,
_context: Option<Arc<dyn Any + Send + Sync>>,
) {
if let Ok(BrowserEvent::Add(result)) = result {
tokio::task::spawn(async move {
println!("Found iOS device to pair with!! - {result:?}");
let host_name = result.host_name().to_string();
let service_address = result.address().to_string();
let scope_id = link_local_scope_id_from_avahi(&host_name, &service_address);
let stream = match connect_to_service_port(
&host_name,
&service_address,
scope_id,
*result.port(),
)
.await
{
Some(s) => s,
None => {
println!("Couldn't open TCP port on device");
return;
}
};
let handshake = RsdHandshake::new(stream).await.expect("no rsd");
println!("handshake: {handshake:#?}");
let ts = handshake
.services
.get("com.apple.internal.dt.coredevice.untrusted.tunnelservice")
.unwrap();
println!("connecting to tunnel service");
let stream = connect_to_service_port(&host_name, &service_address, scope_id, ts.port)
.await
.expect("failed to connect to tunnselservice");
let mut conn = RemoteXpcClient::new(stream).await.unwrap();
println!("doing tunnel service handshake");
conn.do_handshake().await.unwrap();
let msg = conn.recv_root().await.unwrap();
println!("{msg:#?}");
let host = "idevice-rs-jkcoxson";
let mut rpf = RpPairingFile::generate(host);
let mut rpc = RemotePairingClient::new(conn, host, &mut rpf);
rpc.connect(
async |_| "000000".to_string(),
0u8, // we need no state, so pass a single byte that will hopefully get optimized out
)
.await
.expect("no pair");
rpf.write_to_file("ios_pairing_file.plist").await.unwrap();
println!(
"congrats you're paired now, the rppairing record has been saved. Have a nice day."
);
});
}
}
async fn connect_to_service_port(
host_name: &str,
service_address: &str,
scope_id: Option<u32>,
port: u16,
) -> Option<TcpStream> {
if let Some(stream) = lookup_host_and_connect(host_name, port).await {
return Some(stream);
}
let addr: IpAddr = match service_address.parse() {
Ok(addr) => addr,
Err(e) => {
println!("failed to parse resolved service address {service_address}: {e}");
return None;
}
};
let socket = match addr {
IpAddr::V6(v6) if v6.is_unicast_link_local() => {
SocketAddr::V6(SocketAddrV6::new(v6, port, 0, scope_id.unwrap_or(0)))
}
_ => SocketAddr::new(addr, port),
};
println!("using resolved service address fallback: {socket}");
match TcpStream::connect(socket).await {
Ok(s) => {
println!("connected with local addr {:?}", s.local_addr());
Some(s)
}
Err(e) => {
println!("failed to connect with service address fallback: {e:?}");
None
}
}
}
async fn lookup_host_and_connect(host: &str, port: u16) -> Option<TcpStream> {
let looked_up = match tokio::net::lookup_host((host, port)).await {
Ok(addrs) => addrs,
Err(e) => {
println!("hostname lookup failed for {host}:{port}: {e}");
return None;
}
};
let mut stream = None;
for l in looked_up {
if l.is_ipv4() {
continue;
}
println!("Found IP: {l:?}");
match tokio::net::TcpStream::connect(l).await {
Ok(s) => {
println!("connected with local addr {:?}", s.local_addr());
stream = Some(s);
break;
}
Err(e) => println!("failed to connect: {e:?}"),
}
}
stream
}
#[cfg(target_os = "linux")]
fn link_local_scope_id_from_avahi(host_name: &str, service_address: &str) -> Option<u32> {
let output = OsCommand::new("avahi-browse")
.args(["-rpt", "_remoted._tcp"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if !line.starts_with("=;") {
continue;
}
let parts: Vec<&str> = line.split(';').collect();
if parts.len() < 9 {
continue;
}
let ifname = parts[1];
let resolved_host = parts[6];
let resolved_addr = parts[7];
if resolved_host == host_name && resolved_addr == service_address {
let ifindex_path = format!("/sys/class/net/{ifname}/ifindex");
return fs::read_to_string(ifindex_path).ok()?.trim().parse().ok();
}
}
None
}
#[cfg(not(target_os = "linux"))]
fn link_local_scope_id_from_avahi(_host_name: &str, _service_address: &str) -> Option<u32> {
None
}