Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion vm/devices/net/gdma/src/bnic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,11 @@ impl BasicNic {
.get_queues(
vec![QueueConfig {
driver: Box::new(state.queues.driver.clone()),
enabled_rx_offloads: Default::default(),
// The guest reads the tag from the completion.
enabled_rx_offloads: net_backend::RxOffloadSupport {
vlan_strip: true,
..Default::default()
},
}],
None,
&mut queues,
Expand Down
3 changes: 3 additions & 0 deletions vm/devices/net/net_backend_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ pub struct RxOffloadSupport {
pub tcpv4_gso: bool,
/// The guest can receive TCPv6 GSO packets.
pub tcpv6_gso: bool,
/// The backend strips a received 802.1Q tag from the frame and reports it in
/// [`RxMetadata::vlan`].
pub vlan_strip: bool,
}

/// The "L4" protocol: the TCP/UDP layer.
Expand Down
2 changes: 2 additions & 0 deletions vm/devices/net/net_consomme/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ async fn receive_gso_metadata(driver: DefaultDriver) {
checksum: true,
tcpv4_gso: !ipv6,
tcpv6_gso: ipv6,
vlan_strip: false,
},
};
client.recv_segments(
Expand Down Expand Up @@ -179,6 +180,7 @@ async fn receive_gso_requires_negotiated_checksum_and_ip_version(driver: Default
checksum,
tcpv4_gso,
tcpv6_gso,
vlan_strip: false,
},
};
assert_eq!(client.rx_checksum_supported(), checksum);
Expand Down
94 changes: 94 additions & 0 deletions vm/devices/net/net_mana/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ use crate::GuestDmaMode;
use crate::ManaEndpoint;
use crate::ManaTestConfiguration;
use crate::QueueStats;
use async_trait::async_trait;
use chipset_device::mmio::ExternallyManagedMmioIntercepts;
use gdma::VportConfig;
use gdma_defs::bnic::ManaQueryDeviceCfgResp;
use inspect::InspectMut;
use inspect_counters::Counter;
use mana_driver::mana::ManaDevice;
use mesh::CancelContext;
Expand All @@ -26,6 +28,9 @@ use pal_async::DefaultDriver;
use pal_async::async_test;
use pci_core::msi::MsiConnection;
use std::future::poll_fn;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::time::Duration;
use test_with_tracing::test;
use user_driver_emulated_mock::DeviceTestMemory;
Expand Down Expand Up @@ -544,6 +549,95 @@ async fn test_multi_mixed_packet(driver: DefaultDriver) {
.await;
}

/// A loopback endpoint that records whether it was asked for the VLAN tag out of band.
struct VlanRecordingEndpoint {
inner: LoopbackEndpoint,
vlan: Arc<AtomicBool>,
}

impl InspectMut for VlanRecordingEndpoint {
fn inspect_mut(&mut self, req: inspect::Request<'_>) {
req.ignore();
}
}

#[async_trait]
impl Endpoint for VlanRecordingEndpoint {
fn endpoint_type(&self) -> &'static str {
"vlan-recording"
}

async fn get_queues(
&mut self,
config: Vec<QueueConfig>,
rss: Option<&net_backend::RssConfig<'_>>,
queues: &mut Vec<Box<dyn net_backend::Queue>>,
) -> anyhow::Result<()> {
self.vlan
.store(config[0].enabled_rx_offloads.vlan_strip, Ordering::Relaxed);
self.inner.get_queues(config, rss, queues).await
}

async fn stop(&mut self) {
self.inner.stop().await
}

fn is_ordered(&self) -> bool {
self.inner.is_ordered()
}
}

/// The guest reads the tag from the completion, so the backend has to take it out of the frame.
#[async_test]
async fn test_backend_is_asked_for_the_vlan_tag_out_of_band(driver: DefaultDriver) {
let pages = 256; // 1MB
let mem = DeviceTestMemory::new(pages * 2, true, "test_vlan_out_of_band");
let vlan = Arc::new(AtomicBool::new(false));
let msi_conn = MsiConnection::new();
let device = gdma::GdmaDevice::new(
&VmTaskDriverSource::new(SingleDriverBackend::new(driver.clone())),
mem.guest_memory(),
&msi_conn.target(),
vec![VportConfig {
mac_address: [1, 2, 3, 4, 5, 6].into(),
endpoint: Box::new(VlanRecordingEndpoint {
inner: LoopbackEndpoint::new(),
vlan: vlan.clone(),
}),
}],
&mut ExternallyManagedMmioIntercepts,
);
let device = EmulatedDevice::new(device, msi_conn, mem.dma_client());
let dev_config = ManaQueryDeviceCfgResp {
pf_cap_flags1: 0.into(),
pf_cap_flags2: 0,
pf_cap_flags3: 0,
pf_cap_flags4: 0,
max_num_vports: 1,
reserved: 0,
max_num_eqs: 64,
adapter_mtu: 0,
reserved2: 0,
adapter_link_speed_mbps: 0,
};
let thing = ManaDevice::new(&driver, device, 1, 1, None).await.unwrap();
let vport = thing.new_vport(0, None, &dev_config).await.unwrap();
let mut endpoint = ManaEndpoint::new(driver.clone(), vport, GuestDmaMode::DirectDma).await;
let mut queues = Vec::new();
endpoint
.get_queues(
vec![QueueConfig {
driver: Box::new(driver.clone()),
enabled_rx_offloads: Default::default(),
}],
None,
&mut queues,
)
.await
.unwrap();
assert!(vlan.load(Ordering::Relaxed));
}

