virtio-devices: net: add guest-announce plumbing

Advertise `VIRTIO_NET_F_GUEST_ANNOUNCE` on virtio-net devices, surface
`VIRTIO_NET_S_ANNOUNCE` through config status, and handle
`VIRTIO_NET_CTRL_ANNOUNCE_ACK` on the control queue.

This adds the guest-visible state needed for post-migration or
post-restore announce requests; the VMM side triggering is added in
follow-up commits.

The motivation is to reduce post-migration and post-restore
connectivity gap. After a live migration or after restoring, it can
take the guest several seconds to be reachable again over the network.
With these announcements, the network path should be refreshed within a
few milliseconds.

On-behalf-of: SAP sebastian.eydam@sap.com
Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
This commit is contained in:
Sebastian Eydam
2026-03-31 13:21:59 +02:00
committed by Rob Bradford
parent e17c0be127
commit 53a00c0514
9 changed files with 967 additions and 125 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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<Tap>,
pub announce_pending: Arc<AtomicBool>,
}
impl CtrlQueue {
pub fn new(taps: Vec<Tap>) -> Self {
CtrlQueue { taps }
pub fn new(taps: Vec<Tap>, announce_pending: Arc<AtomicBool>) -> 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::<u16>(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::<u64>(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::<ControlHeader>() 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::<u8>(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::<ControlHeader>() 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)
));
}
}

View File

@@ -101,7 +101,7 @@ fn create_unix_socket() -> Result<net::UdpSocket> {
Ok(unsafe { net::UdpSocket::from_raw_fd(sock) })
}
fn vnet_hdr_len() -> usize {
pub fn vnet_hdr_len() -> usize {
size_of::<virtio_net_hdr_v1>()
}

View File

@@ -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>;

View File

@@ -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<Arc<dyn AccessPlatform>>,
pub interrupt_cb: Arc<dyn VirtioInterrupt>,
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<T> = result::Result<T, Error>;
@@ -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<AtomicBool>,
/// Generation counter used to invalidate active announcers before a
/// reset or device teardown, so they stop sending notifications.
pub(crate) generation: Arc<AtomicU64>,
/// When signaled, the epoll thread will do the announcements.
pub(crate) evt: EventFd,
}
impl AnnouncementState {
pub fn new(pending: bool) -> io::Result<Self> {
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<RateLimiterConfig>,
exit_evt: EventFd,
device_status: Arc<AtomicU8>,
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(&ETH_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<dyn AnnounceOps>,
Box::new(host_announce_ops) as Box<dyn AnnounceOps>,
]
.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<AtomicU64>,
generation: u64,
announcements_done: usize,
announce_ops: Box<[Box<dyn AnnounceOps>]>,
}
impl Announcer {
const MAX_ANNOUNCEMENTS: usize = 5;
pub fn new(announce: &AnnouncementState, announce_ops: Box<[Box<dyn AnnounceOps>]>) -> 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::<Vec<AnnounceOutcome>>();
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<dyn VirtioInterrupt>,
guest_announce_negotiated: bool,
announce_pending: Arc<AtomicBool>,
}
impl VirtioNetGuestAnnounceOps {
pub fn new(
interrupt_cb: Arc<dyn VirtioInterrupt>,
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<Arc<dyn VirtioInterrupt>>,
) -> Result<Net> {
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::<VirtioNetConfig>()];
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<EventFd>,
_vm: &dyn hypervisor::Vm,
) -> io::Result<()> {
unimplemented!()
}
}
fn test_announcer(dev: &Net) -> Result<Announcer> {
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<dyn AnnounceOps>,
Box::new(host_announce_ops) as Box<dyn AnnounceOps>,
]
.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<dyn VirtioInterrupt>),
)
.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<dyn VirtioInterrupt>),
)
.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<dyn VirtioInterrupt>)).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<dyn VirtioInterrupt>),
)
.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<F>(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<dyn VirtioInterrupt>),
)
.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<AtomicUsize>,
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<AtomicUsize>,
) -> Result<Announcer> {
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<dyn AnnounceOps>,
Box::new(second_ops) as Box<dyn AnnounceOps>,
]
.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);
}
}

View File

@@ -186,7 +186,10 @@ fn create_virtio_net_ctl_ioctl_seccomp_rule() -> Vec<SeccompRule> {
}
fn virtio_net_ctl_thread_rules() -> Vec<(i64, Vec<SeccompRule>)> {
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<SeccompRule>)> {
@@ -244,7 +247,7 @@ fn virtio_generic_vhost_user_thread_rules() -> Vec<(i64, Vec<SeccompRule>)> {
}
fn virtio_vhost_net_ctl_thread_rules() -> Vec<(i64, Vec<SeccompRule>)> {
vec![]
vec![(libc::SYS_timerfd_settime, vec![])]
}
fn virtio_vhost_net_thread_rules() -> Vec<(i64, Vec<SeccompRule>)> {

View File

@@ -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")]

View File

@@ -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<dyn AnnounceOps>].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<FrontendReqHandler<BackendReqHandler>> = 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<Arc<dyn VirtioInterrupt>>,
) -> Result<Net> {
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::<VirtioNetConfig>()];
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);
}