Skip to content

Put --expose port forwards on the vmnet network configuration - #223

Merged
edi-oai merged 1 commit into
openai:mainfrom
simonisjp:network-port-forwarding
Oct 29, 2026
Merged

edi-oai merged 1 commit into
openai:mainfrom
simonisjp:network-port-forwarding

Conversation

@simonisjp

@simonisjp simonisjp commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

Fixes #222: since 0.24.0, any --expose makes softnet exit at startup with vmnet returned an error status: Failure, which breaks tart run --net-softnet --net-softnet-expose. softnet now adds every VM's forwards to the shared network's configuration in Network::new, before the network is created. It uses NetworkConfiguration::add_port_forwarding_rule from vmnet 0.10.0 (edigaryev/vmnet#12).

Details

What broke. #209 moved VMs onto networks created with Network::new + Interface::with_network, and #221 kept that with one shared network for up to five VMs. Proxy::new still added each --expose forward after the interface was up, with Host::port_forwarding_add_rule. That calls the interface-level vmnet_interface_add_ip_port_forwarding_rule, which vmnet rejects on an interface attached to a network, so initialization failed.

The change.

  • Network::new takes port_forwardings: &[(Ipv4Addr, ExposedPort)] and adds them to the configuration after the DHCP reservations, before vmnet::Network::new. That's the only point where vmnet accepts them for these networks.
  • Manager::initialize collects every VM's exposed ports with its reserved address, next to the reservations, and passes them to Network::new.
  • Proxy::new no longer takes exposed_ports, and Host::port_forwarding_add_rule is removed, since nothing else uses it.
  • Cargo.toml bumps vmnet from 0.9.0 to 0.10.0. No other changes were needed.
  • Test: manager::tests::native_batch_exposes_ports_for_every_vm initializes a batch of two VMs with three forwards. On main it fails with the error from --expose fails on 0.24: vmnet rejects port forwarding on per-VM networks #222; with this PR it passes.

Behaviour notes.

  • Error: still failed to add port forwarding rule external_port=…, internal_addr=…, internal_port=…, with the vmnet error as its cause, and initialization still fails as a whole. The outer failed to configure port forwarding context is gone.
  • Log: added port forwarding rule … is logged once the network is created.
  • Forwards belong to the network, not to a VM's interface. vmnet also rejects listing and removing rules through the interface on these networks. With one VM per network (the tart path), stopping the VM removes its forwards. I haven't checked when they go away for one VM of a multi-VM batch.
  • Duplicate external ports aren't checked. vmnet accepts the same external port twice in one configuration; I didn't test which VM then receives the traffic.

Verification

Tested on an Apple silicon Mac mini running macOS 27.0.1, with tart 2.41.1 and the public image ghcr.io/cirruslabs/macos-sonoma-vanilla:latest. Builds were made from main (f834910) with and without this PR. I probed the forwarded ports from a different network.

Scenario main This PR
New test: a batch of two VMs with three forwards fails: vmnet returned an error status: Failure passes
Two VMs through tart at once, --expose 2222:22 and 2223:22 (fails as above) both reachable in about 10 s; guest sshd answers (SSH-2.0-OpenSSH_9.9); guests on 10.x and 192.168.x subnets
Stop one of the two VMs (fails as above) its rdr rule is removed and its port closes; the other keeps working
Stop both (fails as above) no rdr rules left

Before #221 landed, the same approach also passed five start/stop cycles with guests on 10.x, 172.16.x and 192.168.x subnets, plus a check that guests can still reach the internet.

  • Repo checks: cargo fmt --check and cargo clippy --all-targets --all-features -- -D warnings are clean. cargo test passes 85 tests. Two control_transport tests (excessive_attachments_close_every_descriptor_without_kernel_truncation, validates_owned_descriptor_and_signals_eof_on_drop) fail on this host on unmodified main too.
  • Not covered: a multi-VM batch through --control-fd end to end (only the new test covers it), UDP and IPv6 forwards (which --expose doesn't offer), and hosts with the stock pf configuration. The test host uses a custom pf ruleset.

Questions are welcome here.

🤖 Generated with Claude Code

Comment thread Cargo.toml Outdated
futures-util = { version = "0.3", default-features = false, features = ["std", "sink"] }
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
vmnet = "0.7.0"
vmnet = "0.9.0"

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.

vmnet 0.7 → 0.9. No other softnet changes were needed for the API changes in 0.8 and 0.9 (serial event queue, Send callbacks, finalize behaviour, Send packet-size iterator).

Comment thread Cargo.toml Outdated
debug = true

[patch.crates-io]
vmnet = { git = "https://github.com/simonisjp/vmnet", branch = "network-configuration-port-forwarding" }

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.

Temporary, so this branch builds: it points at the branch of the vmnet PR that adds NetworkConfiguration::add_port_forwarding_rule.

Before merge it should be replaced by the vmnet release that contains it, with a regenerated Cargo.lock.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Version 0.10.0 with your NetworkConfiguration::add_port_forwarding_rule addition should be available now.

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.

Thanks! Switched to vmnet 0.10.0 from crates.io and dropped the [patch.crates-io] entry. I also rebased onto #221 and moved the forwards into Network::new.

Comment thread lib/host.rs Outdated
vm_net_type: NetType,
vm_mac: EthernetAddress,
enable_isolation: bool,
exposed_ports: &[ExposedPort],

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.

Host::new now receives the exposed ports. They have to go on the configuration before the network exists, and this is where the configuration is built.

Comment thread lib/host.rs Outdated
.add_dhcp_reservation(vm_mac.0, vm_ip)
.context("failed to add DHCP address reservation")?;

// Forward the exposed ports to the VM's reserved address

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 forwards are added to the NetworkConfiguration after the DHCP reservation, which fixes vm_ip, and before Network::new. On these networks that's the only point where vmnet accepts them.

The details strings are built once, so the error and the log keep the same text as before.

Comment thread lib/host.rs Outdated
)
.context("failed to initialize vmnet interface")?;

