use std::ffi::CString;
use std::net::Ipv4Addr;
use std::os::raw::c_int;
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::error::{Result, VmnetError};
use crate::ffi::*;
#[derive(Debug, Clone)]
pub struct VmnetInterfaceInfo {
pub mac: [u8; 6],
pub mtu: u16,
pub max_packet_size: usize,
}
const DEFAULT_MTU: u16 = 1500;
const DEFAULT_MAX_PACKET_SIZE: usize = 1518;
#[derive(Debug, Clone)]
pub struct VmnetConfig {
pub mode: VmnetMode,
pub mac: Option<[u8; 6]>,
pub mtu: u16,
pub bridge_interface: Option<String>,
pub dhcp_start: Option<Ipv4Addr>,
pub dhcp_end: Option<Ipv4Addr>,
pub subnet_mask: Option<Ipv4Addr>,
pub isolated: bool,
}
impl Default for VmnetConfig {
fn default() -> Self {
Self {
mode: VmnetMode::Shared,
mac: None,
mtu: DEFAULT_MTU,
bridge_interface: None,
dhcp_start: None,
dhcp_end: None,
subnet_mask: None,
isolated: false,
}
}
}
impl VmnetConfig {
#[must_use]
pub fn shared() -> Self {
Self {
mode: VmnetMode::Shared,
..Default::default()
}
}
#[must_use]
pub fn host_only() -> Self {
Self {
mode: VmnetMode::HostOnly,
..Default::default()
}
}
#[must_use]
pub fn bridged(interface: impl Into<String>) -> Self {
Self {
mode: VmnetMode::Bridged,
bridge_interface: Some(interface.into()),
..Default::default()
}
}
#[must_use]
pub fn with_mac(mut self, mac: [u8; 6]) -> Self {
self.mac = Some(mac);
self
}
#[must_use]
pub fn with_mtu(mut self, mtu: u16) -> Self {
self.mtu = mtu;
self
}
#[must_use]
pub fn with_dhcp_range(mut self, start: Ipv4Addr, end: Ipv4Addr) -> Self {
self.dhcp_start = Some(start);
self.dhcp_end = Some(end);
self
}
#[must_use]
pub fn with_subnet_mask(mut self, mask: Ipv4Addr) -> Self {
self.subnet_mask = Some(mask);
self
}
#[must_use]
pub fn with_isolation(mut self, isolated: bool) -> Self {
self.isolated = isolated;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VmnetMode {
#[default]
Shared,
HostOnly,
Bridged,
}
impl From<VmnetMode> for VmnetOperatingMode {
fn from(mode: VmnetMode) -> Self {
match mode {
VmnetMode::Shared => Self::Shared,
VmnetMode::HostOnly => Self::Host,
VmnetMode::Bridged => Self::Bridged,
}
}
}
type StartResult = (
VmnetInterfaceRef,
[u8; 6],
u16,
usize,
Option<VmnetInterfaceInfo>,
);
pub struct Vmnet {
interface: VmnetInterfaceRef,
queue: DispatchQueue,
mac: [u8; 6],
mtu: u16,
max_packet_size: usize,
running: AtomicBool,
interface_info: Option<VmnetInterfaceInfo>,
}
unsafe impl Send for Vmnet {}
unsafe impl Sync for Vmnet {}
impl Vmnet {
pub fn new(config: VmnetConfig) -> Result<Self> {
let queue_label = CString::new("com.arcbox.vmnet").unwrap();
let queue = unsafe { dispatch_queue_create(queue_label.as_ptr(), ptr::null()) };
if queue.is_null() {
return Err(VmnetError::config("failed to create dispatch queue"));
}
let config_dict: *mut std::ffi::c_void = unsafe {
let dict = xpc_dictionary_create(ptr::null(), ptr::null(), 0);
if dict.is_null() {
dispatch_release(queue.cast());
return Err(VmnetError::config("failed to create xpc config dictionary"));
}
let mode_value: u64 = VmnetOperatingMode::from(config.mode) as u64;
xpc_dictionary_set_uint64(dict, vmnet_operation_mode_key, mode_value);
if let Some(mac) = config.mac {
let mac_str = std::ffi::CString::new(format!(
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
))
.unwrap();
xpc_dictionary_set_string(dict, vmnet_mac_address_key, mac_str.as_ptr());
}
xpc_dictionary_set_uint64(dict, vmnet_mtu_key, u64::from(config.mtu));
match config.mode {
VmnetMode::Shared => {
if let Some(start) = config.dhcp_start {
let s = std::ffi::CString::new(start.to_string()).unwrap();
xpc_dictionary_set_string(dict, vmnet_start_address_key, s.as_ptr());
}
if let Some(end) = config.dhcp_end {
let s = std::ffi::CString::new(end.to_string()).unwrap();
xpc_dictionary_set_string(dict, vmnet_end_address_key, s.as_ptr());
}
if let Some(mask) = config.subnet_mask {
let s = std::ffi::CString::new(mask.to_string()).unwrap();
xpc_dictionary_set_string(dict, vmnet_subnet_mask_key, s.as_ptr());
}
}
VmnetMode::HostOnly => {
if config.isolated {
xpc_dictionary_set_bool(dict, vmnet_enable_isolation_key, true);
let uuid_bytes = uuid::Uuid::new_v4();
let xpc_uuid = xpc_uuid_create(uuid_bytes.as_bytes().as_ptr());
xpc_dictionary_set_value(dict, vmnet_interface_id_key, xpc_uuid);
xpc_release(xpc_uuid);
}
}
VmnetMode::Bridged => {
let Some(ref iface) = config.bridge_interface else {
xpc_release(dict);
dispatch_release(queue.cast());
return Err(VmnetError::config("bridge mode requires interface name"));
};
let s = match std::ffi::CString::new(iface.as_str()) {
Ok(s) => s,
Err(_) => {
xpc_release(dict);
dispatch_release(queue.cast());
return Err(VmnetError::config(
"bridge interface name contains a NUL byte",
));
}
};
xpc_dictionary_set_string(dict, vmnet_shared_interface_name_key, s.as_ptr());
}
}
dict
};
#[cfg(feature = "vmnet")]
let (interface, mac, mtu, max_packet_size, interface_info) =
{ Self::start_with_completion_handler(config_dict, queue, &config)? };
#[cfg(not(feature = "vmnet"))]
let (interface, mac, mtu, max_packet_size, interface_info) =
{ Self::start_with_null_handler(config_dict, queue, &config)? };
Ok(Self {
interface,
queue,
mac,
mtu,
max_packet_size,
running: AtomicBool::new(true),
interface_info,
})
}
#[cfg(feature = "vmnet")]
fn start_with_completion_handler(
config_dict: XpcObjectT,
queue: DispatchQueue,
config: &VmnetConfig,
) -> Result<StartResult> {
use std::ffi::CStr;
let sema = unsafe { dispatch_semaphore_create(0) };
if sema.is_null() {
unsafe {
xpc_release(config_dict);
dispatch_release(queue.cast());
}
return Err(VmnetError::config("failed to create dispatch semaphore"));
}
let block = unsafe { create_vmnet_completion_block(sema) };
let interface = unsafe { vmnet_start_interface(config_dict, queue, block) };
let timeout_ns = 10_i64
.checked_mul(i64::try_from(NSEC_PER_SEC).expect("NSEC_PER_SEC fits in i64"))
.expect("vmnet start timeout fits in i64");
let timeout = unsafe { dispatch_time(DISPATCH_TIME_NOW, timeout_ns) };
let wait_result = unsafe { dispatch_semaphore_wait(sema, timeout) };
if wait_result != 0 {
unsafe { xpc_release(config_dict) };
return Err(VmnetError::config(
"vmnet_start_interface timed out after 10s (completion handler never fired)",
));
}
let (status, xpc_params) = unsafe {
let b = block.cast::<VmnetCompletionBlock>();
((*b).status, (*b).interface_param)
};
unsafe { xpc_release(config_dict) };
if !status.is_success() || interface.is_null() {
unsafe {
if !xpc_params.is_null() {
xpc_release(xpc_params);
}
_Block_release(block);
dispatch_release(sema);
dispatch_release(queue.cast());
}
return Err(VmnetError::config(format!(
"vmnet_start_interface failed: {} (requires entitlement or root)",
status.message()
)));
}
let mut info = VmnetInterfaceInfo {
mac: [0u8; 6],
mtu: DEFAULT_MTU,
max_packet_size: DEFAULT_MAX_PACKET_SIZE,
};
if !xpc_params.is_null() {
let mac_key = c"mac_address";
let mac_ptr = unsafe { xpc_dictionary_get_string(xpc_params, mac_key.as_ptr()) };
if !mac_ptr.is_null() {
let mac_cstr = unsafe { CStr::from_ptr(mac_ptr) };
if let Ok(mac_str) = mac_cstr.to_str() {
if let Some(parsed) = parse_mac(mac_str) {
info.mac = parsed;
}
}
}
let mtu_key = c"mtu";
let mtu_val = unsafe { xpc_dictionary_get_uint64(xpc_params, mtu_key.as_ptr()) };
if mtu_val > 0 {
info.mtu = mtu_val as u16;
}
let mps_key = c"max_packet_size";
let mps_val = unsafe { xpc_dictionary_get_uint64(xpc_params, mps_key.as_ptr()) };
if mps_val > 0 {
info.max_packet_size = mps_val as usize;
}
}
unsafe {
_Block_release(block);
dispatch_release(sema);
}
let mac = config.mac.unwrap_or_else(|| {
if info.mac == [0u8; 6] {
generate_mac()
} else {
info.mac
}
});
let mtu = if config.mtu == DEFAULT_MTU {
info.mtu
} else {
config.mtu
};
let max_packet_size = info.max_packet_size;
let final_info = VmnetInterfaceInfo {
mac,
mtu,
max_packet_size,
};
Ok((interface, mac, mtu, max_packet_size, Some(final_info)))
}
#[cfg(not(feature = "vmnet"))]
fn start_with_null_handler(
config_dict: XpcObjectT,
queue: DispatchQueue,
config: &VmnetConfig,
) -> Result<StartResult> {
let interface = unsafe { vmnet_start_interface(config_dict, queue, ptr::null()) };
unsafe { xpc_release(config_dict) };
if interface.is_null() {
unsafe { dispatch_release(queue.cast()) };
return Err(VmnetError::config(
"failed to start vmnet interface (requires root or entitlements)",
));
}
let mac = config.mac.unwrap_or_else(generate_mac);
Ok((interface, mac, config.mtu, DEFAULT_MAX_PACKET_SIZE, None))
}
#[must_use]
pub fn interface_info(&self) -> Option<&VmnetInterfaceInfo> {
self.interface_info.as_ref()
}
pub fn new_shared() -> Result<Self> {
Self::new(VmnetConfig::shared())
}
pub fn new_host_only() -> Result<Self> {
Self::new(VmnetConfig::host_only())
}
pub fn new_bridged(interface: &str) -> Result<Self> {
Self::new(VmnetConfig::bridged(interface))
}
#[must_use]
pub fn is_running(&self) -> bool {
self.running.load(Ordering::Acquire)
}
#[must_use]
pub fn mac(&self) -> [u8; 6] {
self.mac
}
#[must_use]
pub fn mtu(&self) -> u16 {
self.mtu
}
#[must_use]
pub fn max_packet_size(&self) -> usize {
self.max_packet_size
}
pub fn read_packet(&self, buf: &mut [u8]) -> Result<usize> {
if !self.is_running() {
return Err(VmnetError::config("interface not running"));
}
let mut iov = iovec {
iov_base: buf.as_mut_ptr().cast(),
iov_len: buf.len(),
};
let mut packet = VmnetPacket {
vm_pkt_size: buf.len(),
vm_pkt_iov: &raw mut iov,
vm_pkt_iovcnt: 1,
vm_flags: 0,
};
let mut pktcnt: c_int = 1;
let status = unsafe { vmnet_read(self.interface, &raw mut packet, &raw mut pktcnt) };
if !status.is_success() {
if status == VmnetReturnT::BufferExhausted {
return Ok(0);
}
return Err(VmnetError::Io(std::io::Error::other(status.message())));
}
if pktcnt == 0 {
return Ok(0);
}
Ok(packet.vm_pkt_size)
}
pub fn write_packet(&self, data: &[u8]) -> Result<usize> {
if !self.is_running() {
return Err(VmnetError::config("interface not running"));
}
if data.len() > self.max_packet_size {
return Err(VmnetError::config(format!(
"packet too large: {} > {}",
data.len(),
self.max_packet_size
)));
}
let mut iov = iovec {
iov_base: data.as_ptr().cast_mut().cast::<libc::c_void>(),
iov_len: data.len(),
};
let mut packet = VmnetPacket {
vm_pkt_size: data.len(),
vm_pkt_iov: &raw mut iov,
vm_pkt_iovcnt: 1,
vm_flags: 0,
};
let mut pktcnt: c_int = 1;
let status = unsafe { vmnet_write(self.interface, &raw mut packet, &raw mut pktcnt) };
if !status.is_success() {
return Err(VmnetError::Io(std::io::Error::other(status.message())));
}
if pktcnt == 0 {
return Err(VmnetError::Io(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"no packets written",
)));
}
Ok(data.len())
}
pub(crate) fn set_event_callback(&self, block: *const std::ffi::c_void) -> Result<()> {
let status = unsafe {
vmnet_interface_set_event_callback(
self.interface,
VmnetInterfaceEvent::PacketsAvailable,
self.queue,
block,
)
};
if !status.is_success() {
return Err(VmnetError::config(format!(
"vmnet_interface_set_event_callback failed: {}",
status.message()
)));
}
Ok(())
}
pub(crate) fn clear_event_callback(&self) {
let status = unsafe {
vmnet_interface_set_event_callback(
self.interface,
VmnetInterfaceEvent::PacketsAvailable,
ptr::null_mut(),
ptr::null(),
)
};
if !status.is_success() {
tracing::debug!(
"vmnet_interface_set_event_callback(NULL) returned: {}",
status.message()
);
}
}
pub fn stop(&self) {
if self.running.swap(false, Ordering::AcqRel) {
self.clear_event_callback();
unsafe {
vmnet_stop_interface(self.interface, self.queue, ptr::null());
}
}
}
}
impl Drop for Vmnet {
fn drop(&mut self) {
self.stop();
unsafe {
dispatch_release(self.queue.cast());
}
}
}
#[cfg(any(feature = "vmnet", test))]
fn parse_mac(s: &str) -> Option<[u8; 6]> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 6 {
return None;
}
let mut mac = [0u8; 6];
for (i, part) in parts.iter().enumerate() {
mac[i] = u8::from_str_radix(part, 16).ok()?;
}
Some(mac)
}
fn generate_mac() -> [u8; 6] {
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64;
let mut state = seed;
let mut mac = [0u8; 6];
for byte in &mut mac {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
*byte = (state >> 32) as u8;
}
mac[0] = (mac[0] & 0xFC) | 0x02;
mac
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vmnet_config_default() {
let config = VmnetConfig::default();
assert_eq!(config.mode, VmnetMode::Shared);
assert_eq!(config.mtu, DEFAULT_MTU);
assert!(config.mac.is_none());
assert!(config.bridge_interface.is_none());
assert!(config.dhcp_start.is_none());
assert!(config.dhcp_end.is_none());
assert!(config.subnet_mask.is_none());
assert!(!config.isolated);
}
#[test]
fn test_vmnet_config_builders() {
let config = VmnetConfig::shared()
.with_mac([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
.with_mtu(9000);
assert_eq!(config.mode, VmnetMode::Shared);
assert_eq!(config.mac, Some([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]));
assert_eq!(config.mtu, 9000);
let config = VmnetConfig::host_only().with_isolation(true);
assert_eq!(config.mode, VmnetMode::HostOnly);
assert!(config.isolated);
let config = VmnetConfig::bridged("en0");
assert_eq!(config.mode, VmnetMode::Bridged);
assert_eq!(config.bridge_interface, Some("en0".to_string()));
}
#[test]
fn test_vmnet_config_dhcp_range() {
let config = VmnetConfig::shared()
.with_dhcp_range(
Ipv4Addr::new(192, 168, 64, 2),
Ipv4Addr::new(192, 168, 64, 254),
)
.with_subnet_mask(Ipv4Addr::new(255, 255, 255, 0));
assert_eq!(config.dhcp_start, Some(Ipv4Addr::new(192, 168, 64, 2)));
assert_eq!(config.dhcp_end, Some(Ipv4Addr::new(192, 168, 64, 254)));
assert_eq!(config.subnet_mask, Some(Ipv4Addr::new(255, 255, 255, 0)));
}
#[test]
fn test_vmnet_config_jumbo_frame() {
let config = VmnetConfig::shared().with_mtu(9000);
assert_eq!(config.mtu, 9000);
}
#[test]
fn test_vmnet_mode_conversion() {
assert_eq!(
VmnetOperatingMode::from(VmnetMode::Shared),
VmnetOperatingMode::Shared
);
assert_eq!(
VmnetOperatingMode::from(VmnetMode::HostOnly),
VmnetOperatingMode::Host
);
assert_eq!(
VmnetOperatingMode::from(VmnetMode::Bridged),
VmnetOperatingMode::Bridged
);
}
#[test]
fn test_vmnet_mode_default() {
assert_eq!(VmnetMode::default(), VmnetMode::Shared);
}
#[test]
fn test_vmnet_config_clone() {
let config = VmnetConfig::shared()
.with_mac([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
.with_mtu(1500);
let cloned = config.clone();
assert_eq!(config.mode, cloned.mode);
assert_eq!(config.mac, cloned.mac);
assert_eq!(config.mtu, cloned.mtu);
}
#[test]
fn test_parse_mac() {
assert_eq!(
parse_mac("02:ab:cd:ef:12:34"),
Some([0x02, 0xAB, 0xCD, 0xEF, 0x12, 0x34])
);
assert!(parse_mac("invalid").is_none());
assert!(parse_mac("02:ab:cd:ef:12").is_none());
assert!(parse_mac("02:ab:cd:ef:12:34:56").is_none());
assert!(parse_mac("02:ab:cd:ef:12:gg").is_none());
}
#[test]
#[ignore = "requires macOS vmnet entitlements and root"]
fn test_vmnet_create_shared() {
let vmnet = Vmnet::new_shared();
assert!(
vmnet.is_ok(),
"Failed to create shared vmnet: {:?}",
vmnet.err()
);
let vmnet = vmnet.unwrap();
assert!(vmnet.is_running());
assert!(vmnet.max_packet_size() > 0);
assert_eq!(vmnet.mtu(), DEFAULT_MTU);
}
#[test]
#[ignore = "requires macOS vmnet entitlements and root"]
fn test_vmnet_create_host_only() {
let vmnet = Vmnet::new_host_only();
assert!(
vmnet.is_ok(),
"Failed to create host-only vmnet: {:?}",
vmnet.err()
);
let vmnet = vmnet.unwrap();
assert!(vmnet.is_running());
}
#[test]
#[ignore = "requires macOS vmnet entitlements and root"]
fn test_vmnet_stop() {
let vmnet = Vmnet::new_shared().expect("Failed to create vmnet");
assert!(vmnet.is_running());
vmnet.stop();
assert!(!vmnet.is_running());
}
#[test]
#[ignore = "requires macOS vmnet entitlements and root"]
fn test_vmnet_read_no_data() {
let vmnet = Vmnet::new(VmnetConfig::host_only().with_isolation(true))
.expect("Failed to create vmnet");
let mut buf = vec![0u8; vmnet.max_packet_size()];
for _ in 0..32 {
match vmnet.read_packet(&mut buf) {
Ok(0) => return,
Ok(_) => {}
Err(e) => panic!("read_packet on an idle interface failed: {e}"),
}
}
panic!("queue never drained to empty");
}
#[test]
#[ignore = "requires macOS vmnet entitlements and root"]
fn test_vmnet_write_packet() {
let vmnet = Vmnet::new_shared().expect("Failed to create vmnet");
let mut packet = vec![0u8; 64];
packet[0..6].copy_from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
packet[6..12].copy_from_slice(&vmnet.mac());
packet[12..14].copy_from_slice(&[0x08, 0x00]);
let result = vmnet.write_packet(&packet);
assert!(result.is_ok(), "Failed to write packet: {:?}", result.err());
}
#[test]
#[ignore = "requires macOS vmnet entitlements and root"]
fn test_vmnet_custom_mac() {
let mac = [0x02, 0x00, 0x00, 0x12, 0x34, 0x56];
let config = VmnetConfig::shared().with_mac(mac);
let vmnet = Vmnet::new(config).expect("Failed to create vmnet");
assert_eq!(vmnet.mac(), mac);
}
}