use std::sync::Arc;
use parking_lot::Mutex;
use super::{
VmnetReturn,
block::{StartContext, start_block, stop_block},
dispatch::{DISPATCH_TIME_FOREVER, Queue, Semaphore, dispatch_time_now_plus_ns},
vmnet::{
Iovec, VmPktDesc, keys, vmnet_read, vmnet_start_interface, vmnet_stop_interface,
vmnet_write,
},
xpc::XpcObject,
};
#[derive(Debug, Default, Clone, Copy)]
pub struct IfaceStats {
pub rx_bytes: u64,
pub tx_bytes: u64,
pub rx_frames: u64,
pub tx_frames: u64,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum InnerError {
#[error("dispatch queue allocation failed: {0}")]
DispatchQueue(#[source] std::io::Error),
#[error("vmnet_start_interface failed: code={code:?}")]
StartFailed {
code: VmnetReturn,
},
#[error("vmnet_stop_interface failed: code={code:?}")]
StopFailed {
code: VmnetReturn,
},
#[error("vmnet_read failed: code={code:?}")]
ReadFailed {
code: VmnetReturn,
},
#[error("vmnet_write failed: code={code:?}")]
WriteFailed {
code: VmnetReturn,
},
#[error("vmnet_start_interface timed out after {timeout_ms} ms")]
StartTimeout {
timeout_ms: u64,
},
#[error("vmnet returned an incomplete parameter dictionary: missing {field}")]
MissingField {
field: &'static str,
},
}
pub const BATCH: usize = 32;
#[derive(Debug, Clone)]
pub struct StartParams {
pub iface_id: String,
pub mode: u64,
pub bridged_iface_name: Option<String>,
pub mtu: Option<u32>,
pub start_timeout_ms: u64,
pub enable_isolation: bool,
}
pub struct Inner {
handle: *mut std::os::raw::c_void,
queue: Queue,
mtu: u32,
max_packet_size: u32,
mac: [u8; 6],
stats: Arc<Mutex<IfaceStats>>,
}
impl std::fmt::Debug for Inner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VmnetInner")
.field("handle_is_null", &self.handle.is_null())
.field("mtu", &self.mtu)
.field("max_packet_size", &self.max_packet_size)
.field("mac", &self.mac)
.finish_non_exhaustive()
}
}
unsafe impl Send for Inner {}
unsafe impl Sync for Inner {}
impl Inner {
pub fn start(params: &StartParams) -> Result<Self, InnerError> {
let queue = Queue::create_serial(&format!("squib-net.{}", params.iface_id))
.map_err(InnerError::DispatchQueue)?;
let descriptor = build_descriptor(params);
let semaphore = Semaphore::new();
let ctx = StartContext::new(semaphore);
let block = start_block(Arc::clone(&ctx));
let handle = unsafe {
vmnet_start_interface(
descriptor.as_ptr(),
queue.as_ptr(),
super::block::as_raw_start(&block),
)
};
if handle.is_null() {
return Err(InnerError::StartFailed {
code: VmnetReturn::Failure,
});
}
let timeout = if params.start_timeout_ms == 0 {
DISPATCH_TIME_FOREVER
} else {
dispatch_time_now_plus_ns(params.start_timeout_ms.saturating_mul(1_000_000))
};
if !ctx.semaphore.wait(timeout) {
return Err(InnerError::StartTimeout {
timeout_ms: params.start_timeout_ms,
});
}
let outcome = ctx.outcome.lock().take().ok_or(InnerError::StartFailed {
code: VmnetReturn::Failure,
})?;
if outcome.status != VmnetReturn::Success {
return Err(InnerError::StartFailed {
code: outcome.status,
});
}
let param = outcome.param.ok_or(InnerError::StartFailed {
code: VmnetReturn::Failure,
})?;
let mtu = u32::try_from(param.get_uint64(keys::MTU)).unwrap_or(1500);
let max_packet_size =
u32::try_from(param.get_uint64(keys::MAX_PACKET_SIZE)).unwrap_or(1518);
let mac_str = param
.get_string(keys::MAC_ADDRESS)
.ok_or(InnerError::MissingField {
field: "vmnet_mac_address_key",
})?;
let mac = parse_mac_string(&mac_str).ok_or(InnerError::MissingField {
field: "vmnet_mac_address_key (parse)",
})?;
tracing::info!(
iface_id = %params.iface_id,
mtu,
max_packet_size,
mac = %mac_str,
"vmnet interface up"
);
Ok(Self {
handle,
queue,
mtu,
max_packet_size,
mac,
stats: Arc::new(Mutex::new(IfaceStats::default())),
})
}
pub fn stop(&mut self) -> Result<(), InnerError> {
if self.handle.is_null() {
return Ok(());
}
let semaphore = Semaphore::new();
let ctx = StartContext::new(semaphore);
let block = stop_block(Arc::clone(&ctx));
let rc = unsafe {
vmnet_stop_interface(
self.handle,
self.queue.as_ptr(),
super::block::as_raw_stop(&block),
)
};
if rc != VmnetReturn::Success as u32 {
self.handle = std::ptr::null_mut();
return Err(InnerError::StopFailed {
code: VmnetReturn::from_raw(rc),
});
}
ctx.semaphore.wait(DISPATCH_TIME_FOREVER);
self.handle = std::ptr::null_mut();
Ok(())
}
pub fn read_into_sized(
&self,
frames: &mut [&mut [u8]],
sizes: &mut [usize],
) -> Result<usize, InnerError> {
let n = frames.len().min(BATCH).min(sizes.len());
if n == 0 || self.handle.is_null() {
return Ok(0);
}
let mut iovecs: [Iovec; BATCH] = [Iovec {
iov_base: std::ptr::null_mut(),
iov_len: 0,
}; BATCH];
let mut pkts: [VmPktDesc; BATCH] = [VmPktDesc {
vm_pkt_size: 0,
vm_pkt_iov: std::ptr::null_mut(),
vm_pkt_iovcnt: 0,
vm_flags: 0,
}; BATCH];
for (i, buf) in frames.iter_mut().take(n).enumerate() {
iovecs[i] = Iovec {
iov_base: buf.as_mut_ptr().cast(),
iov_len: buf.len(),
};
pkts[i] = VmPktDesc {
vm_pkt_size: buf.len(),
vm_pkt_iov: &raw mut iovecs[i],
vm_pkt_iovcnt: 1,
vm_flags: 0,
};
}
let mut count: std::os::raw::c_int = i32::try_from(n).unwrap_or(i32::MAX);
let rc = unsafe { vmnet_read(self.handle, pkts.as_mut_ptr(), &raw mut count) };
if rc != VmnetReturn::Success as u32 {
return Err(InnerError::ReadFailed {
code: VmnetReturn::from_raw(rc),
});
}
let delivered = usize::try_from(count.max(0)).unwrap_or(0).min(n);
let mut bytes = 0u64;
for (i, len_slot) in sizes.iter_mut().take(delivered).enumerate() {
*len_slot = pkts[i].vm_pkt_size;
bytes = bytes.saturating_add(*len_slot as u64);
}
let mut stats = self.stats.lock();
stats.rx_bytes = stats.rx_bytes.saturating_add(bytes);
stats.rx_frames = stats.rx_frames.saturating_add(delivered as u64);
Ok(delivered)
}
pub fn write_frames(&self, frames: &[&[u8]]) -> Result<usize, InnerError> {
let n = frames.len().min(BATCH);
if n == 0 || self.handle.is_null() {
return Ok(0);
}
let mut iovecs: [Iovec; BATCH] = [Iovec {
iov_base: std::ptr::null_mut(),
iov_len: 0,
}; BATCH];
let mut pkts: [VmPktDesc; BATCH] = [VmPktDesc {
vm_pkt_size: 0,
vm_pkt_iov: std::ptr::null_mut(),
vm_pkt_iovcnt: 0,
vm_flags: 0,
}; BATCH];
for (i, frame) in frames.iter().take(n).enumerate() {
iovecs[i] = Iovec {
iov_base: frame.as_ptr().cast::<std::os::raw::c_void>().cast_mut(),
iov_len: frame.len(),
};
pkts[i] = VmPktDesc {
vm_pkt_size: frame.len(),
vm_pkt_iov: &raw mut iovecs[i],
vm_pkt_iovcnt: 1,
vm_flags: 0,
};
}
let mut count: std::os::raw::c_int = i32::try_from(n).unwrap_or(i32::MAX);
let rc = unsafe { vmnet_write(self.handle, pkts.as_mut_ptr(), &raw mut count) };
if rc != VmnetReturn::Success as u32 {
return Err(InnerError::WriteFailed {
code: VmnetReturn::from_raw(rc),
});
}
let delivered = usize::try_from(count.max(0)).unwrap_or(0).min(n);
let mut bytes = 0u64;
for frame in frames.iter().take(delivered) {
bytes = bytes.saturating_add(frame.len() as u64);
}
let mut stats = self.stats.lock();
stats.tx_bytes = stats.tx_bytes.saturating_add(bytes);
stats.tx_frames = stats.tx_frames.saturating_add(delivered as u64);
Ok(delivered)
}
pub fn mtu(&self) -> u32 {
self.mtu
}
pub fn max_packet_size(&self) -> u32 {
self.max_packet_size
}
pub fn host_mac(&self) -> [u8; 6] {
self.mac
}
pub fn stats_snapshot(&self) -> IfaceStats {
*self.stats.lock()
}
}
impl Drop for Inner {
fn drop(&mut self) {
if !self.handle.is_null() {
let _ = self.stop();
}
}
}
fn build_descriptor(params: &StartParams) -> XpcObject {
let dict = XpcObject::new_dictionary();
dict.set_uint64(keys::OPERATION_MODE, params.mode);
dict.set_uuid(keys::INTERFACE_ID, &iface_uuid_for(¶ms.iface_id));
if let Some(mtu) = params.mtu {
dict.set_uint64(keys::MTU, u64::from(mtu));
}
if let Some(name) = ¶ms.bridged_iface_name {
dict.set_string(keys::SHARED_INTERFACE_NAME, name);
}
dict.set_bool(keys::ENABLE_ISOLATION, params.enable_isolation);
dict
}
fn iface_uuid_for(iface_id: &str) -> [u8; 16] {
*uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, iface_id.as_bytes()).as_bytes()
}
fn parse_mac_string(raw: &str) -> Option<[u8; 6]> {
let mut out = [0u8; 6];
let parts: Vec<&str> = raw.split(':').collect();
if parts.len() != 6 {
return None;
}
for (i, p) in parts.iter().enumerate() {
out[i] = u8::from_str_radix(p, 16).ok()?;
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_should_render_iface_uuid_as_16_raw_bytes() {
let bytes = iface_uuid_for("eth0");
assert_eq!(bytes.len(), 16);
assert_eq!(iface_uuid_for("eth0"), bytes);
assert_ne!(iface_uuid_for("eth1"), bytes);
}
#[test]
fn test_should_parse_mac_string() {
let mac = parse_mac_string("06:00:ac:10:00:02").unwrap();
assert_eq!(mac, [0x06, 0x00, 0xAC, 0x10, 0x00, 0x02]);
}
}