diff --git a/src/ffi/vmnet.rs b/src/ffi/vmnet.rs index 693ef86..39f7578 100644 --- a/src/ffi/vmnet.rs +++ b/src/ffi/vmnet.rs @@ -2,7 +2,7 @@ use crate::error; use crate::ffi::xpc::XpcObjectT; use bitflags::bitflags; use dispatch2::DispatchQueue; -use libc::{in_addr, iovec, size_t}; +use libc::{in6_addr, in_addr, iovec, size_t}; use num_enum::{IntoPrimitive, TryFromPrimitive}; use std::ffi::c_void; use std::os::raw::{c_char, c_int}; @@ -33,11 +33,31 @@ extern "C" { mode: u32, status: *mut VmnetReturnT, ) -> NetworkConfigurationRef; + pub fn vmnet_network_configuration_set_external_interface( + config: NetworkConfigurationRef, + interface_name: *const c_char, + ) -> VmnetReturnT; + pub fn vmnet_network_configuration_disable_nat44(config: NetworkConfigurationRef); + pub fn vmnet_network_configuration_disable_nat66(config: NetworkConfigurationRef); + pub fn vmnet_network_configuration_disable_dhcp(config: NetworkConfigurationRef); + pub fn vmnet_network_configuration_disable_dns_proxy(config: NetworkConfigurationRef); + pub fn vmnet_network_configuration_disable_router_advertisement( + config: NetworkConfigurationRef, + ); pub fn vmnet_network_configuration_set_ipv4_subnet( config: NetworkConfigurationRef, subnet_addr: *const in_addr, subnet_mask: *const in_addr, ) -> VmnetReturnT; + pub fn vmnet_network_configuration_set_ipv6_prefix( + config: NetworkConfigurationRef, + prefix: *const in6_addr, + len: u8, + ) -> VmnetReturnT; + pub fn vmnet_network_configuration_set_mtu( + config: NetworkConfigurationRef, + mtu: u32, + ) -> VmnetReturnT; // Darwin's ether_addr_t contains only unsigned char octet[6], with alignment // one and no padding, so a pointer to [u8; 6] has the same pointee layout. pub fn vmnet_network_configuration_add_dhcp_reservation( @@ -61,6 +81,16 @@ extern "C" { configuration: NetworkConfigurationRef, status: *mut VmnetReturnT, ) -> NetworkRef; + pub fn vmnet_network_get_ipv4_subnet( + network: NetworkRef, + subnet: *mut in_addr, + mask: *mut in_addr, + ); + pub fn vmnet_network_get_ipv6_prefix( + network: NetworkRef, + prefix: *mut in6_addr, + prefix_len: *mut u8, + ); // Interface functions pub fn vmnet_start_interface( diff --git a/src/network.rs b/src/network.rs index 5d20ddb..febd82d 100644 --- a/src/network.rs +++ b/src/network.rs @@ -2,7 +2,8 @@ use crate::ffi::vmnet::{self, NetworkRef, Status}; use crate::port_forwarding::{AddressFamily, Protocol}; use crate::{Error, Result}; use objc2_core_foundation::{CFRetained, CFType}; -use std::net::{IpAddr, Ipv4Addr}; +use std::ffi::CStr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::ptr::NonNull; /// Modes supported by explicit networks. @@ -32,6 +33,65 @@ impl NetworkConfiguration { Ok(Self { handle }) } + /// Sets the external interface used by a shared network. + pub fn set_external_interface(&mut self, interface_name: &CStr) -> Result<()> { + let status = unsafe { + vmnet::vmnet_network_configuration_set_external_interface( + CFRetained::as_ptr(&self.handle).as_ptr().cast(), + interface_name.as_ptr(), + ) + }; + + Status::from_ffi(status)?; + + Ok(()) + } + + /// Disables IPv4 NAT on the network. + pub fn disable_nat44(&mut self) { + unsafe { + vmnet::vmnet_network_configuration_disable_nat44( + CFRetained::as_ptr(&self.handle).as_ptr().cast(), + ) + } + } + + /// Disables IPv6 NAT on the network. + pub fn disable_nat66(&mut self) { + unsafe { + vmnet::vmnet_network_configuration_disable_nat66( + CFRetained::as_ptr(&self.handle).as_ptr().cast(), + ) + } + } + + /// Disables the DHCP server on the network. + pub fn disable_dhcp(&mut self) { + unsafe { + vmnet::vmnet_network_configuration_disable_dhcp( + CFRetained::as_ptr(&self.handle).as_ptr().cast(), + ) + } + } + + /// Disables the DNS proxy on the network. + pub fn disable_dns_proxy(&mut self) { + unsafe { + vmnet::vmnet_network_configuration_disable_dns_proxy( + CFRetained::as_ptr(&self.handle).as_ptr().cast(), + ) + } + } + + /// Disables IPv6 router advertisements on the network. + pub fn disable_router_advertisement(&mut self) { + unsafe { + vmnet::vmnet_network_configuration_disable_router_advertisement( + CFRetained::as_ptr(&self.handle).as_ptr().cast(), + ) + } + } + /// Sets the IPv4 address and subnet mask used by the network. /// /// vmnet checks for overlaps and prevents colliding CIDRs across networks, @@ -54,6 +114,39 @@ impl NetworkConfiguration { Ok(()) } + /// Sets the IPv6 prefix and prefix length used by the network. + pub fn set_ipv6_prefix(&mut self, prefix: Ipv6Addr, prefix_length: u8) -> Result<()> { + let prefix = libc::in6_addr { + s6_addr: prefix.octets(), + }; + + let status = unsafe { + vmnet::vmnet_network_configuration_set_ipv6_prefix( + CFRetained::as_ptr(&self.handle).as_ptr().cast(), + &prefix, + prefix_length, + ) + }; + + Status::from_ffi(status)?; + + Ok(()) + } + + /// Sets the maximum transmission unit (MTU) used by the network. + pub fn set_mtu(&mut self, mtu: u32) -> Result<()> { + let status = unsafe { + vmnet::vmnet_network_configuration_set_mtu( + CFRetained::as_ptr(&self.handle).as_ptr().cast(), + mtu, + ) + }; + + Status::from_ffi(status)?; + + Ok(()) + } + /// Adds an IPv4 DHCP reservation for a guest's MAC address. pub fn add_dhcp_reservation(&mut self, mac_address: [u8; 6], address: Ipv4Addr) -> Result<()> { let status = unsafe { @@ -134,6 +227,31 @@ impl Network { Ok(Self { handle }) } + /// Returns the reserved IPv4 address and subnet mask of the network. + pub fn ipv4_subnet(&self) -> (Ipv4Addr, Ipv4Addr) { + let mut address = libc::in_addr { s_addr: 0 }; + let mut mask = libc::in_addr { s_addr: 0 }; + + unsafe { vmnet::vmnet_network_get_ipv4_subnet(self.as_raw(), &mut address, &mut mask) }; + + ( + Ipv4Addr::from(address.s_addr.to_ne_bytes()), + Ipv4Addr::from(mask.s_addr.to_ne_bytes()), + ) + } + + /// Returns the reserved IPv6 prefix and prefix length of the network. + pub fn ipv6_prefix(&self) -> (Ipv6Addr, u8) { + let mut prefix = libc::in6_addr { s6_addr: [0; 16] }; + let mut prefix_length = 0; + + unsafe { + vmnet::vmnet_network_get_ipv6_prefix(self.as_raw(), &mut prefix, &mut prefix_length) + }; + + (Ipv6Addr::from(prefix.s6_addr), prefix_length) + } + pub(crate) fn as_raw(&self) -> NetworkRef { CFRetained::as_ptr(&self.handle).as_ptr().cast() } @@ -151,6 +269,102 @@ fn ipv4_to_native(address: Ipv4Addr) -> libc::in_addr { mod tests { use super::*; + #[test] + fn set_external_interface() { + // Select an explicit upstream interface instead of vmnet's routing-table default + let mut configuration = NetworkConfiguration::new(Mode::Shared).unwrap(); + configuration.set_external_interface(c"en0").unwrap(); + + // Verify that vmnet accepts the interface selection when starting the network + let network = Network::new(&configuration).unwrap(); + crate::Interface::with_network(&network, crate::Options::default()).unwrap(); + } + + #[test] + fn disable_services() { + // Shared mode enables each of these services by default + let cases = [ + ( + "disable_nat44", + NetworkConfiguration::disable_nat44 as fn(&mut NetworkConfiguration), + ), + ("disable_nat66", NetworkConfiguration::disable_nat66), + ("disable_dhcp", NetworkConfiguration::disable_dhcp), + ("disable_dns_proxy", NetworkConfiguration::disable_dns_proxy), + ( + "disable_router_advertisement", + NetworkConfiguration::disable_router_advertisement, + ), + ]; + + for (name, disable) in cases { + // Use a fresh configuration so each case disables only one service + let mut configuration = NetworkConfiguration::new(Mode::Shared) + .unwrap_or_else(|error| panic!("{name}: configuration creation failed: {error}")); + disable(&mut configuration); + + // Verify that vmnet accepts the setting when starting an interface + let network = Network::new(&configuration) + .unwrap_or_else(|error| panic!("{name}: network creation failed: {error}")); + crate::Interface::with_network(&network, crate::Options::default()) + .unwrap_or_else(|error| panic!("{name}: interface startup failed: {error}")); + } + } + + #[test] + fn set_ipv6_prefix() { + // Use a prefix from the documentation range reserved by RFC 3849 + let prefix = "2001:db8::".parse::().unwrap(); + let mut configuration = NetworkConfiguration::new(Mode::Host).unwrap(); + configuration.set_ipv6_prefix(prefix, 64).unwrap(); + + // Verify that the reserved network preserves both the address bytes and prefix length + let network = Network::new(&configuration).unwrap(); + assert_eq!(network.ipv6_prefix(), (prefix, 64)); + } + + #[test] + fn set_mtu() { + // Choose an MTU that differs from vmnet's default of 1500 + let mut configuration = NetworkConfiguration::new(Mode::Host).unwrap(); + configuration.set_mtu(1400).unwrap(); + + // Verify that an attached interface inherits the network's configured MTU + let network = Network::new(&configuration).unwrap(); + let interface = + crate::Interface::with_network(&network, crate::Options::default()).unwrap(); + assert!(matches!( + interface + .parameters() + .get(crate::parameters::ParameterKind::MTU), + Some(crate::parameters::Parameter::MTU(1400)) + )); + } + + #[test] + fn ipv4_subnet() { + // Leave the subnet unset so vmnet reserves one using its defaults + let configuration = NetworkConfiguration::new(Mode::Host).unwrap(); + let network = Network::new(&configuration).unwrap(); + let (address, mask) = network.ipv4_subnet(); + + // The allocated subnet must be private and use vmnet's default /24 mask + assert!(address.is_private()); + assert_eq!(mask, Ipv4Addr::new(255, 255, 255, 0)); + } + + #[test] + fn ipv6_prefix() { + // Leave the prefix unset so vmnet reserves a random ULA prefix + let configuration = NetworkConfiguration::new(Mode::Host).unwrap(); + let network = Network::new(&configuration).unwrap(); + let (prefix, prefix_length) = network.ipv6_prefix(); + + // Check the prefix type and length without depending on the randomly chosen address + assert!(prefix.is_unique_local()); + assert!((1..=128).contains(&prefix_length)); + } + #[test] fn overlapping_ipv4_subnets_are_rejected() { let mut first_configuration = NetworkConfiguration::new(Mode::Host).unwrap();