use std::{ffi::CString, io, os::fd::AsFd as _, path::Path};
use aya_obj::generated::{
TC_H_CLSACT, TC_H_MIN_EGRESS, TC_H_MIN_INGRESS,
bpf_attach_type::{self, BPF_TCX_EGRESS, BPF_TCX_INGRESS},
bpf_link_type,
bpf_prog_type::BPF_PROG_TYPE_SCHED_CLS,
};
use thiserror::Error;
use super::{FdLink, ProgramInfo};
use crate::{
VerifierLogLevel,
programs::{
Link, LinkError, LinkOrder, ProgramData, ProgramError, ProgramType, define_link_wrapper,
id_as_key, impl_try_into_fdlink, load_program, query,
},
sys::{
BpfLinkCreateArgs, LinkTarget, NetlinkError, NetlinkSocket, ProgQueryTarget, SyscallError,
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,
},
util::{KernelVersion, ifindex_from_ifname, tc_handler_make},
};
#[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(transparent)]
NetlinkError(#[from] NetlinkError),
#[error(transparent)]
NulError(#[from] std::ffi::NulError),
#[error(transparent)]
IoError(#[from] 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) const 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) const 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 const PROGRAM_TYPE: ProgramType = ProgramType::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::at_least(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(TcError::IoError)?;
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::Fd(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::Fd(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(TcError::NetlinkError)?;
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()?,
options.flags.bits(),
Some(BpfLinkCreateArgs::Tcx(&options.link_ref)),
)
.map_err(|io_error| SyscallError {
call: "bpf_mprog_attach",
io_error,
})?;
self.data
.links
.insert(SchedClassifierLink::new(TcLinkInner::Fd(FdLink::new(
link_fd,
))))
}
}
}
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(TcError::IoError)?;
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(ProgramError::NetlinkError)?;
Ok(())
}
}
id_as_key!(NlLink, NlLinkId);
#[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 {
Fd(FdLink),
NlLink(NlLink),
}
impl Link for TcLinkInner {
type Id = TcLinkIdInner;
fn id(&self) -> Self::Id {
match self {
Self::Fd(link) => TcLinkIdInner::FdLinkId(link.id()),
Self::NlLink(link) => TcLinkIdInner::NlLinkId(link.id()),
}
}
fn detach(self) -> Result<(), ProgramError> {
match self {
Self::Fd(link) => link.detach(),
Self::NlLink(link) => link.detach(),
}
}
}
id_as_key!(TcLinkInner, TcLinkIdInner);
impl<'a> TryFrom<&'a SchedClassifierLink> for &'a FdLink {
type Error = LinkError;
fn try_from(value: &'a SchedClassifierLink) -> Result<Self, Self::Error> {
if let TcLinkInner::Fd(fd) = value.inner() {
Ok(fd)
} else {
Err(LinkError::InvalidLink)
}
}
}
impl_try_into_fdlink!(SchedClassifierLink, TcLinkInner);
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::Fd(fd_link)));
}
Err(LinkError::InvalidLink)
}
}
define_link_wrapper!(
SchedClassifierLink,
SchedClassifierLinkId,
TcLinkInner,
TcLinkIdInner,
SchedClassifier,
);
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<(), TcError> {
let if_index = ifindex_from_ifname(if_name)?;
unsafe { netlink_qdisc_add_clsact(if_index as i32).map_err(TcError::NetlinkError) }
}
pub fn qdisc_detach_program(
if_name: &str,
attach_type: TcAttachType,
name: &str,
) -> Result<(), TcError> {
let cstr = CString::new(name).map_err(TcError::NulError)?;
let if_index = ifindex_from_ifname(if_name)? as i32;
let sock = NetlinkSocket::open().map_err(NetlinkError::from)?;
let filter_info = netlink_find_filter_with_name(&sock, if_index, attach_type, &cstr)?;
let filter_info: Vec<_> = filter_info.collect::<Result<_, _>>()?;
if filter_info.is_empty() {
return Err(TcError::IoError(io::Error::new(
io::ErrorKind::NotFound,
name.to_owned(),
)));
}
for (prio, handle) in filter_info {
unsafe { netlink_qdisc_detach(if_index, attach_type, prio, handle)? }
}
Ok(())
}