#[async_test]
async fn test_vport_with_query_filter_state(driver: DefaultDriver) {
let pages = 512; // 2MB
Expand Down
57 changes: 51 additions & 6 deletions vm/devices/net/net_tap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use net_backend::TxId;
use net_backend::TxMetadata;
use net_backend::TxOffloadSupport;
use net_backend::TxSegment;
use net_backend::VlanMetadata;
use pal_async::driver::Driver;
use parking_lot::Mutex;
use std::collections::VecDeque;
Expand All @@ -47,6 +48,10 @@ const MAX_TAP_FRAME_LEN: usize =
// read buffer to that contract rather than adding overflow detection.
const TAP_READ_BUFFER_LEN: usize = size_of::<VirtioNetHdr>() + MAX_TAP_FRAME_LEN;
const MAX_RX_PACKETS_PER_POLL: usize = 64;
/// The destination and source MAC addresses, which an 802.1Q tag follows.
const ETHERNET_ADDRESSES_LEN: usize = 12;
const ETH_P_8021Q: u16 = 0x8100;
const VLAN_TAG_LEN: u16 = 4;

// TODO: These virtio net header types duplicate definitions in virtio_net.
// Consider extracting a shared `virtio_net_header` crate if more consumers
Expand Down Expand Up @@ -304,28 +309,42 @@ impl Queue for TapQueue {
Poll::Ready(Ok(read_len)) => {
self.read_since_pending = true;
packets_processed += 1;
let (hdr, frame) = VirtioNetHdr::read_from_prefix(&self.buffer[..read_len])
let (mut hdr, frame) = VirtioNetHdr::read_from_prefix(&self.buffer[..read_len])
.expect("TAP packet is missing the vnet header");
assert!(
!frame.is_empty(),
"TAP packet is missing the Ethernet frame"
);
let frame_len = frame.len();
assert_vnet_hdr_contract(&hdr, frame_len);
let Some(rx_meta) = parse_vnet_hdr_for_queue(
assert_vnet_hdr_contract(&hdr, frame.len());
let strip_tag = self.enabled_rx_offloads.vlan_strip && has_vlan_tag(frame);
if strip_tag && hdr.flags.needs_csum() {
// The kernel's offset counts the tag.
hdr.csum_start = hdr.csum_start.saturating_sub(VLAN_TAG_LEN);
}
let Some(mut rx_meta) = parse_vnet_hdr_for_queue(
&hdr,
self.enabled_rx_offloads,
&self.stale_offload_recovery,
) else {
continue;
};
// Stripped here because the capacity check has to judge the frame the
// guest gets: with the tag, a full-size frame does not fit an MTU buffer.
let frame = if strip_tag {
let frame_start = size_of::<VirtioNetHdr>();
rx_meta.vlan =
Some(strip_vlan_tag(&mut self.buffer[frame_start..read_len]));
&self.buffer[frame_start + usize::from(VLAN_TAG_LEN)..read_len]
} else {
frame
};
let capacity = pool.capacity(rx);
if frame_len <= capacity as usize {
if frame.len() <= capacity as usize {
pool.write_packet(
rx,
&RxMetadata {
offset: 0,
len: frame_len,
len: frame.len(),
..rx_meta
},
frame,
Expand Down Expand Up @@ -638,6 +657,27 @@ fn build_vnet_hdr(meta: &TxMetadata) -> VirtioNetHdr {
}
}

/// Whether a frame has an outer 802.1Q tag. An outer 802.1ad tag has no out-of-band form, so it
/// stays in the frame.
fn has_vlan_tag(frame: &[u8]) -> bool {
frame.len() >= ETHERNET_VLAN_HEADER_LEN as usize
&& frame[ETHERNET_ADDRESSES_LEN..ETHERNET_ADDRESSES_LEN + 2] == ETH_P_8021Q.to_be_bytes()
}

/// Takes the tag out of a frame [`has_vlan_tag`] accepted, moving the MAC addresses over it. The
/// frame then starts 4 bytes later.
fn strip_vlan_tag(frame: &mut [u8]) -> VlanMetadata {
let tci = u16::from_be_bytes([
frame[ETHERNET_ADDRESSES_LEN + 2],
frame[ETHERNET_ADDRESSES_LEN + 3],
]);
frame.copy_within(..ETHERNET_ADDRESSES_LEN, usize::from(VLAN_TAG_LEN));
VlanMetadata::new()
.with_priority((tci >> 13) as u8)
.with_drop_eligible_indicator(tci & (1 << 12) != 0)
.with_vlan_id(tci & 0xfff)
}

#[derive(Debug, PartialEq, Eq)]
enum InvalidRxMetadata {
UnsupportedGsoProtocol,
Expand Down Expand Up @@ -841,6 +881,7 @@ mod tests {
checksum: true,
tcpv4_gso: true,
tcpv6_gso: true,
vlan_strip: false,
}
}

Expand Down Expand Up @@ -1076,6 +1117,7 @@ mod tests {
checksum: true,
tcpv4_gso: false,
tcpv6_gso: true,
vlan_strip: false,
},
);
assert!(recovery.permits(&InvalidRxMetadata::UnsupportedGsoOffload(
Expand Down Expand Up @@ -1103,6 +1145,7 @@ mod tests {
checksum: true,
tcpv4_gso: false,
tcpv6_gso: true,
vlan_strip: false,
},
&recovery,
)
Expand All @@ -1121,6 +1164,7 @@ mod tests {
checksum: true,
tcpv4_gso: false,
tcpv6_gso: true,
vlan_strip: false,
},
&recovery,
)
Expand All @@ -1136,6 +1180,7 @@ mod tests {
checksum: false,
tcpv4_gso: true,
tcpv6_gso: true,
vlan_strip: false,
},
);
assert!(recovery.permits(&InvalidRxMetadata::UnsupportedChecksumOffload));
Expand Down
Loading
Loading