Skip to content

Commit

Permalink
Initial revision of vmnet, Apple's vmnet.framework bindings for Rust
Browse files Browse the repository at this point in the history
  • Loading branch information
edigaryev committed Jun 8, 2022
0 parents commit cc9592c
Show file tree
Hide file tree
Showing 22 changed files with 1,315 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.aarch64-apple-darwin]
runner = 'sudo -E'
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
/Cargo.lock
24 changes: 24 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "vmnet"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[workspace]
members = ["vmnet-derive"]

[dev-dependencies]
smoltcp = "0.8.1"

[dependencies]
libc = "0.2.126"
block = "0.1.6"
uuid = { version = "1.1.1", features = ["v4"] }
hexdump = "0.1.1"
lazy_static = "1.4.0"
bitflags = "1.3.2"
num_enum = "0.5.7"
thiserror = "1.0.31"
enum-iterator = "1.1.1"
vmnet-derive = { path = "vmnet-derive" }
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# vmnet

Apple's [`vmnet.framework`](https://developer.apple.com/documentation/vmnet) bindings for Rust.

## Installation

Add this to your `Cargo.toml`:

```toml
[dependencies]
vmnet = "0.1.0"
```

## Usage

Ensure that your software either has an `com.apple.vm.networking` entitlement or is running with elevated privileges.

Start a NAT interface and receive some packets destined to it:

```rust
fn main() {
let shared_mode = Shared {
subnet_options: None,
..Default::default()
};

let mut iface = Interface::new(Mode::Shared(shared_mode), Options::default()).unwrap();

let (tx, rx) = sync::mpsc::sync_channel(0);

iface.set_event_callback(Events::PACKETS_AVAILABLE, move |events, params| {
if let Some(Parameter::EstimatedPacketsAvailable(pkts)) = params.get(ParameterKind::EstimatedPacketsAvailable) {
tx.send(pkts);
}
}).unwrap();

let pkts = rx.recv().unwrap();
println!("receiving {} packets...", pkts);
for _ in 0..pkts {
let mut buf: [u8; 1514] = [0; 1514];
println!("{:?}", iface.read(&mut buf));
}

drop(rx);
iface.finalize().unwrap();
}
```

## Quirks and missing functionality

