From 2a90f926ca8b334e9139db7a1fd75782f03c5dbf Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Wed, 20 Aug 2025 12:42:40 -0600 Subject: [PATCH] Implement FFI object stack --- cpp/include/idevice++/tcp_object_stack.hpp | 175 +++++++++++++++++ cpp/src/tcp_callback_feeder.cpp | 113 +++++++++++ ffi/src/lib.rs | 2 + ffi/src/tcp_object_stack.rs | 217 +++++++++++++++++++++ 4 files changed, 507 insertions(+) create mode 100644 cpp/include/idevice++/tcp_object_stack.hpp create mode 100644 cpp/src/tcp_callback_feeder.cpp create mode 100644 ffi/src/tcp_object_stack.rs diff --git a/cpp/include/idevice++/tcp_object_stack.hpp b/cpp/include/idevice++/tcp_object_stack.hpp new file mode 100644 index 0000000..d4e1363 --- /dev/null +++ b/cpp/include/idevice++/tcp_object_stack.hpp @@ -0,0 +1,175 @@ +// Jackson Coxson + +#pragma once +#include +#include +#include +#include +#include + +#include +#include + +namespace IdeviceFFI { + +// ---------------- OwnedBuffer: RAII for zero-copy read buffers ---------------- +class OwnedBuffer { + public: + OwnedBuffer() noexcept : p_(nullptr), n_(0) {} + OwnedBuffer(const OwnedBuffer&) = delete; + OwnedBuffer& operator=(const OwnedBuffer&) = delete; + + OwnedBuffer(OwnedBuffer&& o) noexcept : p_(o.p_), n_(o.n_) { + o.p_ = nullptr; + o.n_ = 0; + } + OwnedBuffer& operator=(OwnedBuffer&& o) noexcept { + if (this != &o) { + reset(); + p_ = o.p_; + n_ = o.n_; + o.p_ = nullptr; + o.n_ = 0; + } + return *this; + } + + ~OwnedBuffer() { reset(); } + + const uint8_t* data() const noexcept { return p_; } + uint8_t* data() noexcept { return p_; } + std::size_t size() const noexcept { return n_; } + bool empty() const noexcept { return n_ == 0; } + + void reset() noexcept { + if (p_) { + ::idevice_data_free(p_, n_); + p_ = nullptr; + n_ = 0; + } + } + + private: + friend class TcpObjectStackEater; + void adopt(uint8_t* p, std::size_t n) noexcept { + reset(); + p_ = p; + n_ = n; + } + + uint8_t* p_; + std::size_t n_; +}; + +// ---------------- TcpFeeder: push inbound IP packets into the stack ---------- +class TcpObjectStackFeeder { + public: + TcpObjectStackFeeder() = default; + TcpObjectStackFeeder(const TcpObjectStackFeeder&) = delete; + TcpObjectStackFeeder& operator=(const TcpObjectStackFeeder&) = delete; + + TcpObjectStackFeeder(TcpObjectStackFeeder&& o) noexcept : h_(o.h_) { o.h_ = nullptr; } + TcpObjectStackFeeder& operator=(TcpObjectStackFeeder&& o) noexcept { + if (this != &o) { + reset(); + h_ = o.h_; + o.h_ = nullptr; + } + return *this; + } + + ~TcpObjectStackFeeder() { reset(); } + + bool write(const uint8_t* data, std::size_t len, FfiError& err) const; + ::TcpFeedObject* raw() const { return h_; } + + private: + friend class TcpObjectStack; + explicit TcpObjectStackFeeder(::TcpFeedObject* h) : h_(h) {} + + void reset() { + if (h_) { + ::idevice_free_tcp_feed_object(h_); + h_ = nullptr; + } + } + + ::TcpFeedObject* h_ = nullptr; +}; + +// ---------------- TcpEater: blocking read of outbound packets ---------------- +class TcpObjectStackEater { + public: + TcpObjectStackEater() = default; + TcpObjectStackEater(const TcpObjectStackEater&) = delete; + TcpObjectStackEater& operator=(const TcpObjectStackEater&) = delete; + + TcpObjectStackEater(TcpObjectStackEater&& o) noexcept : h_(o.h_) { o.h_ = nullptr; } + TcpObjectStackEater& operator=(TcpObjectStackEater&& o) noexcept { + if (this != &o) { + reset(); + h_ = o.h_; + o.h_ = nullptr; + } + return *this; + } + + ~TcpObjectStackEater() { reset(); } + + // Blocks until a packet is available. On success, 'out' adopts the buffer + // and you must keep 'out' alive until done (RAII frees via idevice_data_free). + bool read(OwnedBuffer& out, FfiError& err) const; + + ::TcpEatObject* raw() const { return h_; } + + private: + friend class TcpObjectStack; + explicit TcpObjectStackEater(::TcpEatObject* h) : h_(h) {} + + void reset() { + if (h_) { + ::idevice_free_tcp_eat_object(h_); + h_ = nullptr; + } + } + + ::TcpEatObject* h_ = nullptr; +}; + +// ---------------- Stack builder: returns feeder + eater + adapter ------------ +class TcpObjectStack { + public: + TcpObjectStack() = default; + TcpObjectStack(const TcpObjectStack&) = delete; // no sharing + TcpObjectStack& operator=(const TcpObjectStack&) = delete; + TcpObjectStack(TcpObjectStack&&) noexcept = default; // movable + TcpObjectStack& operator=(TcpObjectStack&&) noexcept = default; + + // Build the stack (dual-handle). Name kept to minimize churn. + static std::optional + create(const std::string& our_ip, const std::string& their_ip, FfiError& err); + + TcpObjectStackFeeder& feeder(); + const TcpObjectStackFeeder& feeder() const; + + TcpObjectStackEater& eater(); + const TcpObjectStackEater& eater() const; + + Adapter& adapter(); + const Adapter& adapter() const; + + std::optional release_feeder(); // nullptr inside wrapper after call + std::optional release_eater(); // nullptr inside wrapper after call + std::optional release_adapter(); + + private: + struct Impl { + TcpObjectStackFeeder feeder; + TcpObjectStackEater eater; + std::optional adapter; + }; + // Unique ownership so there’s a single point of truth to release from + std::unique_ptr impl_; +}; + +} // namespace IdeviceFFI diff --git a/cpp/src/tcp_callback_feeder.cpp b/cpp/src/tcp_callback_feeder.cpp new file mode 100644 index 0000000..9967554 --- /dev/null +++ b/cpp/src/tcp_callback_feeder.cpp @@ -0,0 +1,113 @@ +// Jackson Coxson + +#include +#include + +namespace IdeviceFFI { + +// ---------- TcpFeeder ---------- +bool TcpObjectStackFeeder::write(const uint8_t* data, std::size_t len, FfiError& err) const { + if (IdeviceFfiError* e = ::idevice_tcp_feed_object_write(h_, data, len)) { + err = FfiError(e); + return false; + } + return true; +} + +// ---------- TcpEater ---------- +bool TcpObjectStackEater::read(OwnedBuffer& out, FfiError& err) const { + uint8_t* ptr = nullptr; + std::size_t len = 0; + if (IdeviceFfiError* e = ::idevice_tcp_eat_object_read(h_, &ptr, &len)) { + err = FfiError(e); + return false; + } + // Success: adopt the buffer (freed via idevice_data_free in OwnedBuffer dtor) + out.adopt(ptr, len); + return true; +} + +// ---------- TcpStackFromCallback ---------- +std::optional +TcpObjectStack::create(const std::string& our_ip, const std::string& their_ip, FfiError& err) { + ::TcpFeedObject* feeder_h = nullptr; + ::TcpEatObject* eater_h = nullptr; + ::AdapterHandle* adapter_h = nullptr; + + if (IdeviceFfiError* e = ::idevice_tcp_stack_into_sync_objects( + our_ip.c_str(), their_ip.c_str(), &feeder_h, &eater_h, &adapter_h)) { + err = FfiError(e); + return std::nullopt; + } + + auto impl = std::make_unique(); + impl->feeder = TcpObjectStackFeeder(feeder_h); + impl->eater = TcpObjectStackEater(eater_h); + impl->adapter = Adapter::adopt(adapter_h); + + TcpObjectStack out; + out.impl_ = std::move(impl); + return out; +} + +TcpObjectStackFeeder& TcpObjectStack::feeder() { + return impl_->feeder; +} +const TcpObjectStackFeeder& TcpObjectStack::feeder() const { + return impl_->feeder; +} + +TcpObjectStackEater& TcpObjectStack::eater() { + return impl_->eater; +} +const TcpObjectStackEater& TcpObjectStack::eater() const { + return impl_->eater; +} + +Adapter& TcpObjectStack::adapter() { + if (!impl_ || !impl_->adapter) { + static Adapter* never = nullptr; + return *never; + } + return *(impl_->adapter); +} +const Adapter& TcpObjectStack::adapter() const { + if (!impl_ || !impl_->adapter) { + static Adapter* never = nullptr; + return *never; + } + return *(impl_->adapter); +} + +// ---------- Release APIs ---------- +std::optional TcpObjectStack::release_feeder() { + if (!impl_) + return std::nullopt; + auto has = impl_->feeder.raw() != nullptr; + if (!has) + return std::nullopt; + TcpObjectStackFeeder out = std::move(impl_->feeder); + // impl_->feeder is now empty (h_ == nullptr) thanks to move + return std::optional(std::move(out)); +} + +std::optional TcpObjectStack::release_eater() { + if (!impl_) + return std::nullopt; + auto has = impl_->eater.raw() != nullptr; + if (!has) + return std::nullopt; + TcpObjectStackEater out = std::move(impl_->eater); + return std::optional(std::move(out)); +} + +std::optional TcpObjectStack::release_adapter() { + if (!impl_ || !impl_->adapter) + return std::nullopt; + // Move out and clear our optional + auto out = std::move(*(impl_->adapter)); + impl_->adapter.reset(); + return std::optional(std::move(out)); +} + +} // namespace IdeviceFFI diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 9bd1833..f5738ab 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -39,6 +39,8 @@ pub mod rsd; pub mod springboardservices; #[cfg(feature = "syslog_relay")] pub mod syslog_relay; +#[cfg(feature = "tunnel_tcp_stack")] +pub mod tcp_object_stack; #[cfg(feature = "usbmuxd")] pub mod usbmuxd; pub mod util; diff --git a/ffi/src/tcp_object_stack.rs b/ffi/src/tcp_object_stack.rs new file mode 100644 index 0000000..70e844c --- /dev/null +++ b/ffi/src/tcp_object_stack.rs @@ -0,0 +1,217 @@ +//! Just to make things more complicated, some setups need an IP input from FFI. Or maybe a packet +//! input that is sync only. This is a stupid simple shim between callbacks and an input for the +//! legendary idevice TCP stack. + +use std::{ + ffi::{CStr, c_char, c_void}, + ptr::null_mut, + sync::Arc, +}; + +use log::debug; +use tokio::sync::Mutex; +use tokio::{ + io::AsyncWriteExt, + net::tcp::{OwnedReadHalf, OwnedWriteHalf}, +}; + +use crate::{IdeviceFfiError, RUNTIME, core_device_proxy::AdapterHandle, ffi_err}; + +pub struct TcpFeedObject { + sender: Arc>, +} +pub struct TcpEatObject { + receiver: Arc>, +} + +#[repr(transparent)] +#[derive(Clone)] +pub struct UserContext(*mut c_void); +unsafe impl Send for UserContext {} +unsafe impl Sync for UserContext {} + +/// # Safety +/// Pass valid pointers. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_tcp_stack_into_sync_objects( + our_ip: *const c_char, + their_ip: *const c_char, + feeder: *mut *mut TcpFeedObject, // feed the TCP stack with IP packets + tcp_receiver: *mut *mut TcpEatObject, + adapter_handle: *mut *mut AdapterHandle, // this object can be used throughout the rest of the + // idevice ecosystem +) -> *mut IdeviceFfiError { + if our_ip.is_null() || their_ip.is_null() || feeder.is_null() || adapter_handle.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + + let our_ip = unsafe { CStr::from_ptr(our_ip) } + .to_string_lossy() + .to_string(); + let our_ip = match our_ip.parse::() { + Ok(o) => o, + Err(_) => { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + }; + let their_ip = unsafe { CStr::from_ptr(their_ip) } + .to_string_lossy() + .to_string(); + let their_ip = match their_ip.parse::() { + Ok(o) => o, + Err(_) => { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + }; + + let res = RUNTIME.block_on(async { + let mut port = 4000; + loop { + if port > 4050 { + return None; + } + let listener = match tokio::net::TcpListener::bind(format!("127.0.0.1:{port}")).await { + Ok(l) => l, + Err(_) => { + port += 1; + continue; + } + }; + + let stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")) + .await + .ok()?; + stream.set_nodelay(true).ok()?; + let (stream2, _) = listener.accept().await.ok()?; + stream2.set_nodelay(true).ok()?; + break Some((stream, stream2)); + } + }); + + let (stream, stream2) = match res { + Some(x) => x, + None => { + return ffi_err!(IdeviceError::NoEstablishedConnection); + } + }; + + let (r, w) = stream2.into_split(); + let w = Arc::new(Mutex::new(w)); + let r = Arc::new(Mutex::new(r)); + + // let w = Arc::new(Mutex::new(stream2)); + // let r = w.clone(); + + let feed_object = TcpFeedObject { sender: w }; + let eat_object = TcpEatObject { receiver: r }; + + // we must be inside the runtime for the inner function to spawn threads + let new_adapter = RUNTIME.block_on(async { + idevice::tcp::adapter::Adapter::new(Box::new(stream), our_ip, their_ip).to_async_handle() + }); + // this object can now be used with the rest of the idevice FFI library + + unsafe { + *feeder = Box::into_raw(Box::new(feed_object)); + *tcp_receiver = Box::into_raw(Box::new(eat_object)); + *adapter_handle = Box::into_raw(Box::new(AdapterHandle(new_adapter))); + } + + null_mut() +} + +/// Feed the TCP stack with data +/// # Safety +/// Pass valid pointers. Data is cloned out of slice. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_tcp_feed_object_write( + object: *mut TcpFeedObject, + data: *const u8, + len: usize, +) -> *mut IdeviceFfiError { + if object.is_null() || data.is_null() { + return ffi_err!(IdeviceError::FfiInvalidArg); + } + let object = unsafe { &mut *object }; + let data = unsafe { std::slice::from_raw_parts(data, len) }; + RUNTIME.block_on(async move { + let mut lock = object.sender.lock().await; + match lock.write_all(data).await { + Ok(_) => { + lock.flush().await.ok(); + null_mut() + } + Err(e) => { + ffi_err!(IdeviceError::Socket(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + format!("could not send: {e:?}") + ))) + } + } + }) +} + +/// Block on getting a block of data to write to the underlying stream. +/// Write this to the stream as is, and free the data with idevice_data_free +/// +/// # Safety +/// Pass valid pointers +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_tcp_eat_object_read( + object: *mut TcpEatObject, + data: *mut *mut u8, + len: *mut usize, +) -> *mut IdeviceFfiError { + let object = unsafe { &mut *object }; + let mut buf = [0; 2048]; + RUNTIME.block_on(async { + let lock = object.receiver.lock().await; + match lock.try_read(&mut buf) { + Ok(size) => { + debug!("EATING"); + let bytes = buf[..size].to_vec(); + let mut res = bytes.into_boxed_slice(); + unsafe { + *len = res.len(); + *data = res.as_mut_ptr(); + } + std::mem::forget(res); + std::ptr::null_mut() + } + Err(e) => match e.kind() { + std::io::ErrorKind::WouldBlock => { + unsafe { + *len = 0; + } + std::ptr::null_mut() + } + _ => { + ffi_err!(IdeviceError::Socket(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "channel closed" + ))) + } + }, + } + }) +} + +/// # Safety +/// Pass a valid pointer allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_free_tcp_feed_object(object: *mut TcpFeedObject) { + if object.is_null() { + return; + } + let _ = unsafe { Box::from_raw(object) }; +} + +/// # Safety +/// Pass a valid pointer allocated by this library +#[unsafe(no_mangle)] +pub unsafe extern "C" fn idevice_free_tcp_eat_object(object: *mut TcpEatObject) { + if object.is_null() { + return; + } + let _ = unsafe { Box::from_raw(object) }; +}