use std::net::IpAddr;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use super::ffi;
use crate::error::Error;
#[derive(Debug, Clone)]
#[must_use]
pub struct FlowRule {
flow_type: u32,
src_ip: Option<IpAddr>,
dst_ip: Option<IpAddr>,
src_port: Option<u16>,
dst_port: Option<u16>,
ring_cookie: u64,
location: u32,
}
impl FlowRule {
fn new(flow_type: u32) -> Self {
Self {
flow_type,
src_ip: None,
dst_ip: None,
src_port: None,
dst_port: None,
ring_cookie: 0,
location: ffi::RX_CLS_LOC_ANY,
}
}
pub fn tcp() -> Self {
Self::new(ffi::TCP_V4_FLOW)
}
pub fn udp() -> Self {
Self::new(ffi::UDP_V4_FLOW)
}
pub fn sctp() -> Self {
Self::new(ffi::SCTP_V4_FLOW)
}
pub fn tcp6() -> Self {
Self::new(ffi::TCP_V6_FLOW)
}
pub fn udp6() -> Self {
Self::new(ffi::UDP_V6_FLOW)
}
pub fn sctp6() -> Self {
Self::new(ffi::SCTP_V6_FLOW)
}
pub fn src_ip(mut self, ip: impl Into<IpAddr>) -> Self {
self.src_ip = Some(ip.into());
self
}
pub fn dst_ip(mut self, ip: impl Into<IpAddr>) -> Self {
self.dst_ip = Some(ip.into());
self
}
pub fn src_port(mut self, port: u16) -> Self {
self.src_port = Some(port);
self
}
pub fn dst_port(mut self, port: u16) -> Self {
self.dst_port = Some(port);
self
}
pub fn to_queue(mut self, queue: u32) -> Self {
self.ring_cookie = u64::from(queue);
self
}
pub fn discard(mut self) -> Self {
self.ring_cookie = ffi::RX_CLS_FLOW_DISC;
self
}
pub fn location(mut self, location: u32) -> Self {
self.location = location;
self
}
fn pack(&self) -> Result<ffi::ethtool_rx_flow_spec, Error> {
let v6 = matches!(
self.flow_type,
ffi::TCP_V6_FLOW | ffi::UDP_V6_FLOW | ffi::SCTP_V6_FLOW
);
let (dst_off, sport_off, dport_off) = if v6 { (16, 32, 34) } else { (4, 8, 10) };
let mut h_u = [0u8; 52];
let mut m_u = [0u8; 52];
let mut put = |off: usize, bytes: &[u8]| {
h_u[off..off + bytes.len()].copy_from_slice(bytes);
m_u[off..off + bytes.len()].fill(0xff);
};
if let Some(ip) = self.src_ip {
put(0, &addr_octets(ip, v6)?);
}
if let Some(ip) = self.dst_ip {
put(dst_off, &addr_octets(ip, v6)?);
}
if let Some(p) = self.src_port {
put(sport_off, &p.to_be_bytes());
}
if let Some(p) = self.dst_port {
put(dport_off, &p.to_be_bytes());
}
Ok(ffi::ethtool_rx_flow_spec {
flow_type: self.flow_type,
h_u,
h_ext: [0; 20],
m_u,
m_ext: [0; 20],
ring_cookie: self.ring_cookie,
location: self.location,
})
}
}
fn addr_octets(ip: IpAddr, want_v6: bool) -> Result<Vec<u8>, Error> {
match (ip, want_v6) {
(IpAddr::V4(a), false) => Ok(a.octets().to_vec()),
(IpAddr::V6(a), true) => Ok(a.octets().to_vec()),
_ => Err(Error::Config(format!(
"address {ip} family does not match the rule's flow type"
))),
}
}
#[derive(Debug)]
pub struct RxSteer {
iface: String,
fd: OwnedFd,
}
impl RxSteer {
pub fn open(iface: &str) -> Result<Self, Error> {
if iface.len() >= libc::IFNAMSIZ {
return Err(Error::Config(format!("interface name too long: {iface}")));
}
Ok(Self {
iface: iface.to_string(),
fd: ethtool_socket()?,
})
}
pub fn insert(&self, rule: &FlowRule) -> Result<u32, Error> {
let mut nfc = ffi::ethtool_rxnfc {
cmd: ffi::ETHTOOL_SRXCLSRLINS,
flow_type: 0,
data: 0,
fs: rule.pack()?,
rule_cnt: 0,
};
self.ioctl(&mut nfc)?;
Ok(nfc.fs.location)
}
pub fn remove(&self, location: u32) -> Result<(), Error> {
let mut nfc = ffi::ethtool_rxnfc {
cmd: ffi::ETHTOOL_SRXCLSRLDEL,
flow_type: 0,
data: 0,
fs: zeroed_spec(location),
rule_cnt: 0,
};
self.ioctl(&mut nfc)
}
pub fn rule_count(&self) -> Result<u32, Error> {
let mut nfc = ffi::ethtool_rxnfc {
cmd: ffi::ETHTOOL_GRXCLSRLCNT,
flow_type: 0,
data: 0,
fs: zeroed_spec(0),
rule_cnt: 0,
};
self.ioctl(&mut nfc)?;
Ok(nfc.rule_cnt)
}
pub fn guarded(self, rules: impl IntoIterator<Item = FlowRule>) -> Result<SteerGuard, Error> {
let mut locations = Vec::new();
for rule in rules {
match self.insert(&rule) {
Ok(loc) => locations.push(loc),
Err(e) => {
for &loc in &locations {
let _ = self.remove(loc);
}
return Err(e);
}
}
}
Ok(SteerGuard {
steer: self,
locations,
})
}
fn ioctl(&self, nfc: &mut ffi::ethtool_rxnfc) -> Result<(), Error> {
ethtool_ioctl(
&self.fd,
&self.iface,
(nfc as *mut ffi::ethtool_rxnfc).cast(),
)
}
}
#[derive(Debug)]
pub struct SteerGuard {
steer: RxSteer,
locations: Vec<u32>,
}
impl SteerGuard {
pub fn locations(&self) -> &[u32] {
&self.locations
}
}
impl Drop for SteerGuard {
fn drop(&mut self) {
for &loc in &self.locations {
if let Err(e) = self.steer.remove(loc) {
tracing::warn!(location = loc, error = %e, "RxSteer: failed to remove rule on drop");
}
}
}
}
fn zeroed_spec(location: u32) -> ffi::ethtool_rx_flow_spec {
ffi::ethtool_rx_flow_spec {
flow_type: 0,
h_u: [0; 52],
h_ext: [0; 20],
m_u: [0; 52],
m_ext: [0; 20],
ring_cookie: 0,
location,
}
}
fn ethtool_socket() -> Result<OwnedFd, Error> {
let raw = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if raw < 0 {
return Err(Error::Socket(std::io::Error::last_os_error()));
}
Ok(unsafe { OwnedFd::from_raw_fd(raw) })
}
fn ethtool_ioctl(fd: &OwnedFd, iface: &str, cmd: *mut libc::c_void) -> Result<(), Error> {
let mut ifr = ffi::ethtool_ifreq {
ifr_name: [0; libc::IFNAMSIZ],
ifr_data: cmd,
};
for (dst, &b) in ifr.ifr_name.iter_mut().zip(iface.as_bytes()) {
*dst = b as libc::c_char;
}
let rc = unsafe { libc::ioctl(fd.as_raw_fd(), ffi::SIOCETHTOOL, &mut ifr as *mut _) };
if rc != 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
fn be16(v: u16) -> [u8; 2] {
v.to_be_bytes()
}
#[test]
fn packs_ipv4_5tuple_with_match_mask() {
let spec = FlowRule::tcp()
.src_ip(Ipv4Addr::new(10, 0, 0, 1))
.dst_ip(Ipv4Addr::new(192, 168, 1, 2))
.src_port(1234)
.dst_port(443)
.to_queue(3)
.pack()
.unwrap();
assert_eq!(spec.flow_type, ffi::TCP_V4_FLOW);
assert_eq!(&spec.h_u[0..4], &[10, 0, 0, 1]); assert_eq!(&spec.h_u[4..8], &[192, 168, 1, 2]); assert_eq!(&spec.h_u[8..10], &be16(1234)); assert_eq!(&spec.h_u[10..12], &be16(443)); assert_eq!(spec.ring_cookie, 3);
assert_eq!(spec.location, ffi::RX_CLS_LOC_ANY);
assert_eq!(&spec.m_u[0..12], &[0xffu8; 12]);
assert!(spec.m_u[12..].iter().all(|&b| b == 0));
}
#[test]
fn unset_fields_are_fully_wildcarded() {
let spec = FlowRule::udp().dst_port(53).pack().unwrap();
assert_eq!(spec.flow_type, ffi::UDP_V4_FLOW);
assert_eq!(&spec.h_u[10..12], &be16(53));
assert_eq!(&spec.m_u[10..12], &[0xffu8, 0xff]);
assert!(spec.m_u[0..10].iter().all(|&b| b == 0));
assert!(spec.m_u[12..].iter().all(|&b| b == 0));
}
#[test]
fn packs_ipv6_at_tcpip6_offsets() {
let spec = FlowRule::tcp6()
.dst_ip(Ipv6Addr::LOCALHOST)
.dst_port(443)
.pack()
.unwrap();
assert_eq!(spec.flow_type, ffi::TCP_V6_FLOW);
assert_eq!(&spec.h_u[16..32], &Ipv6Addr::LOCALHOST.octets()); assert_eq!(&spec.h_u[34..36], &be16(443)); assert_eq!(&spec.m_u[16..32], &[0xffu8; 16]);
assert_eq!(&spec.m_u[34..36], &[0xffu8, 0xff]);
}
#[test]
fn discard_sets_disc_cookie() {
let spec = FlowRule::tcp().dst_port(23).discard().pack().unwrap();
assert_eq!(spec.ring_cookie, ffi::RX_CLS_FLOW_DISC);
}
#[test]
fn family_mismatch_is_rejected() {
assert!(FlowRule::tcp().dst_ip(Ipv6Addr::LOCALHOST).pack().is_err());
assert!(FlowRule::tcp6().dst_ip(Ipv4Addr::LOCALHOST).pack().is_err());
}
#[test]
fn lo_insert_degrades_cleanly() {
if let Ok(steer) = RxSteer::open("lo") {
assert!(
steer
.insert(&FlowRule::tcp().dst_port(443).to_queue(0))
.is_err()
);
}
}
}