* due to Apple's usage of [blocks](https://en.wikipedia.org/wiki/Blocks_(C_language_extension)) as a way to retrieve API call results, some methods like `set_event_callback()` require the provided closure to have a `'static` lifetime
* this manifests itself in not being able to use `Interface` from such closure
* however, this can be easily worked around by using [interior mutability pattern](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html) or simply by using the callback as a signal carrier
* no [port forwarding](https://developer.apple.com/documentation/vmnet/vmnet_functions) support
* [`vmnet_copy_shared_interface_list()`](https://developer.apple.com/documentation/vmnet/3152677-vmnet_copy_shared_interface_list) is not yet implemented
* due to `API_AVAILABLE` macro not being supported it is assumed that this package is running on macOS 11.0 or newer
13 changes: 13 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use crate::ffi::vmnet;

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("vmnet returned an error status")]
VmnetErrorStatus(vmnet::Status),
#[error("vmnet returned an unknown status: {0}")]
VmnetUnknownStatus(u32),
#[error("vmnet_start_interface() failed")]
VmnetStartInterfaceFailed,
#[error("vmnet_read() received no packets at this time")]
VmnetReadNothing,
}
7 changes: 7 additions & 0 deletions src/ffi/dispatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use std::ffi::c_void;

pub type DispatchQueueGlobalT = *mut c_void;

extern "C" {
pub fn dispatch_get_global_queue(identifier: isize, flags: usize) -> DispatchQueueGlobalT;
}
3 changes: 3 additions & 0 deletions src/ffi/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod dispatch;
pub mod vmnet;
pub mod xpc;
126 changes: 126 additions & 0 deletions src/ffi/vmnet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use crate::error;
use crate::ffi::dispatch::DispatchQueueGlobalT;
use crate::ffi::xpc::XpcObjectT;
use bitflags::bitflags;
use libc::{iovec, size_t};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::ffi::c_void;
use std::os::raw::{c_char, c_int};

pub type InterfaceRef = *mut c_void;
pub type InterfaceEventT = u32;
pub type VmnetReturnT = u32;

#[repr(C)]
#[derive(Debug)]
pub struct vmpktdesc {
pub vm_pkt_size: size_t,
pub vm_pkt_iov: *mut iovec,
pub vm_pkt_iovcnt: u32,
pub vm_flags: u32,
}

#[link(name = "vmnet", kind = "framework")]
extern "C" {
pub fn vmnet_start_interface(
interface_desc: XpcObjectT,
queue: DispatchQueueGlobalT,
handler: *mut c_void,
) -> InterfaceRef;
pub fn vmnet_interface_set_event_callback(
interface: InterfaceRef,
event_mask: InterfaceEventT,
queue: DispatchQueueGlobalT,
handler: *mut c_void,
) -> VmnetReturnT;
pub fn vmnet_read(
interface: InterfaceRef,
packets: *mut vmpktdesc,
pktcnt: *mut c_int,
) -> VmnetReturnT;
pub fn vmnet_write(
interface: InterfaceRef,
packets: *mut vmpktdesc,
pktcnt: *mut c_int,
) -> VmnetReturnT;
pub fn vmnet_stop_interface(
interface: InterfaceRef,
queue: DispatchQueueGlobalT,
handler: *mut c_void,
) -> VmnetReturnT;

// Mode selector
pub static vmnet_operation_mode_key: *const c_char;

// Generic options and parameters
pub static vmnet_mac_address_key: *const c_char;
pub static vmnet_allocate_mac_address_key: *const c_char;
pub static vmnet_interface_id_key: *const c_char;
pub static vmnet_max_packet_size_key: *const c_char;
pub static vmnet_enable_checksum_offload_key: *const c_char;
pub static vmnet_enable_isolation_key: *const c_char;
pub static vmnet_enable_tso_key: *const c_char;

// Host mode
pub static vmnet_network_identifier_key: *const c_char;
pub static vmnet_host_ip_address_key: *const c_char;
pub static vmnet_host_ipv6_address_key: *const c_char;
pub static vmnet_host_subnet_mask_key: *const c_char;

// Bridged mode
pub static vmnet_shared_interface_name_key: *const c_char;

// Shared and host modes
pub static vmnet_start_address_key: *const c_char;
pub static vmnet_end_address_key: *const c_char;
pub static vmnet_subnet_mask_key: *const c_char;
pub static vmnet_mtu_key: *const c_char;

// Shared mode
pub static vmnet_nat66_prefix_key: *const c_char;

// Event callback
pub static vmnet_estimated_packets_available_key: *const c_char;
}

#[derive(TryFromPrimitive)]
#[repr(u32)]
#[derive(Debug, Eq, PartialEq)]
pub enum Status {
Success = 1000,
Failure = 1001,
MemFailure = 1002,
InvalidArgument = 1003,
SetupIncomplete = 1004,
InvalidAccess = 1005,
PacketTooBig = 1006,
BufferExhausted = 1007,
TooManyPackets = 1008,
SharingServiceBusy = 1009,
}

impl Status {
pub fn from_ffi(status: u32) -> Result<(), error::Error> {
let status =
Status::try_from(status).map_err(|_x| error::Error::VmnetUnknownStatus(status))?;

match status {
Status::Success => Ok(()),
_ => Err(error::Error::VmnetErrorStatus(status)),
}
}
}

#[repr(u64)]
#[derive(IntoPrimitive)]
pub enum Mode {
Host = 1000,
Shared = 1001,
Bridged = 1002,
}

bitflags! {
pub struct Events: u32 {
const PACKETS_AVAILABLE = 1 << 0;
}
}
128 changes: 128 additions & 0 deletions src/ffi/xpc/dictionary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use crate::ffi::xpc::{
xpc_dictionary_apply, xpc_dictionary_create_empty, xpc_dictionary_get_value,
xpc_dictionary_set_bool, xpc_dictionary_set_string, xpc_dictionary_set_uint64,
xpc_dictionary_set_uuid, xpc_release, xpc_retain, XpcData, XpcObjectT,
};
use block::ConcreteBlock;
use std::cell::RefCell;
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::rc::Rc;

pub struct Dictionary {
xdict: XpcObjectT,
}

impl Dictionary {
pub fn new() -> Self {
Dictionary {
xdict: unsafe { xpc_dictionary_create_empty() },
}
}

pub unsafe fn from_xpc(xdict: XpcObjectT) -> Self {
Dictionary {
xdict: xpc_retain(xdict),
}
}

pub unsafe fn to_xpc(&self) -> XpcObjectT {
self.xdict
}

pub fn get(&self, key: String) -> Option<XpcData> {
let key_ = CString::new(key).unwrap();
let key = key_.as_ptr();

let xpc_value = unsafe { xpc_dictionary_get_value(self.xdict, key) };
if xpc_value.is_null() {
return None;
}

unsafe { XpcData::from_xpc_value(xpc_value) }
}

pub fn set(&mut self, key: String, value: XpcData) {
let key_ = CString::new(key).unwrap();
let key = key_.as_ptr();

match value {
XpcData::Uint64(value) => unsafe { xpc_dictionary_set_uint64(self.xdict, key, value) },
XpcData::String(value) => {
let c_string = CString::new(value).unwrap();
unsafe { xpc_dictionary_set_string(self.xdict, key, c_string.as_ptr()) }
}
XpcData::Bool(value) => unsafe { xpc_dictionary_set_bool(self.xdict, key, value) },
XpcData::Uuid(value) => unsafe {
xpc_dictionary_set_uuid(self.xdict, key, value.as_bytes().as_ptr())
},
};
}
}

impl Default for Dictionary {
fn default() -> Self {
Self::new()
}
}

impl Clone for Dictionary {
fn clone(&self) -> Self {
Dictionary {
xdict: unsafe { xpc_retain(self.xdict) },
}
}
}

impl Drop for Dictionary {
fn drop(&mut self) {
unsafe { xpc_release(self.xdict) }
}
}

impl From<HashMap<String, XpcData>> for Dictionary {
fn from(from: HashMap<String, XpcData>) -> Self {
let mut result = Dictionary::new();

for (key, value) in from {
result.set(key, value);
}

result
}
}

impl TryFrom<Dictionary> for HashMap<String, XpcData> {
type Error = ();

fn try_from(value: Dictionary) -> Result<Self, Self::Error> {
let result = Rc::new(RefCell::new(HashMap::new()));
let result_weak = Rc::downgrade(&result);

let block = ConcreteBlock::new(move |key: *const c_char, value: XpcObjectT| -> bool {
let key = unsafe { CStr::from_ptr(key).to_string_lossy().to_string() };

let value = match unsafe { XpcData::from_xpc_value(value) } {
Some(value) => value,
None => return false,
};

result_weak
.upgrade()
.unwrap()
.borrow_mut()
.insert(key, value);

true
});
let block = block.copy();

let ret = unsafe { xpc_dictionary_apply(value.xdict, &*block as *const _ as *mut _) };
if !ret {
return Err(());
}

Ok(Rc::try_unwrap(result).unwrap().into_inner())
}
}
Loading

0 comments on commit cc9592c

Please sign in to comment.