You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aya/aya/src/programs/tc.rs

608 lines
20 KiB
Rust

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

//! Network traffic control programs.
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,
};
/// Traffic control attach type.
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
pub enum TcAttachType {
/// Attach to ingress.
Ingress,
/// Attach to egress.
Egress,
/// Attach to custom parent.
Custom(u32),
}
/// A network traffic control classifier.
///
/// [`SchedClassifier`] programs can be used to inspect, filter or redirect
/// network packets in both ingress and egress. They are executed as part of the
/// linux network traffic control system. See
/// [https://man7.org/linux/man-pages/man8/tc-bpf.8.html](https://man7.org/linux/man-pages/man8/tc-bpf.8.html).
///
/// # Examples
///
/// # Minimum kernel version
///
/// The minimum kernel version required to use this feature is 4.1.
///
/// ```no_run
/// # #[derive(Debug, thiserror::Error)]
/// # enum Error {
/// # #[error(transparent)]
/// # IO(#[from] std::io::Error),
/// # #[error(transparent)]
/// # Map(#[from] aya::maps::MapError),
/// # #[error(transparent)]
/// # Program(#[from] aya::programs::ProgramError),
/// # #[error(transparent)]
/// # Ebpf(#[from] aya::EbpfError)
/// # }
/// # let mut bpf = aya::Ebpf::load(&[])?;
/// use aya::programs::{tc, SchedClassifier, TcAttachType};
///
/// // the clsact qdisc needs to be added before SchedClassifier programs can be
/// // attached
/// tc::qdisc_add_clsact("eth0")?;
///
/// let prog: &mut SchedClassifier = bpf.program_mut("redirect_ingress").unwrap().try_into()?;
/// prog.load()?;
/// prog.attach("eth0", TcAttachType::Ingress)?;
///
/// # Ok::<(), Error>(())
/// ```
#[derive(Debug)]
#[doc(alias = "BPF_PROG_TYPE_SCHED_CLS")]
pub struct SchedClassifier {
pub(crate) data: ProgramData<SchedClassifierLink>,
}
/// Errors from TC programs
#[derive(Debug, Error)]
pub enum TcError {
/// netlink error while attaching ebpf program
#[error("netlink error while attaching ebpf program to tc")]
NetlinkError {
/// the [`io::Error`] from the netlink call
#[source]
io_error: io::Error,
},
/// the clsact qdisc is already attached
#[error("the clsact qdisc is already attached")]
AlreadyAttached,
/// tcx links can only be attached to ingress or egress, custom attachment is not supported
#[error("tcx links can only be attached to ingress or egress, custom attachment: {0} is not supported")]
InvalidTcxAttach(u32),
/// operation not supported for programs loaded via tcx
#[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)),
}
}
}
/// Options for a SchedClassifier attach operation.
///
/// The options vary based on what is supported by the current kernel. Kernels
/// older than 6.6.0 must utilize netlink for attachments, while newer kernels
/// can utilize the modern TCX eBPF link type which supports the kernel's
/// multi-prog API.
#[derive(Debug)]
pub enum TcAttachOptions {
/// Netlink attach options.
Netlink(NlOptions),
/// Tcx attach options.
TcxOrder(LinkOrder),
}
/// Options for SchedClassifier attach via netlink.
#[derive(Debug, Default, Hash, Eq, PartialEq)]
pub struct NlOptions {
/// Priority assigned to tc program with lower number = higher priority.
/// If set to default (0), the system chooses the next highest priority or 49152 if no filters exist yet
pub priority: u16,
/// Handle used to uniquely identify a program at a given priority level.
/// If set to default (0), the system chooses a handle.
pub handle: u32,
}
impl SchedClassifier {
/// Loads the program inside the kernel.
pub fn load(&mut self) -> Result<(), ProgramError> {
load_program(BPF_PROG_TYPE_SCHED_CLS, &mut self.data)
}
/// Attaches the program to the given `interface`.
///
/// On kernels >= 6.6.0, it will attempt to use the TCX interface and attach as
/// the last TCX program. On older kernels, it will fallback to using the
/// legacy netlink interface.
///
/// For finer grained control over link ordering use [`SchedClassifier::attach_with_options`].
///
/// The returned value can be used to detach, see [SchedClassifier::detach].
///
/// # Errors
///
/// When attaching fails, [`ProgramError::SyscallError`] is returned for
/// kernels `>= 6.6.0`, and [`TcError::NetlinkError`] is returned for
/// older kernels. A common cause of netlink attachment failure is not having added
/// the `clsact` qdisc to the given interface, see [`qdisc_add_clsact`]
///
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()),
)
}
}
/// Attaches the program to the given `interface` with options defined in [`TcAttachOptions`].
///
/// The returned value can be used to detach, see [SchedClassifier::detach].
///
/// # Errors
///
/// [`TcError::NetlinkError`] is returned if attaching fails. A common cause
/// of failure is not having added the `clsact` qdisc to the given
/// interface, see [`qdisc_add_clsact`]
///
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)
}
/// Atomically replaces the program referenced by the provided link.
///
/// Ownership of the link will transfer to this program.
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();
// TODO: avoid this unwrap by adding a new error variant.
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,
))))
}
}
}
/// Creates a program from a pinned entry on a bpffs.
///
/// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
///
/// On drop, any managed links are detached and the program is unloaded. This will not result in
/// the program being unloaded from the kernel if it is still pinned.
pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, ProgramError> {
let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
Ok(Self { data })
}
/// Queries a given interface for attached TCX programs.
///
/// # Example
///
/// ```no_run
/// # use aya::programs::tc::{TcAttachType, SchedClassifier};
/// # #[derive(Debug, thiserror::Error)]
/// # enum Error {
/// # #[error(transparent)]
/// # Program(#[from] aya::programs::ProgramError),
/// # }
/// let (revision, programs) = SchedClassifier::query_tcx("eth0", TcAttachType::Ingress)?;
/// # Ok::<(), Error>(())
/// ```
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!(
/// The link used by [SchedClassifier] programs.
SchedClassifierLink,
/// The type returned by [SchedClassifier::attach]. Can be passed to [SchedClassifier::detach].
SchedClassifierLinkId,
TcLinkInner,
TcLinkIdInner,
SchedClassifier,
);
impl SchedClassifierLink {
/// Constructs a [`SchedClassifierLink`] where the `if_name`, `attach_type`,
/// `priority` and `handle` are already known. This may have been found from a link created by
/// [SchedClassifier::attach], the output of the `tc filter` command or from the output of
/// another BPF loader.
///
/// Note: If you create a link for a program that you do not own, detaching it may have
/// unintended consequences.
///
/// # Errors
/// Returns [`io::Error`] if `if_name` is invalid. If the other parameters are invalid this call
/// will succeed, but calling [`SchedClassifierLink::detach`] will return [`TcError::NetlinkError`].
///
/// # Examples
/// ```no_run
/// # use aya::programs::tc::SchedClassifierLink;
/// # use aya::programs::TcAttachType;
/// # #[derive(Debug, thiserror::Error)]
/// # enum Error {
/// # #[error(transparent)]
/// # IO(#[from] std::io::Error),
/// # }
/// # fn read_persisted_link_details() -> (&'static str, TcAttachType, u16, u32) {
/// # ("eth0", TcAttachType::Ingress, 50, 1)
/// # }
/// // Get the link parameters from some external source. Where and how the parameters are
/// // persisted is up to your application.
/// let (if_name, attach_type, priority, handle) = read_persisted_link_details();
/// let new_tc_link = SchedClassifierLink::attached(if_name, attach_type, priority, handle)?;
///
/// # Ok::<(), Error>(())
/// ```
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,
}))))
}
/// Returns the attach type.
pub fn attach_type(&self) -> Result<TcAttachType, ProgramError> {
if let TcLinkInner::NlLink(n) = self.inner() {
Ok(n.attach_type)
} else {
Err(TcError::InvalidLinkOperation.into())
}
}
/// Returns the allocated priority. If none was provided at attach time, this was allocated for you.
pub fn priority(&self) -> Result<u16, ProgramError> {
if let TcLinkInner::NlLink(n) = self.inner() {
Ok(n.priority)
} else {
Err(TcError::InvalidLinkOperation.into())
}
}
/// Returns the assigned handle. If none was provided at attach time, this was allocated for you.
pub fn handle(&self) -> Result<u32, ProgramError> {
if let TcLinkInner::NlLink(n) = self.inner() {
Ok(n.handle)
} else {
Err(TcError::InvalidLinkOperation.into())
}
}
}
/// Add the `clasct` qdisc to the given interface.
///
/// The `clsact` qdisc must be added to an interface before [`SchedClassifier`]
/// programs can be attached.
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) }
}
/// Detaches the programs with the given name.
///
/// # Errors
///
/// Returns [`io::ErrorKind::NotFound`] to indicate that no programs with the
/// given name were found, so nothing was detached. Other error kinds indicate
/// an actual failure while detaching a program.
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)
}
/// Detaches the programs with the given name as a C string.
/// Unlike qdisc_detach_program, this function does not allocate an additional
/// CString to.
///
/// # Errors
///
/// Returns [`io::ErrorKind::NotFound`] to indicate that no programs with the
/// given name were found, so nothing was detached. Other error kinds indicate
/// an actual failure while detaching a program.
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(())
}