use std::ffi::CString;
use std::net::Ipv4Addr;
use std::os::raw::c_int;
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
use super::vmnet_ffi::*;
use crate::backend::NetworkBackend;
use crate::error::{NetError, Result};
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,
}
}
}
pub struct Vmnet {
interface: VmnetInterfaceRef,
queue: DispatchQueue,
mac: [u8; 6],
mtu: u16,
max_packet_size: usize,
running: AtomicBool,
}
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(NetError::config(
"failed to create dispatch queue".to_string(),
));
}
let config_dict = unsafe {
let dict = create_vmnet_config_dict();
if dict.is_null() {
dispatch_release(queue.cast());
return Err(NetError::config(
"failed to create config dictionary".to_string(),
));
}
let mode_value: i64 = VmnetOperatingMode::from(config.mode) as i64;
let mode_num = cfnumber_from_i64(mode_value);
CFDictionarySetValue(dict, vmnet_operation_mode_key.cast(), mode_num.cast());
CFRelease(mode_num.cast());
if let Some(mac) = config.mac {
let mac_str = format!(
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
);
let mac_cf = cfstring_from_str(&mac_str);
CFDictionarySetValue(dict, vmnet_mac_address_key.cast(), mac_cf.cast());
CFRelease(mac_cf.cast());
}
let mtu_value: i64 = i64::from(config.mtu);
let mtu_num = cfnumber_from_i64(mtu_value);
CFDictionarySetValue(dict, vmnet_mtu_key.cast(), mtu_num.cast());
CFRelease(mtu_num.cast());
match config.mode {
VmnetMode::Shared => {
if let Some(start) = config.dhcp_start {
let start_cf = cfstring_from_str(&start.to_string());
CFDictionarySetValue(dict, vmnet_start_address_key.cast(), start_cf.cast());
CFRelease(start_cf.cast());
}
if let Some(end) = config.dhcp_end {
let end_cf = cfstring_from_str(&end.to_string());
CFDictionarySetValue(dict, vmnet_end_address_key.cast(), end_cf.cast());
CFRelease(end_cf.cast());
}
if let Some(mask) = config.subnet_mask {
let mask_cf = cfstring_from_str(&mask.to_string());
CFDictionarySetValue(dict, vmnet_subnet_mask_key.cast(), mask_cf.cast());
CFRelease(mask_cf.cast());
}
}
VmnetMode::HostOnly => {
if config.isolated {
CFDictionarySetValue(
dict,
vmnet_enable_isolation_key.cast(),
kCFBooleanTrue,
);
let uuid = CFUUIDCreate(kCFAllocatorDefault);
let uuid_str = CFUUIDCreateString(kCFAllocatorDefault, uuid);
CFDictionarySetValue(dict, vmnet_interface_id_key.cast(), uuid_str.cast());
CFRelease(uuid_str.cast());
CFRelease(uuid.cast());
}
}
VmnetMode::Bridged => {
if let Some(ref iface) = config.bridge_interface {
let iface_cf = cfstring_from_str(iface);
CFDictionarySetValue(
dict,
vmnet_shared_interface_name_key.cast(),
iface_cf.cast(),
);
CFRelease(iface_cf.cast());
} else {
CFRelease(dict.cast_const());
dispatch_release(queue.cast());
return Err(NetError::config(
"bridge mode requires interface name".to_string(),
));
}
}
}
dict
};
let interface =
unsafe { vmnet_start_interface(config_dict.cast_const(), queue, ptr::null()) };
unsafe {
CFRelease(config_dict.cast_const());
}
if interface.is_null() {
unsafe {
dispatch_release(queue.cast());
}
return Err(NetError::config(
"failed to start vmnet interface (requires root or entitlements)".to_string(),
));
}
let max_packet_size = DEFAULT_MAX_PACKET_SIZE;
let mac = config.mac.unwrap_or_else(super::generate_mac);
Ok(Self {
interface,
queue,
mac,
mtu: config.mtu,
max_packet_size,
running: AtomicBool::new(true),
})
}
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 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(NetError::config("interface not running".to_string()));
}
let mut iov = iovec {
iov_base: buf.as_mut_ptr().cast(),
iov_len: buf.len(),
};
let mut packet = VmnetPacket {
vm_pkt_iov: &raw mut iov,
vm_pkt_iovcnt: 1,
vm_pkt_size: 0,
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(NetError::io(std::io::Error::other(status.message())));
}
if pktcnt == 0 {
return Ok(0);
}
Ok(packet.vm_pkt_size as usize)
}
pub fn write_packet(&self, data: &[u8]) -> Result<usize> {
if !self.is_running() {
return Err(NetError::config("interface not running".to_string()));
}
if data.len() > self.max_packet_size {
return Err(NetError::config(format!(
"packet too large: {} > {}",
data.len(),
self.max_packet_size
)));
}
let mut iov = iovec {
iov_base: data.as_ptr() as *mut _,
iov_len: data.len(),
};
let mut packet = VmnetPacket {
vm_pkt_iov: &raw mut iov,
vm_pkt_iovcnt: 1,
vm_pkt_size: data.len() as u32,
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(NetError::io(std::io::Error::other(status.message())));
}
if pktcnt == 0 {
return Err(NetError::io(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"no packets written",
)));
}
Ok(data.len())
}
pub fn stop(&self) {
if self.running.swap(false, Ordering::AcqRel) {
unsafe {
vmnet_stop_interface(self.interface, self.queue, ptr::null());
}
}
}
}
impl NetworkBackend for Vmnet {
fn send(&self, data: &[u8]) -> Result<usize> {
self.write_packet(data)
}
fn recv(&self, buf: &mut [u8]) -> Result<usize> {
self.read_packet(buf)
}
fn mac(&self) -> [u8; 6] {
self.mac
}
fn mtu(&self) -> u16 {
self.mtu
}
}
impl Drop for Vmnet {
fn drop(&mut self) {
self.stop();
unsafe {
dispatch_release(self.queue.cast());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[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]
#[ignore]
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]
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]
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]
fn test_vmnet_read_no_data() {
let vmnet = Vmnet::new_shared().expect("Failed to create vmnet");
let mut buf = [0u8; 1500];
let result = vmnet.read_packet(&mut buf);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[test]
#[ignore]
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]
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);
}
}