use std::{
ffi::{CStr, CString},
io,
os::fd::AsFd as _,
path::Path,
};
use thiserror::Error;
use super::{FdLink, ProgramInfo};
use crate::{
generated::{
bpf_attach_type::{self, BPF_TCX_EGRESS, BPF_TCX_INGRESS},
bpf_link_type,
bpf_prog_type::BPF_PROG_TYPE_SCHED_CLS,
TC_H_CLSACT, TC_H_MIN_EGRESS, TC_H_MIN_INGRESS,
},
programs::{
define_link_wrapper, load_program, query, Link, LinkError, LinkOrder, ProgramData,
ProgramError,
},
sys::{
bpf_link_create, bpf_link_get_info_by_fd, bpf_link_update, bpf_prog_get_fd_by_id,
netlink_find_filter_with_name, netlink_qdisc_add_clsact, netlink_qdisc_attach,
netlink_qdisc_detach, LinkTarget, ProgQueryTarget, SyscallError,
},
util::{ifindex_from_ifname, tc_handler_make, KernelVersion},
VerifierLogLevel,
};
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
pub enum TcAttachType {
Ingress,
Egress,
Custom(u32),
}
#[derive(Debug)]
#[doc(alias = "BPF_PROG_TYPE_SCHED_CLS")]
pub struct SchedClassifier {
pub(crate) data: ProgramData<SchedClassifierLink>,
}
#[derive(Debug, Error)]
pub enum TcError {
#[error("netlink error while attaching ebpf program to tc")]
NetlinkError {
#[source]
io_error: io::Error,
},
#[error("the clsact qdisc is already attached")]
AlreadyAttached,
#[error("tcx links can only be attached to ingress or egress, custom attachment: {0} is not supported")]
InvalidTcxAttach(u32),
#[error("operation not supported for programs loaded via tcx")]
InvalidLinkOperation,
}
impl TcAttachType {
pub(crate) fn tc_parent(&self) -> u32 {
match self {
Self::Custom(parent) => *parent,
Self::Ingress => tc_handler_make(TC_H_CLSACT, TC_H_MIN_INGRESS),
Self::Egress => tc_handler_make(TC_H_CLSACT, TC_H_MIN_EGRESS),
}
}
pub(crate) fn tcx_attach_type(&self) -> Result<bpf_attach_type, TcError> {
match self {
Self::Ingress => Ok(BPF_TCX_INGRESS),
Self::Egress => Ok(BPF_TCX_EGRESS),
Self::Custom(tcx_attach_type) => Err(TcError::InvalidTcxAttach(*tcx_attach_type)),
}
}
}
#[derive(Debug)]
pub enum TcAttachOptions {
Netlink(NlOptions),
TcxOrder(LinkOrder),
}
#[derive(Debug, Default, Hash, Eq, PartialEq)]
pub struct NlOptions {
pub priority: u16,
pub handle: u32,
}
impl SchedClassifier {
pub fn load(&mut self) -> Result<(), ProgramError> {
load_program(BPF_PROG_TYPE_SCHED_CLS, &mut self.data)
}
pub fn attach(
&mut self,
interface: &str,
attach_type: TcAttachType,
) -> Result<SchedClassifierLinkId, ProgramError> {
if !matches!(attach_type, TcAttachType::Custom(_))
&& KernelVersion::current().unwrap() >= KernelVersion::new(6, 6, 0)
{
self.attach_with_options(
interface,
attach_type,
TcAttachOptions::TcxOrder(LinkOrder::default()),
)
} else {
self.attach_with_options(
interface,
attach_type,
TcAttachOptions::Netlink(NlOptions::default()),
)
}
}
pub fn attach_with_options(
&mut self,
interface: &str,
attach_type: TcAttachType,
options: TcAttachOptions,
) -> Result<SchedClassifierLinkId, ProgramError> {
let if_index = ifindex_from_ifname(interface)
.map_err(|io_error| TcError::NetlinkError { io_error })?;
self.do_attach(if_index, attach_type, options, true)
}
pub fn attach_to_link(
&mut self,
link: SchedClassifierLink,
) -> Result<SchedClassifierLinkId, ProgramError> {
let prog_fd = self.fd()?;
let prog_fd = prog_fd.as_fd();
match link.into_inner() {
TcLinkInner::FdLink(link) => {
let fd = link.fd;
let link_fd = fd.as_fd();
bpf_link_update(link_fd.as_fd(), prog_fd, None, 0).map_err(|(_, io_error)| {
SyscallError {
call: "bpf_link_update",
io_error,
}
})?;
self.data
.links
.insert(SchedClassifierLink::new(TcLinkInner::FdLink(FdLink::new(
fd,
))))
}
TcLinkInner::NlLink(NlLink {
if_index,
attach_type,
priority,
handle,
}) => self.do_attach(
if_index,
attach_type,
TcAttachOptions::Netlink(NlOptions { priority, handle }),
false,
),
}
}
fn do_attach(
&mut self,
if_index: u32,
attach_type: TcAttachType,
options: TcAttachOptions,
create: bool,
) -> Result<SchedClassifierLinkId, ProgramError> {
let prog_fd = self.fd()?;
let prog_fd = prog_fd.as_fd();
match options {
TcAttachOptions::Netlink(options) => {
let name = self.data.name.as_deref().unwrap_or_default();
let name = CString::new(name).unwrap();
let (priority, handle) = unsafe {
netlink_qdisc_attach(
if_index as i32,
&attach_type,
prog_fd,
&name,
options.priority,
options.handle,
create,
)
}
.map_err(|io_error| TcError::NetlinkError { io_error })?;
self.data
.links
.insert(SchedClassifierLink::new(TcLinkInner::NlLink(NlLink {
if_index,
attach_type,
priority,
handle,
})))
}
TcAttachOptions::TcxOrder(options) => {
let link_fd = bpf_link_create(
prog_fd,
LinkTarget::IfIndex(if_index),
attach_type.tcx_attach_type()?,
None,
options.flags.bits(),
Some(&options.link_ref),
)
.map_err(|(_, io_error)| SyscallError {
call: "bpf_mprog_attach",
io_error,
})?;
self.data
.links
.insert(SchedClassifierLink::new(TcLinkInner::FdLink(FdLink::new(
link_fd,
))))
}
}
}
pub fn detach(&mut self, link_id: SchedClassifierLinkId) -> Result<(), ProgramError> {
self.data.links.remove(link_id)
}
pub fn take_link(
&mut self,
link_id: SchedClassifierLinkId,
) -> Result<SchedClassifierLink, ProgramError> {
self.data.take_link(link_id)
}
pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
Ok(Self { data })
}
pub fn query_tcx(
interface: &str,
attach_type: TcAttachType,
) -> Result<(u64, Vec<ProgramInfo>), ProgramError> {
let if_index = ifindex_from_ifname(interface)
.map_err(|io_error| TcError::NetlinkError { io_error })?;
let (revision, prog_ids) = query(
ProgQueryTarget::IfIndex(if_index),
attach_type.tcx_attach_type()?,
0,
&mut None,
)?;
let prog_infos = prog_ids
.into_iter()
.map(|prog_id| {
let prog_fd = bpf_prog_get_fd_by_id(prog_id)?;
let prog_info = ProgramInfo::new_from_fd(prog_fd.as_fd())?;
Ok::<ProgramInfo, ProgramError>(prog_info)
})
.collect::<Result<_, _>>()?;
Ok((revision, prog_infos))
}
}
#[derive(Debug, Hash, Eq, PartialEq)]
pub(crate) struct NlLinkId(u32, TcAttachType, u16, u32);
#[derive(Debug)]
pub(crate) struct NlLink {
if_index: u32,
attach_type: TcAttachType,
priority: u16,
handle: u32,
}
impl Link for NlLink {
type Id = NlLinkId;
fn id(&self) -> Self::Id {
NlLinkId(self.if_index, self.attach_type, self.priority, self.handle)
}
fn detach(self) -> Result<(), ProgramError> {
unsafe {
netlink_qdisc_detach(
self.if_index as i32,
&self.attach_type,
self.priority,
self.handle,
)
}
.map_err(|io_error| TcError::NetlinkError { io_error })?;
Ok(())
}
}
#[derive(Debug, Hash, Eq, PartialEq)]
pub(crate) enum TcLinkIdInner {
FdLinkId(<FdLink as Link>::Id),
NlLinkId(<NlLink as Link>::Id),
}
#[derive(Debug)]
pub(crate) enum TcLinkInner {
FdLink(FdLink),
NlLink(NlLink),
}
impl Link for TcLinkInner {
type Id = TcLinkIdInner;
fn id(&self) -> Self::Id {
match self {
Self::FdLink(link) => TcLinkIdInner::FdLinkId(link.id()),
Self::NlLink(link) => TcLinkIdInner::NlLinkId(link.id()),
}
}
fn detach(self) -> Result<(), ProgramError> {
match self {
Self::FdLink(link) => link.detach(),
Self::NlLink(link) => link.detach(),
}
}
}
impl<'a> TryFrom<&'a SchedClassifierLink> for &'a FdLink {
type Error = LinkError;
fn try_from(value: &'a SchedClassifierLink) -> Result<Self, Self::Error> {
if let TcLinkInner::FdLink(fd) = value.inner() {
Ok(fd)
} else {
Err(LinkError::InvalidLink)
}
}
}
impl TryFrom<SchedClassifierLink> for FdLink {
type Error = LinkError;
fn try_from(value: SchedClassifierLink) -> Result<Self, Self::Error> {
if let TcLinkInner::FdLink(fd) = value.into_inner() {
Ok(fd)
} else {
Err(LinkError::InvalidLink)
}
}
}
impl TryFrom<FdLink> for SchedClassifierLink {
type Error = LinkError;
fn try_from(fd_link: FdLink) -> Result<Self, Self::Error> {
let info = bpf_link_get_info_by_fd(fd_link.fd.as_fd())?;
if info.type_ == (bpf_link_type::BPF_LINK_TYPE_TCX as u32) {
return Ok(Self::new(TcLinkInner::FdLink(fd_link)));
}
Err(LinkError::InvalidLink)
}
}
define_link_wrapper!(
SchedClassifierLink,
SchedClassifierLinkId,
TcLinkInner,
TcLinkIdInner
);
impl SchedClassifierLink {
pub fn attached(
if_name: &str,
attach_type: TcAttachType,
priority: u16,
handle: u32,
) -> Result<Self, io::Error> {
let if_index = ifindex_from_ifname(if_name)?;
Ok(Self(Some(TcLinkInner::NlLink(NlLink {
if_index,
attach_type,
priority,
handle,
}))))
}
pub fn attach_type(&self) -> Result<TcAttachType, ProgramError> {
if let TcLinkInner::NlLink(n) = self.inner() {
Ok(n.attach_type)
} else {
Err(TcError::InvalidLinkOperation.into())
}
}
pub fn priority(&self) -> Result<u16, ProgramError> {
if let TcLinkInner::NlLink(n) = self.inner() {
Ok(n.priority)
} else {
Err(TcError::InvalidLinkOperation.into())
}
}
pub fn handle(&self) -> Result<u32, ProgramError> {
if let TcLinkInner::NlLink(n) = self.inner() {
Ok(n.handle)
} else {
Err(TcError::InvalidLinkOperation.into())
}
}
}
pub fn qdisc_add_clsact(if_name: &str) -> Result<(), io::Error> {
let if_index = ifindex_from_ifname(if_name)?;
unsafe { netlink_qdisc_add_clsact(if_index as i32) }
}
pub fn qdisc_detach_program(
if_name: &str,
attach_type: TcAttachType,
name: &str,
) -> Result<(), io::Error> {
let cstr = CString::new(name)?;
qdisc_detach_program_fast(if_name, attach_type, &cstr)
}
fn qdisc_detach_program_fast(
if_name: &str,
attach_type: TcAttachType,
name: &CStr,
) -> Result<(), io::Error> {
let if_index = ifindex_from_ifname(if_name)? as i32;
let filter_info = unsafe { netlink_find_filter_with_name(if_index, attach_type, name)? };
if filter_info.is_empty() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
name.to_string_lossy(),
));
}
for (prio, handle) in filter_info {
unsafe { netlink_qdisc_detach(if_index, &attach_type, prio, handle)? };
}
Ok(())
}