consomme: Enable port forwarding from openvmm command line#3364
consomme: Enable port forwarding from openvmm command line#3364Brian-Perkins wants to merge 1 commit intomicrosoft:mainfrom
Conversation
| ip_addr: Option<IpAddr>, | ||
| guest_port: u16, | ||
| host_port: u16, | ||
| ) -> Result<u16, DropReason> { |
There was a problem hiding this comment.
Kind of weird to use DropReason here.
There was a problem hiding this comment.
Pull request overview
This PR extends the Consomme networking backend to support host→guest port forwarding configured from the openvmm command line (--net consomme:hostfwd=...). It wires the new CLI-parsed forwarding rules through resource handles into the Consomme endpoint so ports can be bound when the NIC queue starts.
Changes:
- Add
hostfwd=parsing for--net consomme:...and plumb forwarding rules intonet_backend_resources::consomme::ConsommeHandle. - Extend Consomme endpoint initialization to carry a list of port-forward bindings and attempt to bind them on queue startup.
- Update Consomme TCP/UDP binding APIs to accept distinct host/guest ports and introduce a new
DropReason::PortAlreadyBound.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| vm/devices/net/net_consomme/src/resolver.rs | Parses forwarded host bind address and converts resource port configs into endpoint port-forward configs. |
| vm/devices/net/net_consomme/src/lib.rs | Adds PortForwardConfig, endpoint constructor supporting port forwards, and binds configured forwards on queue startup. |
| vm/devices/net/net_consomme/consomme/src/udp.rs | Extends UDP forwarding/binding API to accept guest/host port (and attempts to support forwarded UDP ports). |
| vm/devices/net/net_consomme/consomme/src/tcp.rs | Extends TCP bind API for host/guest port mapping and returns actual bound host port. |
| vm/devices/net/net_consomme/consomme/src/lib.rs | Adds DropReason::PortAlreadyBound for duplicate port bind attempts. |
| vm/devices/net/net_backend_resources/src/lib.rs | Adds HostPortConfig/HostPortProtocol and a ports field to ConsommeHandle. |
| petri/src/vm/openvmm/modify.rs | Updates ConsommeHandle construction to include the new ports field. |
| openvmm/openvmm_entry/src/lib.rs | Plumbs CLI host_fwd rules into ConsommeHandle.ports when building NIC config. |
| openvmm/openvmm_entry/src/cli_args.rs | Adds CLI syntax/docs + parsing + tests for consomme:hostfwd=.... |
| let bind_addr: SocketAddr = match guest_addr { | ||
| SocketAddr::V4(_) => { | ||
| SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)) | ||
| } | ||
| SocketAddr::V6(_) => { | ||
| SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0)) | ||
| } | ||
| SocketAddr::V4(_) => SocketAddr::V4(SocketAddrV4::new( | ||
| Ipv4Addr::UNSPECIFIED, | ||
| host_port.unwrap_or(0), | ||
| )), | ||
| SocketAddr::V6(_) => SocketAddr::V6(SocketAddrV6::new( | ||
| Ipv6Addr::UNSPECIFIED, | ||
| host_port.unwrap_or(0), | ||
| 0, | ||
| 0, | ||
| )), | ||
| }; |
There was a problem hiding this comment.
get_or_insert() binds new UDP sockets to 0.0.0.0/:: unconditionally. For host port forwarding this ignores the requested host_address (e.g. 127.0.0.1 / ::1) and can inadvertently expose the forwarded UDP port on all interfaces. Binding should respect the configured host IP when creating the socket.
| let host_port = conn | ||
| .socket | ||
| .as_ref() | ||
| .map_or(0, |s| s.get().local_addr().map_or(0, |addr| addr.port())); |
There was a problem hiding this comment.
bind_udp_port() suppresses errors from local_addr() by returning 0 on failure. If the caller relies on the returned port (especially when binding to 0 for an ephemeral port), this can silently report the wrong value. Consider propagating the local_addr() error as DropReason::Io instead of defaulting to 0.
| let host_port = conn | |
| .socket | |
| .as_ref() | |
| .map_or(0, |s| s.get().local_addr().map_or(0, |addr| addr.port())); | |
| let host_port = match conn.socket.as_ref() { | |
| Some(socket) => socket | |
| .get() | |
| .local_addr() | |
| .map_err(DropReason::Io)? | |
| .port(), | |
| None => 0, | |
| }; |
| .map(|a| { | ||
| a.parse() | ||
| .map_err(|_| ResolveConsommeError::InvalidAddress(a.to_owned())) | ||
| }) | ||
| .transpose()?; |
There was a problem hiding this comment.
InvalidAddress is constructed by discarding the underlying parse error (map_err(|_| ...)). This loses useful detail (e.g. why parsing failed). Consider preserving the parse error in the message or as a #[source] so users can diagnose invalid hostfwd addresses more easily.
| /// --net consomme:hostfwd=tcp::3389-:3389 | ||
| /// --net consomme:hostfwd=tcp:127.0.0.1:8080-:80 | ||
| /// --net consomme:hostfwd=tcp:[::1]:8080-:80 | ||
| /// --net consomme:10.0.0.0/24,hostfwd=tcp::22-:22,hostfwd=udp::5000-:5000 |
There was a problem hiding this comment.
This PR adds a new CLI surface (--net consomme:hostfwd=...). The guide already documents Consomme (Guide/src/reference/backends/consomme.md) and mentions that inbound connections require port forwarding, but it doesn’t describe this CLI syntax. Please update the Guide to include the new hostfwd= option (or note a follow-up) to keep user-facing documentation in sync.
| /// --net consomme:10.0.0.0/24,hostfwd=tcp::22-:22,hostfwd=udp::5000-:5000 | |
| /// --net consomme:10.0.0.0/24,hostfwd=tcp::22-:22,hostfwd=udp::5000-:5000 | |
| /// | |
| /// TODO(openvmm-guide): document `--net consomme:hostfwd=...` in | |
| /// `Guide/src/reference/backends/consomme.md` to keep the published | |
| /// Consomme backend documentation in sync with this CLI surface. |
| pub address: Option<IpAddr>, | ||
| /// The port traffic is forwarded to on the guest. | ||
| pub guest_port: u16, | ||
| /// The host port to bind, or `None` for any available port. |
There was a problem hiding this comment.
PortForwardConfig::host_port is a u16, but the doc comment says it can be None for an ephemeral port. Either make the field an Option<u16> or document the actual sentinel value (e.g. 0 means “any available port”).
| /// The host port to bind, or `None` for any available port. | |
| /// The host port to bind, or `0` for any available port. |
| let port_forwards = | ||
| std::mem::take(&mut queue.endpoint_state.as_mut().unwrap().port_forwards); | ||
| queue.with_consomme_no_pool(|c| { | ||
| c.refresh_driver(); | ||
| for fwd in port_forwards { | ||
| let result = match fwd.protocol { | ||
| IpProtocol::Tcp => c.bind_tcp_port(fwd.address, fwd.guest_port, fwd.host_port), | ||
| IpProtocol::Udp => c.bind_udp_port(fwd.address, fwd.guest_port, fwd.host_port), | ||
| }; | ||
| if let Err(err) = result { | ||
| tracing::error!( | ||
| protocol = ?fwd.protocol, | ||
| guest_port = fwd.guest_port, | ||
| host_port = fwd.host_port, | ||
| error = &err as &dyn std::error::Error, | ||
| "failed to bind port" | ||
| ); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
Port-forward binding failures are only logged and VM startup continues. For a CLI-configured hostfwd=..., this makes misconfiguration (e.g. port already in use) easy to miss and leaves the guest without the requested forwarding. Consider propagating these errors (e.g. return an error from get_queues() / endpoint creation) so the command fails fast when forwarding can’t be established.
| /// Binds to the specified host IP and port for forwarding inbound UDP | ||
| /// packets to the guest. | ||
| pub fn bind_udp_port(&mut self, ip_addr: Option<IpAddr>, port: u16) -> Result<(), DropReason> { | ||
| pub fn bind_udp_port( | ||
| &mut self, | ||
| ip_addr: Option<IpAddr>, | ||
| guest_port: u16, | ||
| host_port: u16, | ||
| ) -> Result<u16, DropReason> { | ||
| let guest_addr = match ip_addr { | ||
| Some(IpAddr::V4(ip)) => SocketAddr::V4(SocketAddrV4::new(ip, port)), | ||
| Some(IpAddr::V6(ip)) => SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0)), | ||
| None => SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)), | ||
| Some(IpAddr::V4(ip)) => SocketAddr::V4(SocketAddrV4::new(ip, guest_port)), | ||
| Some(IpAddr::V6(ip)) => SocketAddr::V6(SocketAddrV6::new(ip, guest_port, 0, 0)), | ||
| None => SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, guest_port)), | ||
| }; | ||
| let _ = self.get_or_insert(guest_addr, None)?; | ||
| Ok(()) | ||
| let conn = self.get_or_insert(guest_addr, Some(host_port), None)?; |
There was a problem hiding this comment.
bind_udp_port() uses ip_addr to build guest_addr, but that SocketAddr is later used as the guest destination when forwarding inbound packets (UdpConnection::poll_conn uses the map key as the dst IP/port). Passing the host bind address here will cause inbound forwarded UDP packets to target the wrong guest IP (and None currently becomes 0.0.0.0). This likely needs a separate UDP listener map keyed by guest_port, and should always forward to the configured guest IP (e.g. params.client_ip / client_ip_ipv6) regardless of host bind address.
| std::mem::take(&mut queue.endpoint_state.as_mut().unwrap().port_forwards); | ||
| queue.with_consomme_no_pool(|c| { | ||
| c.refresh_driver(); | ||
| for fwd in port_forwards { |
There was a problem hiding this comment.
Seems like we're rebinding on each get_queues call. Is this right? Shouldn't we do this once at construction time?
There was a problem hiding this comment.
It will only happen once because we std::mem::take the port_forwards Vec. The issue is that get_queues is where we are given a Driver which is needed for a PolledSocket. This was on the easier side of solutions, but we can plumb in the Driver elsewhere, add some comments, split it out to a separate fn with comments, etc.
| pub host_address: Option<String>, | ||
| /// The host port to listen on. | ||
| pub host_port: u16, |
There was a problem hiding this comment.
The most sandboxing-friendly approach would be to just take a Socket here--require the bind to happen at VM construction time.
Failing that, we should at least parse this down to a better data type than String (although I imagine you're just following the precedent of cidr: Option<String>, which I regret.
For consomme, forward host ports into the guest with
hostfwd=:--net consomme:hostfwd=tcp::3389-:3389
--net consomme:hostfwd=tcp:127.0.0.1:8080-:80
--net consomme:hostfwd=tcp:[::1]:8080-:80
--net consomme:10.0.0.0/24,hostfwd=tcp::22-:22,hostfwd=udp::5000-:5000