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
10 changes: 10 additions & 0 deletions src/ffi/vmnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ extern "C" {
client: *const [u8; 6],
reservation: *const in_addr,
) -> VmnetReturnT;
// internal_port comes before external_port here, unlike in
// vmnet_interface_add_ip_port_forwarding_rule
pub fn vmnet_network_configuration_add_port_forwarding_rule(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declared exactly as in the macOS 27 SDK's vmnet.h (available since macOS 26): (config, uint8_t protocol, sa_family_t address_family, uint16_t internal_port, uint16_t external_port, void const *internal_address). sa_family_t is __uint8_t on macOS, hence u8.

The port order is internal-then-external, the reverse of vmnet_interface_add_ip_port_forwarding_rule. That's what the comment above is for.

config: NetworkConfigurationRef,
protocol: u8,
address_family: u8,
internal_port: u16,
external_port: u16,
internal_address: *const c_void,
) -> VmnetReturnT;

// Network functions
pub fn vmnet_network_create(
Expand Down
27 changes: 27 additions & 0 deletions src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,33 @@ mod tests {
iface.finalize().unwrap();
}

#[test]
fn network_port_forwarding() {
let addr: IpAddr = Ipv4Addr::new(10, 88, 12, 2).into();

// Configure port forwarding on the network
let mut configuration = NetworkConfiguration::new(NetworkMode::Shared).unwrap();
configuration
.set_ipv4_subnet(
Ipv4Addr::new(10, 88, 12, 1),
Ipv4Addr::new(255, 255, 255, 248),
)
.unwrap();
configuration
.add_port_forwarding_rule(AddressFamily::Ipv4, Protocol::Tcp, 2222, addr, 22)
.unwrap();
configuration
.add_port_forwarding_rule(AddressFamily::Ipv4, Protocol::Tcp, 8080, addr, 80)
.unwrap();

// Create the network and attach an interface. The interface port

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can't read the rules back, because port_forwarding_rules returns Failure on an interface attached to a network. So it checks that configure → create network → attach interface succeeds with rules present.

The forwarding itself was verified end to end through softnet: an rdr rule appears for the port, and traffic reaches the guest.

// forwarding functions fail on it, so the rules can't be listed back
let network = Network::new(&configuration).unwrap();
let mut iface = Interface::with_network(&network, Options::default()).unwrap();

iface.finalize().unwrap();
}

#[test]
fn test_batch() {
// Create two interfaces in the same broadcast domain
Expand Down
71 changes: 69 additions & 2 deletions src/network.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::ffi::vmnet::{self, NetworkRef, Status};
use crate::Result;
use crate::port_forwarding::{AddressFamily, Protocol};
use crate::{Error, Result};
use objc2_core_foundation::{CFRetained, CFType};
use std::net::Ipv4Addr;
use std::net::{IpAddr, Ipv4Addr};
use std::ptr::NonNull;

/// Modes supported by explicit networks.
Expand Down Expand Up @@ -67,6 +68,46 @@ impl NetworkConfiguration {

Ok(())
}

/// Adds a port forwarding rule to the network.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc only covers what a caller needs: add rules before Network::new, and target an address inside the network's subnet.

The macOS 27.0.1 observation that listing and removing through the interface also fail is in the PR description rather than here, because Apple documents those calls as working.

///
/// Rules must be added before the network is created:
/// [`Interface::port_forwarding_rule_add`] can't add rules to an interface
/// attached to a network. `internal_addr` must be inside the network's
/// subnet; use [`set_ipv4_subnet`](Self::set_ipv4_subnet) and
/// [`add_dhcp_reservation`](Self::add_dhcp_reservation) to give a guest a
/// known address.
///
/// [`Interface::port_forwarding_rule_add`]: crate::Interface::port_forwarding_rule_add
pub fn add_port_forwarding_rule(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same arguments, order and AddressFamilyMismatch check as Interface::port_forwarding_rule_add, so the two read the same to callers. The call below maps them onto the C function's internal-before-external order.

&mut self,
address_family: AddressFamily,
protocol: Protocol,
external_port: u16,
internal_addr: IpAddr,
internal_port: u16,
) -> Result<()> {
let internal_addr_ffi = match (address_family, internal_addr) {
(AddressFamily::Ipv4, IpAddr::V4(addr)) => Vec::from(addr.octets()),
(AddressFamily::Ipv6, IpAddr::V6(addr)) => Vec::from(addr.octets()),
_ => return Err(Error::AddressFamilyMismatch),
};

let status = unsafe {
vmnet::vmnet_network_configuration_add_port_forwarding_rule(
CFRetained::as_ptr(&self.handle).as_ptr().cast(),
protocol as u8,
address_family as u8,
internal_port,
external_port,
internal_addr_ffi.as_ptr().cast(),
)
};

Status::from_ffi(status)?;

Ok(())
}
}

/// A reserved network to which multiple interfaces can attach.
Expand Down Expand Up @@ -128,4 +169,30 @@ mod tests {

assert!(Network::new(&second_configuration).is_err());
}

#[test]
fn port_forwarding_rule_address_family_mismatch() {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covers both mismatch directions. The check runs before any FFI call, so this test doesn't create a network.

let mut configuration = NetworkConfiguration::new(Mode::Shared).unwrap();

assert!(matches!(
configuration.add_port_forwarding_rule(
AddressFamily::Ipv6,
Protocol::Tcp,
2222,
Ipv4Addr::new(192, 168, 0, 2).into(),
22,
),
Err(Error::AddressFamilyMismatch)
));
assert!(matches!(
configuration.add_port_forwarding_rule(
AddressFamily::Ipv4,
Protocol::Tcp,
2222,
"fd00::2".parse::<std::net::Ipv6Addr>().unwrap().into(),
22,
),
Err(Error::AddressFamilyMismatch)
));
}
}
Loading