for details in &port_forwardings {

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.

Logged once the interface is up, so the line only appears when the forward is actually in place.

Comment thread lib/host.rs
}

impl Host {
pub fn port_forwarding_add_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.

Removed. This was the interface-level call (vmnet_interface_add_ip_port_forwarding_rule) that vmnet rejects on an interface attached to a network, and nothing else used it.

Comment thread lib/proxy/mod.rs Outdated
vm_net_type,
vm_mac_address,
!allow.contains(&Rule::Stateless(Target::Prefix(Ipv4Net::default()))),
&exposed_ports,

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.

Proxy::new now hands the ports to Host::new instead of adding them after the host is created (the loop removed below).

Comment thread lib/proxy/mod.rs Outdated
host.gateway_ip,
);

// Install port forwardings for the reserved VM's IP address

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 failing path: this loop ran after the interface was already attached to its network, so every --expose failed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@simonisjp
simonisjp force-pushed the network-port-forwarding branch from f97352b to 296545f Compare October 28, 2026 08:33

@simonisjp simonisjp left a comment

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.

Updated for #221 and vmnet 0.10.0. The earlier comments refer to the previous version of this branch; these replace them.

Comment thread Cargo.toml
futures-util = { version = "0.3", default-features = false, features = ["std"] }
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
vmnet = "0.9.0"
vmnet = "0.10.0"

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.

vmnet 0.9 → 0.10, which adds NetworkConfiguration::add_port_forwarding_rule. No other changes were needed for 0.10.

Comment thread lib/network.rs
net_type: NetType,
subnet: Ipv4Net,
reservations: &[(MacAddress, Ipv4Addr)],
port_forwardings: &[(Ipv4Addr, ExposedPort)],

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.

One entry per forward, each paired with its VM's reserved address and flattened across the batch, so Network::new doesn't need to know about VMs.

Comment thread lib/network.rs
})?;
}

// Forward exposed ports to the reserved VM addresses, which vmnet only

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 has to happen before vmnet::Network::new. Once an interface is attached to the network, the interface-level vmnet_interface_add_ip_port_forwarding_rule returns Failure, which is the bug in #222.

Comment thread lib/network.rs
let handle = vmnet::Network::new(&configuration)
.with_context(|| format!("failed to create vmnet network {subnet}"))?;

for details in port_forwardings {

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.

Logged only after the network exists, so the line only appears when the forward is actually in place.

Comment thread lib/manager.rs
.map(|(_, config, vm_ip, _)| (config.vm_mac_address, *vm_ip))
.collect();
let network = Network::new(vm_net_type, subnet, &reservations)?;
let port_forwardings: Vec<_> = configured

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.

Built from the same configured list as reservations just above, so each forward points at the address its VM is reserved.

Comment thread lib/manager_test.rs

#[tokio::test]
#[serial_test::serial]
async fn native_batch_exposes_ports_for_every_vm() {

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.

Regression test for #222. On main it fails with failed to add port forwarding rule external_port=52222, …: vmnet returned an error status: Failure; with this PR it passes.

Comment thread lib/proxy/mod.rs
host.gateway_ip,
);

// Install port forwardings for the VM's reserved IP address

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 failing path: this loop ran after Host::new had attached the interface to the network, where vmnet rejects interface-level forwarding.

Comment thread lib/host.rs
}

impl Host {
pub fn port_forwarding_add_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.

Removed. This was the interface-level call (vmnet_interface_add_ip_port_forwarding_rule), and nothing else uses it.

@simonisjp
simonisjp marked this pull request as ready for review October 28, 2026 08:35
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T08:40:00.132598Z 296545f Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 296545fe63

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread lib/manager.rs
Comment on lines +299 to +302
.iter()
.flat_map(|(_, config, vm_ip, _)| {
config.exposed_ports.iter().map(|port| (*vm_ip, *port))
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject duplicate external ports across the VM batch

When two entries in one softnet.initialize batch request the same external TCP port, this flat_map passes both mappings to vmnet. vmnet accepts duplicate rules in one network configuration, so initialization reports success even though a single external port cannot select two target addresses; at least one VM's exposure is ineffective and traffic may reach the other VM instead. Validate uniqueness of external_port across the complete batch before creating native resources.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There was no explicit duplicate-port check before.

Let's just keep relying on vmnet framework's behavior here where a first matching rule wins.

@edi-oai
edi-oai merged commit e95bd11 into openai:main Oct 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

--expose fails on 0.24: vmnet rejects port forwarding on per-VM networks

2 participants