diff --git a/docs/live_migration.md b/docs/live_migration.md index f1edce5bd..72e5d9353 100644 --- a/docs/live_migration.md +++ b/docs/live_migration.md @@ -135,6 +135,18 @@ src $ ch-remote --api-socket=/tmp/api send-migration destination_url=unix:/tmp/s When the above commands completed, the VM should be successfully migrated to the destination machine without interrupting the workload. +### Network Announcements After Resume + +After a VM resumes from migration, snapshot restore, or any other path +that restores a previously paused VM, Cloud Hypervisor asks supported +network devices to announce the VM from its new host. For `virtio-net`, +the current implementation sets `VIRTIO_NET_S_ANNOUNCE`, raises a config +interrupt, retries that request a few times in the background, and also +sends host-side RARP announcements on the TAP interfaces. A guest +re-announcement therefore only happens when the guest negotiated +`VIRTIO_NET_F_GUEST_ANNOUNCE`. For `vhost-user-net`, the current implementation +only uses the guest announcement path. + ### TCP Socket Migration If TCP socket is selected for migration, we need to consider migrating @@ -190,6 +202,8 @@ After completing the above commands, the source VM will be migrated to the destination host and continue running there. The source VM instance will terminate normally. All ongoing processes and connections within the VM should remain intact after the migration. +See [Network Announcements After Resume](#network-announcements-after-resume) +for the announcement behavior after a VM resumes. #### Encryption diff --git a/docs/snapshot_restore.md b/docs/snapshot_restore.md index 4932945c7..86e7ca68f 100644 --- a/docs/snapshot_restore.md +++ b/docs/snapshot_restore.md @@ -102,6 +102,9 @@ after restore completes: At this point, the VM is fully restored and is identical to the VM which was snapshot earlier. +See [Network Announcements After Resume](live_migration.md#network-announcements-after-resume) +for the announcement behavior after restore/resume. + Restore also supports selecting how guest memory is populated: ```bash diff --git a/net_util/src/ctrl_queue.rs b/net_util/src/ctrl_queue.rs index 28b41c238..2dcc038b6 100644 --- a/net_util/src/ctrl_queue.rs +++ b/net_util/src/ctrl_queue.rs @@ -3,6 +3,8 @@ // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause use std::result; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use log::{debug, error, info, warn}; use thiserror::Error; @@ -74,18 +76,21 @@ fn is_tolerated_ctrl_command(ctrl_hdr: ControlHeader) -> bool { u32::from(ctrl_hdr.cmd), VIRTIO_NET_CTRL_VLAN_ADD | VIRTIO_NET_CTRL_VLAN_DEL ), - VIRTIO_NET_CTRL_ANNOUNCE => u32::from(ctrl_hdr.cmd) == VIRTIO_NET_CTRL_ANNOUNCE_ACK, _ => false, } } pub struct CtrlQueue { pub taps: Vec, + pub announce_pending: Arc, } impl CtrlQueue { - pub fn new(taps: Vec) -> Self { - CtrlQueue { taps } + pub fn new(taps: Vec, announce_pending: Arc) -> Self { + CtrlQueue { + taps, + announce_pending, + } } pub fn process( @@ -106,22 +111,22 @@ impl CtrlQueue { .map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?, ) .map_err(Error::GuestMemory)?; - let data_desc = desc_chain.next().ok_or(Error::NoDataDescriptor)?; - let data_desc_addr = data_desc - .addr() - .translate_gva(access_platform, data_desc.len() as usize) - .map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?; - - let status_desc = desc_chain.next().ok_or(Error::NoStatusDescriptor)?; - - let ok = match u32::from(ctrl_hdr.class) { + let (ok, status_desc) = match u32::from(ctrl_hdr.class) { VIRTIO_NET_CTRL_MQ => { + let data_desc = desc_chain.next().ok_or(Error::NoDataDescriptor)?; + let data_desc_addr = data_desc + .addr() + .translate_gva(access_platform, data_desc.len() as usize) + .map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?; + + let status_desc = desc_chain.next().ok_or(Error::NoStatusDescriptor)?; + let queue_pairs = desc_chain .memory() .read_obj::(data_desc_addr) .map_err(Error::GuestMemory)?; - if u32::from(ctrl_hdr.cmd) != VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET { + let ok = if u32::from(ctrl_hdr.cmd) != VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET { warn!("Unsupported command: {}", ctrl_hdr.cmd); false } else if (queue_pairs < VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MIN as u16) @@ -132,14 +137,23 @@ impl CtrlQueue { } else { info!("Number of MQ pairs requested: {queue_pairs}"); true - } + }; + (ok, status_desc) } VIRTIO_NET_CTRL_GUEST_OFFLOADS => { + let data_desc = desc_chain.next().ok_or(Error::NoDataDescriptor)?; + let data_desc_addr = data_desc + .addr() + .translate_gva(access_platform, data_desc.len() as usize) + .map_err(|e| Error::GuestMemory(GuestMemoryError::IOError(e)))?; + + let status_desc = desc_chain.next().ok_or(Error::NoStatusDescriptor)?; + let features = desc_chain .memory() .read_obj::(data_desc_addr) .map_err(Error::GuestMemory)?; - if u32::from(ctrl_hdr.cmd) == VIRTIO_NET_CTRL_GUEST_OFFLOADS_SET { + let ok = if u32::from(ctrl_hdr.cmd) == VIRTIO_NET_CTRL_GUEST_OFFLOADS_SET { let mut ok = true; for tap in self.taps.iter_mut() { info!("Reprogramming tap offload with features: {features}"); @@ -154,15 +168,31 @@ impl CtrlQueue { } else { warn!("Unsupported command: {}", ctrl_hdr.cmd); false - } + }; + (ok, status_desc) } - _ if is_tolerated_ctrl_command(ctrl_hdr) => { - debug!("Ignoring unsupported but tolerated control command {ctrl_hdr:?}"); - true + VIRTIO_NET_CTRL_ANNOUNCE => { + let status_desc = desc_chain.next().ok_or(Error::NoStatusDescriptor)?; + let ok = if u32::from(ctrl_hdr.cmd) == VIRTIO_NET_CTRL_ANNOUNCE_ACK { + self.announce_pending.store(false, Ordering::Release); + true + } else { + warn!("Unsupported command: {}", ctrl_hdr.cmd); + false + }; + (ok, status_desc) } _ => { - warn!("Unsupported command {ctrl_hdr:?}"); - false + let _data_desc = desc_chain.next().ok_or(Error::NoDataDescriptor)?; + let status_desc = desc_chain.next().ok_or(Error::NoStatusDescriptor)?; + let ok = if is_tolerated_ctrl_command(ctrl_hdr) { + debug!("Ignoring unsupported but tolerated control command {ctrl_hdr:?}"); + true + } else { + warn!("Unsupported command {ctrl_hdr:?}"); + false + }; + (ok, status_desc) } }; @@ -193,3 +223,116 @@ impl CtrlQueue { Ok(()) } } + +#[cfg(test)] +mod unit_tests { + use std::mem::size_of; + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, Ordering}; + + use virtio_bindings::virtio_ring::{VRING_DESC_F_NEXT, VRING_DESC_F_WRITE}; + use vm_memory::{Bytes, GuestAddress}; + use vm_virtio::queue::testing::VirtQueue as GuestQ; + + use super::*; + use crate::GuestMemoryMmap; + + #[test] + fn test_process_announce_ack_without_data_descriptor() { + // Build a minimal control virtqueue with one available request. + // + // The descriptor chain models the Linux ANNOUNCE_ACK layout: + // 1. readable control header descriptor + // 2. writable status descriptor + // + // There is intentionally no command-specific data descriptor between + // them. The parser must still accept the request, clear the pending + // flag, and write VIRTIO_NET_OK to the status byte. + const MEM_SIZE: usize = 0x20_0000; + const QSIZE: u16 = 2; + const QUEUE_ADDR: u64 = 0x0010_0000; + const HEADER_ADDR: u64 = 0x0011_0000; + const STATUS_ADDR: u64 = 0x0011_1000; + + let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), MEM_SIZE)]).unwrap(); + let guest_q = GuestQ::new(GuestAddress(QUEUE_ADDR), &mem, QSIZE); + let mut queue = guest_q.create_queue(); + + // Descriptor 0 points at the control header and continues to the + // trailing status descriptor. + guest_q.dtable[0].set( + HEADER_ADDR, + size_of::() as u32, + VRING_DESC_F_NEXT.try_into().unwrap(), + 1, + ); + // Descriptor 1 is the writable ack/status byte produced by the device. + guest_q.dtable[1].set(STATUS_ADDR, 1, VRING_DESC_F_WRITE.try_into().unwrap(), 0); + // Publish the descriptor chain by placing head descriptor 0 into the + // avail ring and advancing idx to one entry. + guest_q.avail.ring[0].set(0); + guest_q.avail.idx.set(1); + + // Seed guest memory with the ANNOUNCE_ACK control header and a sentinel + // status byte so the test can verify the device overwrites it. + mem.write_obj( + ControlHeader { + class: VIRTIO_NET_CTRL_ANNOUNCE as u8, + cmd: VIRTIO_NET_CTRL_ANNOUNCE_ACK as u8, + }, + GuestAddress(HEADER_ADDR), + ) + .unwrap(); + mem.write_obj(0xff_u8, GuestAddress(STATUS_ADDR)).unwrap(); + + let announce_pending = Arc::new(AtomicBool::new(true)); + let mut ctrl_q = CtrlQueue::new(Vec::new(), Arc::clone(&announce_pending)); + + ctrl_q.process(&mem, &mut queue, None).unwrap(); + + assert!(!announce_pending.load(Ordering::Acquire)); + assert_eq!( + mem.read_obj::(GuestAddress(STATUS_ADDR)).unwrap(), + VIRTIO_NET_OK as u8 + ); + } + + #[test] + fn test_process_guest_offloads_without_data_descriptor_fails() { + // Build a malformed control virtqueue request for a data-bearing + // command. The chain contains only the readable control header and no + // command-specific payload descriptor. + // + // GUEST_OFFLOADS_SET requires a data descriptor, so process() must + // reject this header-only request with NoDataDescriptor. + const MEM_SIZE: usize = 0x20_0000; + const QSIZE: u16 = 1; + const QUEUE_ADDR: u64 = 0x0012_0000; + const HEADER_ADDR: u64 = 0x0013_0000; + + let mem = GuestMemoryMmap::from_ranges(&[(GuestAddress(0), MEM_SIZE)]).unwrap(); + let guest_q = GuestQ::new(GuestAddress(QUEUE_ADDR), &mem, QSIZE); + let mut queue = guest_q.create_queue(); + + // Publish a single descriptor that contains only the control header. + guest_q.dtable[0].set(HEADER_ADDR, size_of::() as u32, 0, 0); + guest_q.avail.ring[0].set(0); + guest_q.avail.idx.set(1); + + mem.write_obj( + ControlHeader { + class: VIRTIO_NET_CTRL_GUEST_OFFLOADS as u8, + cmd: VIRTIO_NET_CTRL_GUEST_OFFLOADS_SET as u8, + }, + GuestAddress(HEADER_ADDR), + ) + .unwrap(); + + let mut ctrl_q = CtrlQueue::new(Vec::new(), Arc::new(AtomicBool::new(false))); + + assert!(matches!( + ctrl_q.process(&mem, &mut queue, None), + Err(Error::NoDataDescriptor) + )); + } +} diff --git a/net_util/src/lib.rs b/net_util/src/lib.rs index d5a74b40f..4337ae4d0 100644 --- a/net_util/src/lib.rs +++ b/net_util/src/lib.rs @@ -101,7 +101,7 @@ fn create_unix_socket() -> Result { Ok(unsafe { net::UdpSocket::from_raw_fd(sock) }) } -fn vnet_hdr_len() -> usize { +pub fn vnet_hdr_len() -> usize { size_of::() } diff --git a/virtio-devices/src/lib.rs b/virtio-devices/src/lib.rs index e4c802fae..c81f37087 100644 --- a/virtio-devices/src/lib.rs +++ b/virtio-devices/src/lib.rs @@ -15,6 +15,7 @@ use std::{fmt, io, result}; use serde::{Deserialize, Serialize}; use thiserror::Error; +use vmm_sys_util::errno::Error as ErrnoError; #[macro_use] mod device; @@ -122,6 +123,8 @@ pub enum ActivateError { CreateRateLimiter(#[source] io::Error), #[error("Failed to activate the vDPA device")] ActivateVdpa(#[source] vdpa::Error), + #[error("Failed to create TimerFd")] + CreateTimerFd(#[source] ErrnoError), } pub type ActivateResult = result::Result<(), ActivateError>; diff --git a/virtio-devices/src/net.rs b/virtio-devices/src/net.rs index b26e541e6..ca3cb73f5 100644 --- a/virtio-devices/src/net.rs +++ b/virtio-devices/src/net.rs @@ -6,22 +6,25 @@ // found in the THIRD-PARTY file. use std::collections::HashMap; +use std::io::{self, Write}; use std::net::IpAddr; use std::num::Wrapping; use std::ops::Deref; use std::os::unix::io::{AsRawFd, RawFd}; -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::result; +use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering}; use std::sync::{Arc, Barrier}; -use std::{io, result}; +use std::time::Duration; -use anyhow::anyhow; +use anyhow::{Context, anyhow}; use event_monitor::event; use log::{debug, error, info, warn}; #[cfg(not(fuzzing))] use net_util::virtio_features_to_tap_offload; use net_util::{ - CtrlQueue, MacAddr, NetCounters, NetQueuePair, OpenTapError, RxVirtio, Tap, TapError, TxVirtio, - VirtioNetConfig, build_net_config_space, build_net_config_space_with_mq, open_tap, + CtrlQueue, MAC_ADDR_LEN, MacAddr, NetCounters, NetQueuePair, OpenTapError, RxVirtio, Tap, + TapError, TxVirtio, VirtioNetConfig, build_net_config_space, build_net_config_space_with_mq, + open_tap, vnet_hdr_len, }; use seccompiler::SeccompAction; use serde::{Deserialize, Serialize}; @@ -34,6 +37,7 @@ use vm_memory::{ByteValued, GuestAddressSpace, GuestMemoryAtomic}; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; use vm_virtio::AccessPlatform; use vmm_sys_util::eventfd::EventFd; +use vmm_sys_util::timerfd::TimerFd; use super::{ ActivateError, ActivateResult, EPOLL_HELPER_EVENT_LAST, EpollHelper, EpollHelperError, @@ -47,6 +51,10 @@ use crate::{GuestMemoryMmap, VirtioInterrupt}; /// Control queue // Event available on the control queue. const CTRL_QUEUE_EVENT: u16 = EPOLL_HELPER_EVENT_LAST + 1; +// Start post-migration or post-restore announcements. +const START_ANNOUNCEMENTS_EVENT: u16 = CTRL_QUEUE_EVENT + 1; +// Retry post-migration or post-restore announcements. +const RETRY_ANNOUNCEMENTS_EVENT: u16 = START_ANNOUNCEMENTS_EVENT + 1; // Following the VIRTIO specification, the MTU should be at least 1280. pub const MIN_MTU: u16 = 1280; @@ -61,6 +69,9 @@ pub struct NetCtrlEpollHandler { pub access_platform: Option>, pub interrupt_cb: Arc, pub queue_index: u16, + pub announce_evt: EventFd, + pub announce_retry_timer: TimerFd, + pub announcer: Announcer, } impl NetCtrlEpollHandler { @@ -80,10 +91,34 @@ impl NetCtrlEpollHandler { ) -> result::Result<(), EpollHelperError> { let mut helper = EpollHelper::new(&self.kill_evt, &self.pause_evt)?; helper.add_event(self.queue_evt.as_raw_fd(), CTRL_QUEUE_EVENT)?; + helper.add_event(self.announce_evt.as_raw_fd(), START_ANNOUNCEMENTS_EVENT)?; + helper.add_event( + self.announce_retry_timer.as_raw_fd(), + RETRY_ANNOUNCEMENTS_EVENT, + )?; helper.run(paused, paused_sync, self)?; Ok(()) } + + const ANNOUNCE_RETRY_INTERVAL: Duration = Duration::from_millis(200); + + fn arm_retry_timer(&mut self) -> result::Result<(), EpollHelperError> { + self.announce_retry_timer + .reset( + Self::ANNOUNCE_RETRY_INTERVAL, + Some(Self::ANNOUNCE_RETRY_INTERVAL), + ) + .context("Failed to arm announcement retry timer") + .map_err(EpollHelperError::HandleEvent) + } + + fn disarm_retry_timer(&mut self) -> result::Result<(), EpollHelperError> { + self.announce_retry_timer + .clear() + .context("Failed to disarm announcement retry timer") + .map_err(EpollHelperError::HandleEvent) + } } impl EpollHelperHandler for NetCtrlEpollHandler { @@ -128,6 +163,31 @@ impl EpollHelperHandler for NetCtrlEpollHandler { } } } + START_ANNOUNCEMENTS_EVENT => { + self.announce_evt.read().map_err(|e| { + EpollHelperError::HandleEvent(anyhow!( + "Failed to get start announcements event: {e:?}" + )) + })?; + + self.announcer.initialize(); + match self.announcer.send_announce() { + AnnounceOutcome::Done => self.disarm_retry_timer()?, + AnnounceOutcome::Retry => self.arm_retry_timer()?, + } + } + RETRY_ANNOUNCEMENTS_EVENT => { + self.announce_retry_timer.wait().map_err(|e| { + EpollHelperError::HandleEvent(anyhow!( + "Failed to get retry announcements event: {e:?}" + )) + })?; + + match self.announcer.send_announce() { + AnnounceOutcome::Done => self.disarm_retry_timer()?, + AnnounceOutcome::Retry => {} + } + } _ => { return Err(EpollHelperError::HandleEvent(anyhow!( "Unknown event for virtio-net control queue" @@ -161,6 +221,10 @@ pub enum Error { TapError(#[source] TapError), #[error("Error calling dup() on tap fd")] DuplicateTapFd(#[source] io::Error), + #[error("Error creating EventFd")] + CreateEventFd(#[source] io::Error), + #[error("Error cloning EventFd")] + CloneEventFd(#[source] io::Error), } pub type Result = result::Result; @@ -395,6 +459,49 @@ impl EpollHelperHandler for NetEpollHandler { } } +// Minimum length of an ethernet frame. This size omits the FCS/CRC (frame check +// sequence), which will be added by the hardware. +const ETH_FRAME_LEN: usize = 60; + +/// Shared announcement bookkeeping for virtio-net backends. +pub struct AnnouncementState { + pub(crate) pending: Arc, + /// Generation counter used to invalidate active announcers before a + /// reset or device teardown, so they stop sending notifications. + pub(crate) generation: Arc, + /// When signaled, the epoll thread will do the announcements. + pub(crate) evt: EventFd, +} + +impl AnnouncementState { + pub fn new(pending: bool) -> io::Result { + Ok(Self { + pending: Arc::new(AtomicBool::new(pending)), + generation: Arc::new(AtomicU64::new(0)), + evt: EventFd::new(libc::EFD_NONBLOCK)?, + }) + } + + pub fn invalidate(&self) { + self.generation.fetch_add(1, Ordering::Release); + } + + pub fn reset(&self) { + self.generation.fetch_add(1, Ordering::Release); + self.pending.store(false, Ordering::Release); + } + + pub fn notify(&self, enabled: bool) { + if enabled && self.pending.load(Ordering::Acquire) { + self.generation.fetch_add(1, Ordering::Release); + self.evt + .write(1) + .inspect_err(|e| warn!("Could not write to announce EventFd: {e:?}")) + .ok(); + } + } +} + pub struct Net { common: VirtioCommon, id: String, @@ -405,6 +512,7 @@ pub struct Net { rate_limiter_config: Option, exit_evt: EventFd, device_status: Arc, + announce: AnnouncementState, } #[derive(Serialize, Deserialize)] @@ -444,67 +552,76 @@ impl Net { } }; - let (avail_features, acked_features, config, queue_sizes, paused) = if let Some(state) = - state - { - info!("Restoring virtio-net {id}"); - ( - state.avail_features, - state.acked_features, - state.config, - state.queue_size, - true, - ) - } else { - let mut avail_features = (1 << VIRTIO_RING_F_EVENT_IDX) | (1 << VIRTIO_F_VERSION_1); - - if mtu.is_some() { - avail_features |= 1 << VIRTIO_NET_F_MTU; - } - - if access_platform_enabled { - avail_features |= 1u64 << VIRTIO_F_ACCESS_PLATFORM; - } - - // Configure TSO/UFO features when hardware checksum offload is enabled. - if offload_csum { - avail_features |= (1 << VIRTIO_NET_F_CSUM) - | (1 << VIRTIO_NET_F_GUEST_CSUM) - | (1 << VIRTIO_NET_F_CTRL_GUEST_OFFLOADS); - - if offload_tso { - avail_features |= (1 << VIRTIO_NET_F_HOST_ECN) - | (1 << VIRTIO_NET_F_HOST_TSO4) - | (1 << VIRTIO_NET_F_HOST_TSO6) - | (1 << VIRTIO_NET_F_GUEST_ECN) - | (1 << VIRTIO_NET_F_GUEST_TSO4) - | (1 << VIRTIO_NET_F_GUEST_TSO6); - } - - if offload_ufo { - avail_features |= (1 << VIRTIO_NET_F_HOST_UFO) | (1 << VIRTIO_NET_F_GUEST_UFO); - } - } - - avail_features |= 1 << VIRTIO_NET_F_CTRL_VQ; - avail_features |= 1 << VIRTIO_NET_F_STATUS; - let queue_num = num_queues + 1; - - let mut config = VirtioNetConfig::default(); - if let Some(mac) = guest_mac { - build_net_config_space(&mut config, mac, num_queues, mtu, &mut avail_features); + let (avail_features, acked_features, config, queue_sizes, paused, announce_pending) = + if let Some(state) = state { + info!("Restoring virtio-net {id}"); + // Always mark the announcement pending if the device was restored + // so the device announces itself. + ( + state.avail_features, + state.acked_features, + state.config, + state.queue_size, + true, + true, + ) } else { - build_net_config_space_with_mq(&mut config, num_queues, mtu, &mut avail_features); - } + let mut avail_features = (1 << VIRTIO_RING_F_EVENT_IDX) | (1 << VIRTIO_F_VERSION_1); - ( - avail_features, - 0, - config, - vec![queue_size; queue_num], - false, - ) - }; + if mtu.is_some() { + avail_features |= 1 << VIRTIO_NET_F_MTU; + } + + if access_platform_enabled { + avail_features |= 1u64 << VIRTIO_F_ACCESS_PLATFORM; + } + + // Configure TSO/UFO features when hardware checksum offload is enabled. + if offload_csum { + avail_features |= (1 << VIRTIO_NET_F_CSUM) + | (1 << VIRTIO_NET_F_GUEST_CSUM) + | (1 << VIRTIO_NET_F_CTRL_GUEST_OFFLOADS); + + if offload_tso { + avail_features |= (1 << VIRTIO_NET_F_HOST_ECN) + | (1 << VIRTIO_NET_F_HOST_TSO4) + | (1 << VIRTIO_NET_F_HOST_TSO6) + | (1 << VIRTIO_NET_F_GUEST_ECN) + | (1 << VIRTIO_NET_F_GUEST_TSO4) + | (1 << VIRTIO_NET_F_GUEST_TSO6); + } + + if offload_ufo { + avail_features |= + (1 << VIRTIO_NET_F_HOST_UFO) | (1 << VIRTIO_NET_F_GUEST_UFO); + } + } + + avail_features |= 1 << VIRTIO_NET_F_CTRL_VQ; + avail_features |= 1 << VIRTIO_NET_F_STATUS; + let queue_num = num_queues + 1; + + let mut config = VirtioNetConfig::default(); + if let Some(mac) = guest_mac { + build_net_config_space(&mut config, mac, num_queues, mtu, &mut avail_features); + } else { + build_net_config_space_with_mq( + &mut config, + num_queues, + mtu, + &mut avail_features, + ); + } + + ( + avail_features, + 0, + config, + vec![queue_size; queue_num], + false, + false, + ) + }; Ok(Net { common: VirtioCommon { @@ -525,6 +642,7 @@ impl Net { rate_limiter_config, exit_evt, device_status: Arc::new(AtomicU8::new(0)), + announce: AnnouncementState::new(announce_pending).map_err(Error::CreateEventFd)?, }) } @@ -646,11 +764,54 @@ impl Net { if self.common.feature_acked(VIRTIO_NET_F_STATUS.into()) { status |= VIRTIO_NET_S_LINK_UP as u16; + + if self.announce.pending.load(Ordering::Acquire) { + status |= VIRTIO_NET_S_ANNOUNCE as u16; + } } status } + // Builds a reverse ARP packet with this device's MAC address. Without a + // negotiated VIRTIO_NET_F_MAC feature, valid construction paths may leave + // config.mac as zeros, which must not be announced on the host network. + fn build_rarp_announce(&self) -> Option<[u8; ETH_FRAME_LEN]> { + if !self.common.feature_acked(VIRTIO_NET_F_MAC.into()) { + return None; + } + + const ETH_P_RARP: u16 = 0x8035; // Ethertype RARP + const ARP_HTYPE_ETH: u16 = 0x1; // Hardware type Ethernet + const ARP_PTYPE_IP: u16 = 0x0800; // Protocol type IPv4 + const ARP_OP_REQUEST_REV: u16 = 0x0003; // RARP Request opcode + + const IPV4_ADDR_LENGTH: usize = 4; // Size of an IPv4 address + + let mut buf = [0u8; ETH_FRAME_LEN]; + + // Ethernet header + buf[0..6].copy_from_slice(&[0xff; MAC_ADDR_LEN]); // This is a broadcast + buf[6..12].copy_from_slice(&self.config.mac); // Src is this NIC + buf[12..14].copy_from_slice(Ð_P_RARP.to_be_bytes()); // This is a RARP packet + + // ARP Header + buf[14..16].copy_from_slice(&ARP_HTYPE_ETH.to_be_bytes()); + buf[16..18].copy_from_slice(&ARP_PTYPE_IP.to_be_bytes()); + buf[18] = MAC_ADDR_LEN as u8; // Hardware address length (ethernet) + buf[19] = IPV4_ADDR_LENGTH as u8; // Protocol address length (IPv4) + // This is a "fake RARP" packet, we don't want to perform a real RARP lookup. + // Thus the content of the next fields is largely irrelevant. Setting source + // hardware address = target hardware address is fine according to RFC 903. + buf[20..22].copy_from_slice(&ARP_OP_REQUEST_REV.to_be_bytes()); + buf[22..28].copy_from_slice(&self.config.mac); // Source hardware address + buf[28..32].copy_from_slice(&[0x00; IPV4_ADDR_LENGTH]); // Source protocol address + buf[32..38].copy_from_slice(&self.config.mac); // Target hardware address + buf[38..42].copy_from_slice(&[0x00; IPV4_ADDR_LENGTH]); // Target protocol address + + Some(buf) + } + #[cfg(fuzzing)] pub fn wait_for_epoll_threads(&mut self) { self.common.wait_for_epoll_threads(); @@ -707,16 +868,45 @@ impl VirtioDevice for Net { ctrl_queue.set_event_idx(event_idx); let (kill_evt, pause_evt) = self.common.dup_eventfds()?; + + let guest_announce_ops = VirtioNetGuestAnnounceOps::new( + interrupt_cb.clone(), + self.common + .feature_acked(VIRTIO_NET_F_GUEST_ANNOUNCE.into()), + &self.announce, + ); + + let host_announce_ops = VirtioNetHostAnnounceOps::new( + self.build_rarp_announce(), + self.taps.clone().into_boxed_slice(), + ); + + let announcer = Announcer::new( + &self.announce, + vec![ + Box::new(guest_announce_ops) as Box, + Box::new(host_announce_ops) as Box, + ] + .into_boxed_slice(), + ); + let mut ctrl_handler = NetCtrlEpollHandler { mem: mem.clone(), kill_evt, pause_evt, - ctrl_q: CtrlQueue::new(self.taps.clone()), + ctrl_q: CtrlQueue::new(self.taps.clone(), self.announce.pending.clone()), queue: ctrl_queue, queue_evt: ctrl_queue_evt, access_platform: self.common.access_platform(), queue_index: ctrl_queue_index as u16, interrupt_cb: interrupt_cb.clone(), + announce_evt: self + .announce + .evt + .try_clone() + .map_err(ActivateError::CloneEventFd)?, + announce_retry_timer: TimerFd::new().map_err(ActivateError::CreateTimerFd)?, + announcer, }; let paused = self.common.paused.clone(); @@ -809,12 +999,15 @@ impl VirtioDevice for Net { )?; } + self.announce.notify(true); + event!("virtio-device", "activated", "id", &self.id); Ok(()) } fn reset(&mut self) { self.common.reset(); + self.announce.reset(); event!("virtio-device", "reset", "id", &self.id); } @@ -852,11 +1045,14 @@ impl VirtioDevice for Net { impl Pausable for Net { fn pause(&mut self) -> result::Result<(), MigratableError> { + self.announce.invalidate(); self.common.pause() } fn resume(&mut self) -> result::Result<(), MigratableError> { - self.common.resume() + self.common.resume()?; + self.announce.notify(true); + Ok(()) } } @@ -870,22 +1066,182 @@ impl Snapshottable for Net { } } impl Transportable for Net {} -impl Migratable for Net {} +impl Migratable for Net { + fn start_migration(&mut self) -> result::Result<(), MigratableError> { + self.announce.invalidate(); + Ok(()) + } +} + +/// Whether announcements have to be retried. To avoid ambiguity when using a bool, +/// this enum clearly describes whether announcements are done, or have to be +/// retried. +#[derive(Clone)] +pub enum AnnounceOutcome { + Retry, + Done, +} + +/// Backend-specific logic for driving announcements. +pub trait AnnounceOps: Send { + /// Send an announcement and return whether this function has to be executed + /// again. + fn send_announce(&mut self) -> AnnounceOutcome; +} + +pub struct Announcer { + announce_generation: Arc, + generation: u64, + announcements_done: usize, + announce_ops: Box<[Box]>, +} + +impl Announcer { + const MAX_ANNOUNCEMENTS: usize = 5; + + pub fn new(announce: &AnnouncementState, announce_ops: Box<[Box]>) -> Self { + Self { + announce_generation: announce.generation.clone(), + generation: 0, + announcements_done: 0, + announce_ops, + } + } + + pub fn initialize(&mut self) { + self.generation = self.announce_generation.load(Ordering::Acquire); + self.announcements_done = 0; + } + + /// Execute all announcers and return whether more announcements are necessary. + pub fn send_announce(&mut self) -> AnnounceOutcome { + if self.announce_generation.load(Ordering::Acquire) != self.generation + || self.announcements_done >= Self::MAX_ANNOUNCEMENTS + { + return AnnounceOutcome::Done; + } + + let announce_outcomes = self + .announce_ops + .iter_mut() + .map(|ops| ops.send_announce()) + .collect::>(); + + self.announcements_done += 1; + if self.announcements_done < Self::MAX_ANNOUNCEMENTS + && announce_outcomes + .iter() + .any(|outcome| matches!(outcome, AnnounceOutcome::Retry)) + { + return AnnounceOutcome::Retry; + } + + AnnounceOutcome::Done + } +} + +pub(crate) struct VirtioNetGuestAnnounceOps { + interrupt_cb: Arc, + guest_announce_negotiated: bool, + announce_pending: Arc, +} + +impl VirtioNetGuestAnnounceOps { + pub fn new( + interrupt_cb: Arc, + guest_announce_negotiated: bool, + announce: &AnnouncementState, + ) -> Self { + Self { + interrupt_cb, + guest_announce_negotiated, + announce_pending: announce.pending.clone(), + } + } +} + +impl AnnounceOps for VirtioNetGuestAnnounceOps { + fn send_announce(&mut self) -> AnnounceOutcome { + if !self.guest_announce_negotiated { + self.announce_pending.store(false, Ordering::Release); + return AnnounceOutcome::Done; + } + + // If the guest hasn't ack'ed the announce, we trigger the interrupt. + if self.announce_pending.load(Ordering::Acquire) { + self.interrupt_cb + .trigger(VirtioInterruptType::Config) + .inspect_err(|e| { + warn!("Unable to send interrupt for virtio-net device: {e}"); + }) + .ok(); + + // We have to check again whether the driver ack'ed the announcement. + return AnnounceOutcome::Retry; + } + AnnounceOutcome::Done + } +} + +struct VirtioNetHostAnnounceOps { + rarp_announce: Option<[u8; ETH_FRAME_LEN]>, + taps: Box<[Tap]>, +} + +impl VirtioNetHostAnnounceOps { + pub fn new(rarp_announce: Option<[u8; ETH_FRAME_LEN]>, taps: Box<[Tap]>) -> Self { + Self { + rarp_announce, + taps, + } + } +} + +impl AnnounceOps for VirtioNetHostAnnounceOps { + fn send_announce(&mut self) -> AnnounceOutcome { + if let Some(rarp_announce) = self.rarp_announce { + // The TAP fd expects the virtio-net header configured by + // TUNSETVNETHDRSZ before the Ethernet frame. + let mut buf = vec![0u8; vnet_hdr_len() + rarp_announce.len()]; + buf[vnet_hdr_len()..].copy_from_slice(&rarp_announce); + + for tap in &mut self.taps { + if let Err(e) = tap.write(&buf) { + // The host-side RARP packets are best-effort. Thus, to keep things simple, we + // only log errors here instead of waiting for the TAP to become writable again. + error!("Host RARP write to TAP failed: {e}"); + } + } + + return AnnounceOutcome::Retry; + } + + AnnounceOutcome::Done + } +} #[cfg(test)] mod unit_tests { - use std::mem::size_of; + use std::mem::{offset_of, size_of}; + use std::sync::Arc; + use std::sync::atomic::{AtomicUsize, Ordering}; use seccompiler::SeccompAction; - use virtio_bindings::virtio_net::{VIRTIO_NET_F_STATUS, VIRTIO_NET_S_LINK_UP}; + use virtio_bindings::virtio_net::{ + VIRTIO_NET_F_STATUS, VIRTIO_NET_S_ANNOUNCE, VIRTIO_NET_S_LINK_UP, + }; use vmm_sys_util::eventfd::EventFd; use super::*; - fn test_net(acked_features: u64) -> Net { - Net { + fn test_net( + acked_features: u64, + interrupt_cb: Option>, + ) -> Result { + Ok(Net { common: VirtioCommon { acked_features, + interrupt_cb, ..Default::default() }, id: "test-net".to_string(), @@ -896,10 +1252,11 @@ mod unit_tests { rate_limiter_config: None, exit_evt: EventFd::new(libc::EFD_NONBLOCK).unwrap(), device_status: Arc::new(AtomicU8::new(0)), - } + announce: AnnouncementState::new(false).map_err(Error::CreateEventFd)?, + }) } - const STATUS_OFFSET: usize = std::mem::offset_of!(VirtioNetConfig, status); + const STATUS_OFFSET: usize = offset_of!(VirtioNetConfig, status); fn read_status(device: &Net) -> u16 { let mut data = vec![0; size_of::()]; device.read_config(0, &mut data); @@ -915,8 +1272,248 @@ mod unit_tests { fn test_status_feature_reports_link_up() { // The current implementation should always report "link up" if // VIRTIO_NET_F_STATUS has been negotiated. - let net = test_net(1 << VIRTIO_NET_F_STATUS); + let net = test_net(1 << VIRTIO_NET_F_STATUS, None).unwrap(); assert_eq!(read_status(&net), VIRTIO_NET_S_LINK_UP as u16); } + + struct TestInterrupt { + config_count: AtomicUsize, + } + + impl TestInterrupt { + fn new() -> Self { + Self { + config_count: AtomicUsize::new(0), + } + } + } + + impl VirtioInterrupt for TestInterrupt { + fn trigger(&self, int_type: VirtioInterruptType) -> result::Result<(), io::Error> { + if matches!(int_type, VirtioInterruptType::Config) { + self.config_count.fetch_add(1, Ordering::AcqRel); + } + Ok(()) + } + + fn set_notifier( + &self, + _int_type: u32, + _notifier: Option, + _vm: &dyn hypervisor::Vm, + ) -> io::Result<()> { + unimplemented!() + } + } + + fn test_announcer(dev: &Net) -> Result { + let guest_announce_ops = VirtioNetGuestAnnounceOps::new( + dev.common.interrupt_cb.clone().unwrap(), + dev.common.feature_acked(VIRTIO_NET_F_GUEST_ANNOUNCE.into()), + &dev.announce, + ); + + let host_announce_ops = VirtioNetHostAnnounceOps::new( + dev.build_rarp_announce(), + dev.taps.clone().into_boxed_slice(), + ); + + let announcer = Announcer::new( + &dev.announce, + vec![ + Box::new(guest_announce_ops) as Box, + Box::new(host_announce_ops) as Box, + ] + .into_boxed_slice(), + ); + + Ok(announcer) + } + + #[test] + fn test_announcer_stop_retrying_on_generation_change() { + let interrupt = Arc::new(TestInterrupt::new()); + let net = test_net( + (1 << VIRTIO_NET_F_STATUS) | (1 << VIRTIO_NET_F_GUEST_ANNOUNCE), + Some(interrupt.clone() as Arc), + ) + .unwrap(); + let mut announcer = test_announcer(&net).unwrap(); + + net.announce.pending.store(true, Ordering::Release); + + announcer.initialize(); + assert!(matches!(announcer.send_announce(), AnnounceOutcome::Retry)); + + net.announce.generation.store(1, Ordering::Release); + + assert!(matches!(announcer.send_announce(), AnnounceOutcome::Done)); + assert!(net.announce.pending.load(Ordering::Acquire)); + } + + #[test] + fn test_guest_ack_before_first_announce_run() { + let interrupt = Arc::new(TestInterrupt::new()); + let net = test_net( + (1 << VIRTIO_NET_F_STATUS) | (1 << VIRTIO_NET_F_GUEST_ANNOUNCE), + Some(interrupt.clone() as Arc), + ) + .unwrap(); + let mut announcer = test_announcer(&net).unwrap(); + + // Here we check what happens if the guest ACK arrives before the epoll thread + // does the first announcement. + net.announce.pending.store(true, Ordering::Release); + announcer.initialize(); + net.announce.pending.store(false, Ordering::Release); + + assert!(matches!(announcer.send_announce(), AnnounceOutcome::Done)); + assert!(!net.announce.pending.load(Ordering::Acquire)); + assert_eq!(read_status(&net) & VIRTIO_NET_S_ANNOUNCE as u16, 0); + assert_eq!(interrupt.config_count.load(Ordering::Acquire), 0); + } + + #[test] + fn test_post_migration_without_feature_is_noop() { + let interrupt = Arc::new(TestInterrupt::new()); + let net = test_net(0, Some(interrupt.clone() as Arc)).unwrap(); + let mut announcer = test_announcer(&net).unwrap(); + + net.announce.pending.store(true, Ordering::Release); + + announcer.initialize(); + assert!(matches!(announcer.send_announce(), AnnounceOutcome::Done)); + + assert!(!net.announce.pending.load(Ordering::Acquire)); + assert_eq!(read_status(&net) & VIRTIO_NET_S_ANNOUNCE as u16, 0); + assert_eq!(interrupt.config_count.load(Ordering::Acquire), 0); + } + + #[test] + fn test_reset_clears_pending_announce() { + let interrupt = Arc::new(TestInterrupt::new()); + let mut net = test_net( + (1 << VIRTIO_NET_F_GUEST_ANNOUNCE) | (1 << VIRTIO_NET_F_STATUS), + Some(interrupt.clone() as Arc), + ) + .unwrap(); + let mut announcer = test_announcer(&net).unwrap(); + + net.announce.pending.store(true, Ordering::Release); + + announcer.initialize(); + assert!(matches!(announcer.send_announce(), AnnounceOutcome::Retry)); + + assert!(net.announce.pending.load(Ordering::Acquire)); + + net.reset(); + + assert!(!net.announce.pending.load(Ordering::Acquire)); + assert_eq!(read_status(&net) & VIRTIO_NET_S_ANNOUNCE as u16, 0); + } + + fn assert_old_announcer_invalidated(invalidate: F) + where + F: FnOnce(&mut Net), + { + let interrupt = Arc::new(TestInterrupt::new()); + let mut net = test_net( + 1 << VIRTIO_NET_F_GUEST_ANNOUNCE, + Some(interrupt.clone() as Arc), + ) + .unwrap(); + let mut announcer = test_announcer(&net).unwrap(); + + net.announce.pending.store(true, Ordering::Release); + + announcer.initialize(); + assert!(matches!(announcer.send_announce(), AnnounceOutcome::Retry)); + assert_eq!(interrupt.config_count.load(Ordering::Acquire), 1); + + invalidate(&mut net); + assert!(matches!(announcer.send_announce(), AnnounceOutcome::Done)); + + assert_eq!(interrupt.config_count.load(Ordering::Acquire), 1); + } + + #[test] + fn test_reset_invalidates_old_announcer() { + assert_old_announcer_invalidated(|net| { + net.reset(); + }); + } + + #[test] + fn test_pause_invalidates_old_announcer() { + assert_old_announcer_invalidated(|net| { + net.pause().unwrap(); + }); + } + + #[test] + fn test_start_migration_invalidates_old_announcer() { + assert_old_announcer_invalidated(|net| { + net.start_migration().unwrap(); + }); + } + + struct RecordingAnnounceOps { + val: Arc, + outcome: AnnounceOutcome, + } + + impl AnnounceOps for RecordingAnnounceOps { + fn send_announce(&mut self) -> AnnounceOutcome { + self.val.fetch_add(1, Ordering::AcqRel); + self.outcome.clone() + } + } + + fn recording_test_announcer( + dev: &Net, + first_outcome: AnnounceOutcome, + second_outcome: AnnounceOutcome, + val: Arc, + ) -> Result { + let first_ops = RecordingAnnounceOps { + val: val.clone(), + outcome: first_outcome, + }; + let second_ops = RecordingAnnounceOps { + val, + outcome: second_outcome, + }; + + Ok(Announcer::new( + &dev.announce, + vec![ + Box::new(first_ops) as Box, + Box::new(second_ops) as Box, + ] + .into_boxed_slice(), + )) + } + + #[test] + fn test_all_announcers_run_before_retry_decision() { + let net = test_net( + (1 << VIRTIO_NET_F_STATUS) | (1 << VIRTIO_NET_F_GUEST_ANNOUNCE), + None, + ) + .unwrap(); + + let val = Arc::new(AtomicUsize::new(0)); + let mut announcer = recording_test_announcer( + &net, + AnnounceOutcome::Retry, + AnnounceOutcome::Done, + val.clone(), + ) + .unwrap(); + + announcer.initialize(); + assert!(matches!(announcer.send_announce(), AnnounceOutcome::Retry)); + assert_eq!(val.load(Ordering::Acquire), 2); + } } diff --git a/virtio-devices/src/seccomp_filters.rs b/virtio-devices/src/seccomp_filters.rs index 9b051ec5e..6cb03116c 100644 --- a/virtio-devices/src/seccomp_filters.rs +++ b/virtio-devices/src/seccomp_filters.rs @@ -186,7 +186,10 @@ fn create_virtio_net_ctl_ioctl_seccomp_rule() -> Vec { } fn virtio_net_ctl_thread_rules() -> Vec<(i64, Vec)> { - vec![(libc::SYS_ioctl, create_virtio_net_ctl_ioctl_seccomp_rule())] + vec![ + (libc::SYS_ioctl, create_virtio_net_ctl_ioctl_seccomp_rule()), + (libc::SYS_timerfd_settime, vec![]), + ] } fn virtio_pmem_thread_rules() -> Vec<(i64, Vec)> { @@ -244,7 +247,7 @@ fn virtio_generic_vhost_user_thread_rules() -> Vec<(i64, Vec)> { } fn virtio_vhost_net_ctl_thread_rules() -> Vec<(i64, Vec)> { - vec![] + vec![(libc::SYS_timerfd_settime, vec![])] } fn virtio_vhost_net_thread_rules() -> Vec<(i64, Vec)> { diff --git a/virtio-devices/src/vhost_user/mod.rs b/virtio-devices/src/vhost_user/mod.rs index 0c5d0b0c9..22405ddbf 100644 --- a/virtio-devices/src/vhost_user/mod.rs +++ b/virtio-devices/src/vhost_user/mod.rs @@ -59,8 +59,8 @@ pub enum Error { BadQueueNum, #[error("Failed binding vhost-user socket")] BindSocket(#[source] io::Error), - #[error("Creating kill eventfd failed")] - CreateKillEventFd(#[source] io::Error), + #[error("Creating eventfd failed")] + CreateEventFd(#[source] io::Error), #[error("Cloning kill eventfd failed")] CloneKillEventFd(#[source] io::Error), #[error("Invalid descriptor table address")] diff --git a/virtio-devices/src/vhost_user/net.rs b/virtio-devices/src/vhost_user/net.rs index 58fe99953..302dd5d2f 100644 --- a/virtio-devices/src/vhost_user/net.rs +++ b/virtio-devices/src/vhost_user/net.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use std::result; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Barrier, Mutex}; use log::{error, info}; @@ -11,11 +11,11 @@ use seccompiler::SeccompAction; use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures}; use vhost::vhost_user::{FrontendReqHandler, VhostUserFrontend, VhostUserFrontendReqHandler}; use virtio_bindings::virtio_net::{ - VIRTIO_NET_F_CSUM, VIRTIO_NET_F_CTRL_VQ, VIRTIO_NET_F_GUEST_CSUM, VIRTIO_NET_F_GUEST_ECN, - VIRTIO_NET_F_GUEST_TSO4, VIRTIO_NET_F_GUEST_TSO6, VIRTIO_NET_F_GUEST_UFO, - VIRTIO_NET_F_HOST_ECN, VIRTIO_NET_F_HOST_TSO4, VIRTIO_NET_F_HOST_TSO6, VIRTIO_NET_F_HOST_UFO, - VIRTIO_NET_F_MAC, VIRTIO_NET_F_MRG_RXBUF, VIRTIO_NET_F_MTU, VIRTIO_NET_F_STATUS, - VIRTIO_NET_S_LINK_UP, + VIRTIO_NET_F_CSUM, VIRTIO_NET_F_CTRL_VQ, VIRTIO_NET_F_GUEST_ANNOUNCE, VIRTIO_NET_F_GUEST_CSUM, + VIRTIO_NET_F_GUEST_ECN, VIRTIO_NET_F_GUEST_TSO4, VIRTIO_NET_F_GUEST_TSO6, + VIRTIO_NET_F_GUEST_UFO, VIRTIO_NET_F_HOST_ECN, VIRTIO_NET_F_HOST_TSO4, VIRTIO_NET_F_HOST_TSO6, + VIRTIO_NET_F_HOST_UFO, VIRTIO_NET_F_MAC, VIRTIO_NET_F_MRG_RXBUF, VIRTIO_NET_F_MTU, + VIRTIO_NET_F_STATUS, VIRTIO_NET_S_ANNOUNCE, VIRTIO_NET_S_LINK_UP, }; use virtio_bindings::virtio_ring::VIRTIO_RING_F_EVENT_IDX; use virtio_queue::QueueT; @@ -23,14 +23,16 @@ use vm_memory::ByteValued; use vm_migration::protocol::MemoryRangeTable; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; use vmm_sys_util::eventfd::EventFd; +use vmm_sys_util::timerfd::TimerFd; use crate::device::ActivationContext; +use crate::net::{AnnounceOps, AnnouncementState, Announcer, VirtioNetGuestAnnounceOps}; use crate::seccomp_filters::Thread; use crate::vhost_user::vu_common_ctrl::{VhostUserConfig, VhostUserHandle}; use crate::vhost_user::{DEFAULT_VIRTIO_FEATURES, Error, Result, VhostUserCommon, VhostUserState}; use crate::{ - ActivateResult, GuestRegionMmap, NetCtrlEpollHandler, VIRTIO_F_ACCESS_PLATFORM, VirtioCommon, - VirtioDevice, VirtioDeviceType, + ActivateError, ActivateResult, GuestRegionMmap, NetCtrlEpollHandler, VIRTIO_F_ACCESS_PLATFORM, + VirtioCommon, VirtioDevice, VirtioDeviceType, }; const DEFAULT_QUEUE_NUMBER: usize = 2; @@ -47,9 +49,27 @@ pub struct Net { seccomp_action: SeccompAction, exit_evt: EventFd, access_platform_enabled: bool, + announce: AnnouncementState, } impl Net { + /// Derive the guest-visible feature set from the backend-negotiated + /// features plus frontend-only bits that Cloud Hypervisor implements + /// locally, such as `VIRTIO_NET_F_MAC`, `VIRTIO_NET_F_STATUS`, and + /// `VIRTIO_NET_F_GUEST_ANNOUNCE`. + fn frontend_avail_features(backend_acked_features: u64) -> u64 { + let mut guest_avail_features = backend_acked_features | (1 << VIRTIO_NET_F_MAC); + + // Guest announce is implemented by the frontend through config + // changes and the locally handled control queue. + if guest_avail_features & (1 << VIRTIO_NET_F_CTRL_VQ) != 0 { + guest_avail_features |= 1 << VIRTIO_NET_F_STATUS; + guest_avail_features |= 1 << VIRTIO_NET_F_GUEST_ANNOUNCE; + } + + guest_avail_features + } + /// Create a new vhost-user-net device #[expect(clippy::too_many_arguments)] pub fn new( @@ -84,13 +104,16 @@ impl Net { config, paused, vring_bases, + announce_pending, ) = if let Some(state) = state { info!("Restoring vhost-user-net {id}"); // The backend acknowledged features must not contain frontend-only // bits since we don't expect the backend to handle them. - let backend_acked_features = - state.acked_features & !((1 << VIRTIO_NET_F_MAC) | (1 << VIRTIO_NET_F_STATUS)); + let backend_acked_features = state.acked_features + & !((1 << VIRTIO_NET_F_MAC) + | (1 << VIRTIO_NET_F_STATUS) + | (1 << VIRTIO_NET_F_GUEST_ANNOUNCE)); vu.set_protocol_features_vhost_user( backend_acked_features, @@ -105,6 +128,11 @@ impl Net { num_queues += 1; } + // Always set the announcement pending if the device was restored and + // VIRTIO_NET_F_GUEST_ANNOUNCE was negotiated, to make sure the device announces itself. + let announce_pending = + (state.acked_features & (1u64 << VIRTIO_NET_F_GUEST_ANNOUNCE)) != 0; + ( state.avail_features, state.acked_features, @@ -113,11 +141,13 @@ impl Net { state.config, true, state.vring_bases, + announce_pending, ) } else { // Filling device and vring features VMM supports. let mut avail_features = (1 << VIRTIO_NET_F_MRG_RXBUF) | (1 << VIRTIO_NET_F_CTRL_VQ) + | (1 << VIRTIO_NET_F_GUEST_ANNOUNCE) | DEFAULT_VIRTIO_FEATURES; if mtu.is_some() { @@ -152,7 +182,7 @@ impl Net { | VhostUserProtocolFeatures::LOG_SHMFD | VhostUserProtocolFeatures::DEVICE_STATE; - let (mut acked_features, acked_protocol_features) = + let (acked_features, acked_protocol_features) = vu.negotiate_features_vhost_user(avail_features, avail_protocol_features)?; let backend_num_queues = @@ -178,12 +208,12 @@ impl Net { num_queues += 1; } - // Make sure frontend-owned config-space features stay exposed to - // the guest, even if they are not negotiated with the backend. - acked_features |= (1 << VIRTIO_NET_F_MAC) | (1 << VIRTIO_NET_F_STATUS); + // Build the feature set that gets exposed to the guest. Some frontend available + // features are dependent on the features the backend supports. + let guest_avail_features = Self::frontend_avail_features(acked_features); ( - acked_features, + guest_avail_features, // If part of the available features that have been acked, // the PROTOCOL_FEATURES bit must be already set through // the VIRTIO acked features as we know the guest would @@ -194,6 +224,7 @@ impl Net { config, false, None, + false, ) }; @@ -222,6 +253,7 @@ impl Net { seccomp_action, exit_evt, access_platform_enabled, + announce: AnnouncementState::new(announce_pending).map_err(Error::CreateEventFd)?, }) } @@ -239,6 +271,10 @@ impl Net { .feature_acked(VIRTIO_NET_F_STATUS.into()) { status |= VIRTIO_NET_S_LINK_UP as u16; + + if self.announce.pending.load(Ordering::Acquire) { + status |= VIRTIO_NET_S_ANNOUNCE as u16; + } } status @@ -307,16 +343,36 @@ impl VirtioDevice for Net { let (kill_evt, pause_evt) = self.vu_common.virtio_common.dup_eventfds()?; + let announce_ops = VirtioNetGuestAnnounceOps::new( + interrupt_cb.clone(), + self.vu_common + .virtio_common + .feature_acked(VIRTIO_NET_F_GUEST_ANNOUNCE.into()), + &self.announce, + ); + + let announcer = Announcer::new( + &self.announce, + vec![Box::new(announce_ops) as Box].into_boxed_slice(), + ); + let mut ctrl_handler = NetCtrlEpollHandler { mem: mem.clone(), kill_evt, pause_evt, - ctrl_q: CtrlQueue::new(Vec::new()), + ctrl_q: CtrlQueue::new(Vec::new(), self.announce.pending.clone()), queue: ctrl_queue, queue_evt: ctrl_queue_evt, access_platform: None, interrupt_cb: interrupt_cb.clone(), queue_index: ctrl_queue_index as u16, + announce_evt: self + .announce + .evt + .try_clone() + .map_err(ActivateError::CloneEventFd)?, + announce_retry_timer: TimerFd::new().map_err(ActivateError::CreateTimerFd)?, + announcer, }; let paused = self.vu_common.virtio_common.paused.clone(); @@ -340,9 +396,11 @@ impl VirtioDevice for Net { let backend_req_handler: Option> = None; // The backend acknowledged features must not contain frontend-only - // bits since we don't expect the backend to handle them. + // features since we don't expect the backend to handle them. let backend_acked_features = self.vu_common.virtio_common.acked_features - & !((1 << VIRTIO_NET_F_MAC) | (1 << VIRTIO_NET_F_STATUS)); + & !((1 << VIRTIO_NET_F_MAC) + | (1 << VIRTIO_NET_F_STATUS) + | (1 << VIRTIO_NET_F_GUEST_ANNOUNCE)); // Run a dedicated thread for handling potential reconnections with // the backend. @@ -371,11 +429,18 @@ impl VirtioDevice for Net { move || handler.run(&paused, paused_sync.as_ref().unwrap()), )?; + self.announce.notify( + self.vu_common + .virtio_common + .feature_acked(VIRTIO_NET_F_GUEST_ANNOUNCE.into()), + ); + Ok(()) } fn reset(&mut self) { self.vu_common.reset(&self.id); + self.announce.reset(); } fn shutdown(&mut self) { @@ -392,13 +457,20 @@ impl VirtioDevice for Net { impl Pausable for Net { fn pause(&mut self) -> result::Result<(), MigratableError> { + self.announce.invalidate(); self.vu_common.pause()?; self.vu_common.virtio_common.pause() } fn resume(&mut self) -> result::Result<(), MigratableError> { self.vu_common.virtio_common.resume()?; - self.vu_common.resume() + self.vu_common.resume()?; + self.announce.notify( + self.vu_common + .virtio_common + .feature_acked(VIRTIO_NET_F_GUEST_ANNOUNCE.into()), + ); + Ok(()) } } @@ -427,6 +499,7 @@ impl Migratable for Net { } fn start_migration(&mut self) -> result::Result<(), MigratableError> { + self.announce.invalidate(); self.vu_common.start_migration() } @@ -437,19 +510,24 @@ impl Migratable for Net { #[cfg(test)] mod unit_tests { - use std::mem::size_of; + use std::mem::{offset_of, size_of}; use seccompiler::SeccompAction; use virtio_bindings::virtio_net::{VIRTIO_NET_F_STATUS, VIRTIO_NET_S_LINK_UP}; use vmm_sys_util::eventfd::EventFd; use super::*; + use crate::VirtioInterrupt; - fn test_net(acked_features: u64) -> Net { - Net { + fn test_net( + acked_features: u64, + interrupt_cb: Option>, + ) -> Result { + Ok(Net { vu_common: VhostUserCommon { virtio_common: VirtioCommon { acked_features, + interrupt_cb, ..Default::default() }, ..Default::default() @@ -459,10 +537,11 @@ mod unit_tests { seccomp_action: SeccompAction::Allow, exit_evt: EventFd::new(libc::EFD_NONBLOCK).unwrap(), access_platform_enabled: false, - } + announce: AnnouncementState::new(false).map_err(Error::CreateEventFd)?, + }) } - const STATUS_OFFSET: usize = std::mem::offset_of!(VirtioNetConfig, status); + const STATUS_OFFSET: usize = offset_of!(VirtioNetConfig, status); fn read_status(device: &Net) -> u16 { let mut data = vec![0; size_of::()]; device.read_config(0, &mut data); @@ -478,7 +557,7 @@ mod unit_tests { fn test_status_feature_reports_link_up() { // The current implementation should always report "link up" if // VIRTIO_NET_F_STATUS has been negotiated. - let net = test_net(1 << VIRTIO_NET_F_STATUS); + let net = test_net(1 << VIRTIO_NET_F_STATUS, None).unwrap(); assert_eq!(read_status(&net), VIRTIO_NET_S_LINK_UP as u16); }