Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/ffi/vmnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
216 changes: 215 additions & 1 deletion src/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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()
}
Expand All @@ -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::<Ipv6Addr>().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();
Expand Down
Loading