use crate::config::FanoutMode;
pub const AUTO_FANOUT_GROUP: u16 = 0xA070;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Fanout {
#[default]
None,
Cpu(u16),
Hash(u16),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Backend {
Auto,
AfPacket {
fanout: Fanout,
},
#[cfg(all(feature = "af-xdp", feature = "xdp-loader"))]
AfXdp {
queues: crate::xdp::Queues,
},
#[cfg(all(feature = "pcap", feature = "tokio"))]
Pcap {
path: std::path::PathBuf,
speed_factor: Option<f32>,
},
}
impl Backend {
pub fn af_packet() -> Self {
Self::AfPacket {
fanout: Fanout::None,
}
}
pub fn af_packet_fanout(fanout: Fanout) -> Self {
Self::AfPacket { fanout }
}
#[cfg(all(feature = "af-xdp", feature = "xdp-loader"))]
pub fn af_xdp() -> Self {
Self::AfXdp {
queues: crate::xdp::Queues::Auto,
}
}
#[cfg(all(feature = "pcap", feature = "tokio"))]
pub fn pcap(path: impl Into<std::path::PathBuf>) -> Self {
Self::Pcap {
path: path.into(),
speed_factor: None,
}
}
#[cfg(all(feature = "pcap", feature = "tokio"))]
pub fn pcap_at_speed(path: impl Into<std::path::PathBuf>, speed_factor: f32) -> Self {
Self::Pcap {
path: path.into(),
speed_factor: Some(speed_factor),
}
}
}
#[allow(dead_code)]
pub(crate) trait CapabilityProbe {
fn xdp_loader_compiled(&self) -> bool;
fn queue_count(&self, iface: &str) -> Option<usize>;
fn parallelism(&self) -> usize;
}
pub(crate) struct SystemProbe;
impl CapabilityProbe for SystemProbe {
fn xdp_loader_compiled(&self) -> bool {
cfg!(all(feature = "af-xdp", feature = "xdp-loader"))
}
fn queue_count(&self, _iface: &str) -> Option<usize> {
#[cfg(feature = "af-xdp")]
{
crate::xdp::queue_count(_iface).ok().map(|n| n as usize)
}
#[cfg(not(feature = "af-xdp"))]
{
None
}
}
fn parallelism(&self) -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
}
pub(crate) struct ResolvedBackend {
pub backend: Backend,
pub description: String,
}
pub(crate) fn resolve(
iface: &str,
backend: &Backend,
probe: &dyn CapabilityProbe,
) -> ResolvedBackend {
match backend {
Backend::Auto => resolve_auto(iface, probe),
concrete => ResolvedBackend {
backend: concrete.clone(),
description: describe(concrete),
},
}
}
fn resolve_auto(iface: &str, probe: &dyn CapabilityProbe) -> ResolvedBackend {
let cores = probe.parallelism();
#[cfg(all(feature = "af-xdp", feature = "xdp-loader"))]
if probe.xdp_loader_compiled() {
let queues = crate::xdp::Queues::Auto;
let q = probe.queue_count(iface);
let qdesc = match q {
Some(n) => format!("{n} RX queue(s)"),
None => "all RX queues".to_string(),
};
let advice = if cores > 1 {
format!("; single-reactor — for line rate across {cores} cores use XdpShardedRunner")
} else {
String::new()
};
return ResolvedBackend {
backend: Backend::AfXdp { queues },
description: format!("AF_XDP self-loaded (SKB_MODE, {qdesc}{advice})"),
};
}
let _ = iface;
let advice = if cores > 1 {
format!(" — for multi-core scaling use ShardedRunner (PACKET_FANOUT across {cores} cores)")
} else {
String::new()
};
ResolvedBackend {
backend: Backend::AfPacket {
fanout: Fanout::None,
},
description: format!("AF_PACKET (TPACKET_v3, single ring{advice})"),
}
}
fn describe(backend: &Backend) -> String {
match backend {
Backend::Auto => "Auto (unresolved)".to_string(),
Backend::AfPacket { fanout } => match fanout {
Fanout::None => "AF_PACKET (TPACKET_v3, single ring)".to_string(),
Fanout::Cpu(g) => format!("AF_PACKET (PACKET_FANOUT_CPU, group {g:#06x})"),
Fanout::Hash(g) => format!("AF_PACKET (PACKET_FANOUT_HASH, group {g:#06x})"),
},
#[cfg(all(feature = "af-xdp", feature = "xdp-loader"))]
Backend::AfXdp { queues } => format!("AF_XDP self-loaded (SKB_MODE, queues={queues:?})"),
#[cfg(all(feature = "pcap", feature = "tokio"))]
Backend::Pcap { path, speed_factor } => match speed_factor {
Some(f) => format!("offline pcap replay: {} ({f}x)", path.display()),
None => format!("offline pcap replay: {} (max speed)", path.display()),
},
}
}
pub(crate) fn fanout_to_spec(fanout: Fanout) -> Option<(FanoutMode, u16)> {
match fanout {
Fanout::None => None,
Fanout::Cpu(g) => Some((FanoutMode::Cpu, g)),
Fanout::Hash(g) => Some((FanoutMode::Hash, g)),
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockProbe {
loader: bool,
queues: Option<usize>,
cores: usize,
}
impl CapabilityProbe for MockProbe {
fn xdp_loader_compiled(&self) -> bool {
self.loader
}
fn queue_count(&self, _: &str) -> Option<usize> {
self.queues
}
fn parallelism(&self) -> usize {
self.cores
}
}
#[test]
fn auto_without_xdp_loader_picks_af_packet() {
let probe = MockProbe {
loader: false,
queues: Some(8),
cores: 16,
};
let r = resolve("eth0", &Backend::Auto, &probe);
assert!(matches!(
r.backend,
Backend::AfPacket {
fanout: Fanout::None
}
));
assert!(r.description.contains("AF_PACKET"));
assert!(r.description.contains("ShardedRunner"), "{}", r.description);
}
#[test]
fn explicit_backend_passes_through_with_description() {
let probe = MockProbe {
loader: false,
queues: None,
cores: 1,
};
let r = resolve(
"eth0",
&Backend::af_packet_fanout(Fanout::Cpu(0x1234)),
&probe,
);
assert!(matches!(
r.backend,
Backend::AfPacket {
fanout: Fanout::Cpu(0x1234)
}
));
assert!(r.description.contains("PACKET_FANOUT_CPU"));
}
#[test]
fn fanout_spec_mapping() {
assert_eq!(fanout_to_spec(Fanout::None), None);
assert_eq!(fanout_to_spec(Fanout::Cpu(7)), Some((FanoutMode::Cpu, 7)));
assert_eq!(fanout_to_spec(Fanout::Hash(9)), Some((FanoutMode::Hash, 9)));
}
#[cfg(all(feature = "pcap", feature = "tokio"))]
#[test]
fn pcap_backend_resolves_to_offline_description() {
let probe = MockProbe {
loader: false,
queues: None,
cores: 1,
};
let r = resolve(
"trace",
&Backend::pcap_at_speed("/tmp/cap.pcapng", 4.0),
&probe,
);
assert!(matches!(r.backend, Backend::Pcap { .. }));
assert!(
r.description.contains("offline pcap replay"),
"{}",
r.description
);
assert!(r.description.contains("4x"), "{}", r.description);
let r2 = resolve("trace", &Backend::pcap("/tmp/cap.pcapng"), &probe);
assert!(r2.description.contains("max speed"), "{}", r2.description);
}
#[cfg(all(feature = "af-xdp", feature = "xdp-loader"))]
#[test]
fn auto_with_xdp_loader_picks_af_xdp() {
let probe = MockProbe {
loader: true,
queues: Some(4),
cores: 8,
};
let r = resolve("eth0", &Backend::Auto, &probe);
assert!(matches!(r.backend, Backend::AfXdp { .. }));
assert!(r.description.contains("AF_XDP"));
assert!(r.description.contains("4 RX queue"), "{}", r.description);
assert!(
r.description.contains("XdpShardedRunner"),
"{}",
r.description
);
}
}