use flowscope::{RssHashType, RxHash, RxMetadata, Timestamp, VlanProto, VlanTag};
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RxMetaFlags: u32 {
const TIMESTAMP = 1 << 0;
const HASH = 1 << 1;
const VLAN = 1 << 2;
}
}
pub(crate) const META_MAGIC: u32 = 0x6E_72_6D_01;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub(crate) struct XdpRxMeta {
magic: u32,
flags: u32,
rx_timestamp: u64,
rx_hash: u32,
rx_hash_type: u32,
vlan_tci: u32,
vlan_proto: u32,
}
impl XdpRxMeta {
pub(crate) const LEN: usize = std::mem::size_of::<Self>();
pub(crate) fn from_headroom(headroom: &[u8]) -> Option<Self> {
if headroom.len() < Self::LEN {
return None;
}
let meta = unsafe { (headroom.as_ptr() as *const Self).read_unaligned() };
(meta.magic == META_MAGIC).then_some(meta)
}
pub(crate) fn rx_metadata(&self) -> RxMetadata {
let flags = RxMetaFlags::from_bits_truncate(self.flags);
let mut m = RxMetadata::default();
if flags.contains(RxMetaFlags::TIMESTAMP) {
m.hw_timestamp = Some(timestamp_from_nanos(self.rx_timestamp));
}
if flags.contains(RxMetaFlags::HASH) {
m.rx_hash = Some(RxHash::new(self.rx_hash, rss_hash_type(self.rx_hash_type)));
}
if flags.contains(RxMetaFlags::VLAN) {
m.vlan = Some(VlanTag::new(
self.vlan_tci as u16,
vlan_proto(self.vlan_proto as u16),
));
}
m
}
}
fn timestamp_from_nanos(ns: u64) -> Timestamp {
Timestamp::new((ns / 1_000_000_000) as u32, (ns % 1_000_000_000) as u32)
}
fn rss_hash_type(code: u32) -> RssHashType {
match code {
0 => RssHashType::L2,
1 => RssHashType::L3Ipv4,
2 => RssHashType::L3Ipv6,
3 => RssHashType::L4TcpIpv4,
4 => RssHashType::L4UdpIpv4,
5 => RssHashType::L4SctpIpv4,
6 => RssHashType::L4TcpIpv6,
7 => RssHashType::L4UdpIpv6,
8 => RssHashType::L4SctpIpv6,
_ => RssHashType::Unknown,
}
}
fn vlan_proto(tpid: u16) -> VlanProto {
match tpid {
0x8100 => VlanProto::Dot1Q,
0x88A8 => VlanProto::Dot1Ad,
other => VlanProto::Other(other),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layout_is_32_bytes() {
assert_eq!(XdpRxMeta::LEN, 32);
}
fn encode(meta: &XdpRxMeta) -> [u8; XdpRxMeta::LEN] {
unsafe { std::mem::transmute_copy(meta) }
}
fn valid_all() -> XdpRxMeta {
XdpRxMeta {
magic: META_MAGIC,
flags: (RxMetaFlags::TIMESTAMP | RxMetaFlags::HASH | RxMetaFlags::VLAN).bits(),
rx_timestamp: 1_500_000_000_750_000_000,
rx_hash: 0xDEAD_BEEF,
rx_hash_type: 3, vlan_tci: 0x0064, vlan_proto: 0x8100,
}
}
#[test]
fn parses_all_fields() {
let bytes = encode(&valid_all());
let m = XdpRxMeta::from_headroom(&bytes)
.expect("valid magic")
.rx_metadata();
assert_eq!(
m.hw_timestamp,
Some(Timestamp::new(1_500_000_000, 750_000_000))
);
let h = m.rx_hash.expect("hash flagged");
assert_eq!(h.value, 0xDEAD_BEEF);
assert_eq!(h.ty, RssHashType::L4TcpIpv4);
let v = m.vlan.expect("vlan flagged");
assert_eq!(v.vid(), 100);
assert_eq!(v.proto, VlanProto::Dot1Q);
}
#[test]
fn honours_per_field_flags() {
let meta = XdpRxMeta {
flags: RxMetaFlags::HASH.bits(),
..valid_all()
};
let m = XdpRxMeta::from_headroom(&encode(&meta))
.unwrap()
.rx_metadata();
assert!(m.hw_timestamp.is_none());
assert!(m.vlan.is_none());
assert!(m.rx_hash.is_some());
}
#[test]
fn rejects_bad_magic() {
let mut bytes = encode(&valid_all());
bytes[0] ^= 0xFF;
assert!(XdpRxMeta::from_headroom(&bytes).is_none());
}
#[test]
fn rejects_short_headroom() {
let bytes = encode(&valid_all());
assert!(XdpRxMeta::from_headroom(&bytes[..XdpRxMeta::LEN - 1]).is_none());
}
#[test]
fn all_zero_headroom_is_absent() {
assert!(XdpRxMeta::from_headroom(&[0u8; XdpRxMeta::LEN]).is_none());
}
#[test]
fn unknown_hash_type_degrades() {
let meta = XdpRxMeta {
flags: RxMetaFlags::HASH.bits(),
rx_hash_type: 99,
..valid_all()
};
let m = XdpRxMeta::from_headroom(&encode(&meta))
.unwrap()
.rx_metadata();
assert_eq!(m.rx_hash.unwrap().ty, RssHashType::Unknown);
}
#[test]
fn maps_vlan_protocols() {
assert_eq!(vlan_proto(0x8100), VlanProto::Dot1Q);
assert_eq!(vlan_proto(0x88A8), VlanProto::Dot1Ad);
assert_eq!(vlan_proto(0x9100), VlanProto::Other(0x9100));
}
}