Skip to main content

profile_bee_aya/programs/
tc.rs

1//! Network traffic control programs.
2use std::{ffi::CString, io, os::fd::AsFd as _, path::Path};
3
4use aya_obj::generated::{
5    TC_H_CLSACT, TC_H_MIN_EGRESS, TC_H_MIN_INGRESS,
6    bpf_attach_type::{self, BPF_TCX_EGRESS, BPF_TCX_INGRESS},
7    bpf_link_type,
8    bpf_prog_type::BPF_PROG_TYPE_SCHED_CLS,
9};
10use thiserror::Error;
11
12use super::{FdLink, ProgramInfo};
13use crate::{
14    VerifierLogLevel,
15    programs::{
16        Link, LinkError, LinkOrder, ProgramData, ProgramError, ProgramType, define_link_wrapper,
17        id_as_key, impl_try_into_fdlink, load_program, query,
18    },
19    sys::{
20        BpfLinkCreateArgs, LinkTarget, NetlinkError, NetlinkSocket, ProgQueryTarget, SyscallError,
21        bpf_link_create, bpf_link_get_info_by_fd, bpf_link_update, bpf_prog_get_fd_by_id,
22        netlink_find_filter_with_name, netlink_qdisc_add_clsact, netlink_qdisc_attach,
23        netlink_qdisc_detach,
24    },
25    util::{KernelVersion, ifindex_from_ifname, tc_handler_make},
26};
27
28/// Traffic control attach type.
29#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
30pub enum TcAttachType {
31    /// Attach to ingress.
32    Ingress,
33    /// Attach to egress.
34    Egress,
35    /// Attach to custom parent.
36    Custom(u32),
37}
38
39/// A network traffic control classifier.
40///
41/// [`SchedClassifier`] programs can be used to inspect, filter or redirect
42/// network packets in both ingress and egress. They are executed as part of the
43/// linux network traffic control system. See
44/// [https://man7.org/linux/man-pages/man8/tc-bpf.8.html](https://man7.org/linux/man-pages/man8/tc-bpf.8.html).
45///
46/// # Examples
47///
48/// # Minimum kernel version
49///
50/// The minimum kernel version required to use this feature is 4.1.
51///
52/// ```no_run
53/// # #[derive(Debug, thiserror::Error)]
54/// # enum Error {
55/// #     #[error(transparent)]
56/// #     IO(#[from] std::io::Error),
57/// #     #[error(transparent)]
58/// #     Map(#[from] aya::maps::MapError),
59/// #     #[error(transparent)]
60/// #     Program(#[from] aya::programs::ProgramError),
61/// #     #[error(transparent)]
62/// #     Tc(#[from] aya::programs::tc::TcError),
63/// #     #[error(transparent)]
64/// #     Ebpf(#[from] aya::EbpfError)
65/// # }
66/// # let mut bpf = aya::Ebpf::load(&[])?;
67/// use aya::programs::{tc, SchedClassifier, TcAttachType};
68///
69/// // the clsact qdisc needs to be added before SchedClassifier programs can be
70/// // attached
71/// tc::qdisc_add_clsact("eth0")?;
72///
73/// let prog: &mut SchedClassifier = bpf.program_mut("redirect_ingress").unwrap().try_into()?;
74/// prog.load()?;
75/// prog.attach("eth0", TcAttachType::Ingress)?;
76///
77/// # Ok::<(), Error>(())
78/// ```
79#[derive(Debug)]
80#[doc(alias = "BPF_PROG_TYPE_SCHED_CLS")]
81pub struct SchedClassifier {
82    pub(crate) data: ProgramData<SchedClassifierLink>,
83}
84
85/// Errors from TC programs
86#[derive(Debug, Error)]
87pub enum TcError {
88    /// a netlink error occurred.
89    #[error(transparent)]
90    NetlinkError(#[from] NetlinkError),
91    /// the provided string contains a nul byte.
92    #[error(transparent)]
93    NulError(#[from] std::ffi::NulError),
94    /// an IO error occurred.
95    #[error(transparent)]
96    IoError(#[from] io::Error),
97    /// the clsact qdisc is already attached.
98    #[error("the clsact qdisc is already attached")]
99    AlreadyAttached,
100    /// tcx links can only be attached to ingress or egress, custom attachment is not supported.
101    #[error(
102        "tcx links can only be attached to ingress or egress, custom attachment: {0} is not supported"
103    )]
104    InvalidTcxAttach(u32),
105    /// operation not supported for programs loaded via tcx.
106    #[error("operation not supported for programs loaded via tcx")]
107    InvalidLinkOperation,
108}
109
110impl TcAttachType {
111    pub(crate) const fn tc_parent(self) -> u32 {
112        match self {
113            Self::Custom(parent) => parent,
114            Self::Ingress => tc_handler_make(TC_H_CLSACT, TC_H_MIN_INGRESS),
115            Self::Egress => tc_handler_make(TC_H_CLSACT, TC_H_MIN_EGRESS),
116        }
117    }
118
119    pub(crate) const fn tcx_attach_type(self) -> Result<bpf_attach_type, TcError> {
120        match self {
121            Self::Ingress => Ok(BPF_TCX_INGRESS),
122            Self::Egress => Ok(BPF_TCX_EGRESS),
123            Self::Custom(tcx_attach_type) => Err(TcError::InvalidTcxAttach(tcx_attach_type)),
124        }
125    }
126}
127
128/// Options for a [`SchedClassifier`] attach operation.
129///
130/// The options vary based on what is supported by the current kernel. Kernels
131/// older than 6.6.0 must utilize netlink for attachments, while newer kernels
132/// can utilize the modern TCX eBPF link type which supports the kernel's
133/// multi-prog API.
134#[derive(Debug)]
135pub enum TcAttachOptions {
136    /// Netlink attach options.
137    Netlink(NlOptions),
138    /// Tcx attach options.
139    TcxOrder(LinkOrder),
140}
141
142/// Options for [`SchedClassifier`] attach via netlink.
143#[derive(Debug, Default, Hash, Eq, PartialEq)]
144pub struct NlOptions {
145    /// Priority assigned to tc program with lower number = higher priority.
146    /// If set to default (0), the system chooses the next highest priority or 49152 if no filters exist yet
147    pub priority: u16,
148    /// Handle used to uniquely identify a program at a given priority level.
149    /// If set to default (0), the system chooses a handle.
150    pub handle: u32,
151}
152
153impl SchedClassifier {
154    /// The type of the program according to the kernel.
155    pub const PROGRAM_TYPE: ProgramType = ProgramType::SchedClassifier;
156
157    /// Loads the program inside the kernel.
158    pub fn load(&mut self) -> Result<(), ProgramError> {
159        load_program(BPF_PROG_TYPE_SCHED_CLS, &mut self.data)
160    }
161
162    /// Attaches the program to the given `interface`.
163    ///
164    /// On kernels >= 6.6.0, it will attempt to use the TCX interface and attach as
165    /// the last TCX program. On older kernels, it will fallback to using the
166    /// legacy netlink interface.
167    ///
168    /// For finer grained control over link ordering use [`SchedClassifier::attach_with_options`].
169    ///
170    /// The returned value can be used to detach, see [`SchedClassifier::detach`].
171    ///
172    /// # Errors
173    ///
174    /// When attaching fails, [`ProgramError::SyscallError`] is returned for
175    /// kernels `>= 6.6.0`, and [`TcError::NetlinkError`] is returned for
176    /// older kernels. A common cause of netlink attachment failure is not having added
177    /// the `clsact` qdisc to the given interface, seeĀ [`qdisc_add_clsact`]
178    ///
179    pub fn attach(
180        &mut self,
181        interface: &str,
182        attach_type: TcAttachType,
183    ) -> Result<SchedClassifierLinkId, ProgramError> {
184        if !matches!(attach_type, TcAttachType::Custom(_)) && KernelVersion::at_least(6, 6, 0) {
185            self.attach_with_options(
186                interface,
187                attach_type,
188                TcAttachOptions::TcxOrder(LinkOrder::default()),
189            )
190        } else {
191            self.attach_with_options(
192                interface,
193                attach_type,
194                TcAttachOptions::Netlink(NlOptions::default()),
195            )
196        }
197    }
198
199    /// Attaches the program to the given `interface` with options defined in [`TcAttachOptions`].
200    ///
201    /// The returned value can be used to detach, see [`SchedClassifier::detach`].
202    ///
203    /// # Link Pinning (TCX mode, kernel >= 6.6)
204    ///
205    /// Links can be pinned to bpffs for atomic replacement across process restarts.
206    ///
207    /// ```no_run
208    /// # use std::{io, path::Path};
209    ///
210    /// # use aya::{
211    /// #     programs::{
212    /// #         LinkOrder, SchedClassifier, TcAttachType,
213    /// #         links::{FdLink, LinkError, PinnedLink},
214    /// #         tc::TcAttachOptions,
215    /// #     },
216    /// #     sys::SyscallError,
217    /// # };
218    ///
219    /// # let mut bpf = aya::Ebpf::load(&[])?;
220    /// # let prog: &mut SchedClassifier = bpf.program_mut("prog").unwrap().try_into()?;
221    /// # prog.load()?;
222    /// let pin_path = "/sys/fs/bpf/my_link";
223    ///
224    /// let link_id = match PinnedLink::from_pin(pin_path) {
225    ///     Ok(old) => {
226    ///         let link = FdLink::from(old).try_into()?;
227    ///         prog.attach_to_link(link)?
228    ///     }
229    ///     Err(LinkError::SyscallError(SyscallError { io_error, .. }))
230    ///         if io_error.kind() == io::ErrorKind::NotFound =>
231    ///     {
232    ///         prog.attach_with_options(
233    ///             "eth0",
234    ///             TcAttachType::Ingress,
235    ///             TcAttachOptions::TcxOrder(LinkOrder::default()),
236    ///         )?
237    ///     }
238    ///     Err(e) => return Err(e.into()),
239    /// };
240    ///
241    /// let link = prog.take_link(link_id)?;
242    /// let fd_link: FdLink = link.try_into()?;
243    /// fd_link.pin(pin_path)?;
244    /// # Ok::<(), Box<dyn std::error::Error>>(())
245    /// ```
246    ///
247    /// # Errors
248    ///
249    /// [`TcError::NetlinkError`] is returned if attaching fails. A common cause
250    /// of failure is not having added the `clsact` qdisc to the given
251    /// interface, see [`qdisc_add_clsact`].
252    pub fn attach_with_options(
253        &mut self,
254        interface: &str,
255        attach_type: TcAttachType,
256        options: TcAttachOptions,
257    ) -> Result<SchedClassifierLinkId, ProgramError> {
258        let if_index = ifindex_from_ifname(interface).map_err(TcError::IoError)?;
259        self.do_attach(if_index, attach_type, options, true)
260    }
261
262    /// Atomically replaces the program referenced by the provided link.
263    ///
264    /// Ownership of the link will transfer to this program.
265    pub fn attach_to_link(
266        &mut self,
267        link: SchedClassifierLink,
268    ) -> Result<SchedClassifierLinkId, ProgramError> {
269        let prog_fd = self.fd()?;
270        let prog_fd = prog_fd.as_fd();
271        match link.into_inner() {
272            TcLinkInner::Fd(link) => {
273                let fd = link.fd;
274                let link_fd = fd.as_fd();
275
276                bpf_link_update(link_fd.as_fd(), prog_fd, None, 0).map_err(|io_error| {
277                    SyscallError {
278                        call: "bpf_link_update",
279                        io_error,
280                    }
281                })?;
282
283                self.data
284                    .links
285                    .insert(SchedClassifierLink::new(TcLinkInner::Fd(FdLink::new(fd))))
286            }
287            TcLinkInner::NlLink(NlLink {
288                if_index,
289                attach_type,
290                priority,
291                handle,
292            }) => self.do_attach(
293                if_index,
294                attach_type,
295                TcAttachOptions::Netlink(NlOptions { priority, handle }),
296                false,
297            ),
298        }
299    }
300
301    fn do_attach(
302        &mut self,
303        if_index: u32,
304        attach_type: TcAttachType,
305        options: TcAttachOptions,
306        create: bool,
307    ) -> Result<SchedClassifierLinkId, ProgramError> {
308        let prog_fd = self.fd()?;
309        let prog_fd = prog_fd.as_fd();
310
311        match options {
312            TcAttachOptions::Netlink(options) => {
313                let name = self.data.name.as_deref().unwrap_or_default();
314                // TODO: avoid this unwrap by adding a new error variant.
315                let name = CString::new(name).unwrap();
316                let (priority, handle) = unsafe {
317                    netlink_qdisc_attach(
318                        if_index as i32,
319                        &attach_type,
320                        prog_fd,
321                        &name,
322                        options.priority,
323                        options.handle,
324                        create,
325                    )
326                }
327                .map_err(TcError::NetlinkError)?;
328
329                self.data
330                    .links
331                    .insert(SchedClassifierLink::new(TcLinkInner::NlLink(NlLink {
332                        if_index,
333                        attach_type,
334                        priority,
335                        handle,
336                    })))
337            }
338            TcAttachOptions::TcxOrder(options) => {
339                let link_fd = bpf_link_create(
340                    prog_fd,
341                    LinkTarget::IfIndex(if_index),
342                    attach_type.tcx_attach_type()?,
343                    options.flags.bits(),
344                    Some(BpfLinkCreateArgs::Tcx(&options.link_ref)),
345                )
346                .map_err(|io_error| SyscallError {
347                    call: "bpf_mprog_attach",
348                    io_error,
349                })?;
350
351                self.data
352                    .links
353                    .insert(SchedClassifierLink::new(TcLinkInner::Fd(FdLink::new(
354                        link_fd,
355                    ))))
356            }
357        }
358    }
359
360    /// Creates a program from a pinned entry on a bpffs.
361    ///
362    /// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
363    ///
364    /// On drop, any managed links are detached and the program is unloaded. This will not result in
365    /// the program being unloaded from the kernel if it is still pinned.
366    pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, ProgramError> {
367        let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
368        Ok(Self { data })
369    }
370
371    /// Queries a given interface for attached TCX programs.
372    ///
373    /// # Example
374    ///
375    /// ```no_run
376    /// # use aya::programs::tc::{TcAttachType, SchedClassifier};
377    /// # #[derive(Debug, thiserror::Error)]
378    /// # enum Error {
379    /// #     #[error(transparent)]
380    /// #     Program(#[from] aya::programs::ProgramError),
381    /// # }
382    /// let (revision, programs) = SchedClassifier::query_tcx("eth0", TcAttachType::Ingress)?;
383    /// # Ok::<(), Error>(())
384    /// ```
385    pub fn query_tcx(
386        interface: &str,
387        attach_type: TcAttachType,
388    ) -> Result<(u64, Vec<ProgramInfo>), ProgramError> {
389        let if_index = ifindex_from_ifname(interface).map_err(TcError::IoError)?;
390
391        let (revision, prog_ids) = query(
392            ProgQueryTarget::IfIndex(if_index),
393            attach_type.tcx_attach_type()?,
394            0,
395            &mut None,
396        )?;
397
398        let prog_infos = prog_ids
399            .into_iter()
400            .map(|prog_id| {
401                let prog_fd = bpf_prog_get_fd_by_id(prog_id)?;
402                let prog_info = ProgramInfo::new_from_fd(prog_fd.as_fd())?;
403                Ok::<ProgramInfo, ProgramError>(prog_info)
404            })
405            .collect::<Result<_, _>>()?;
406
407        Ok((revision, prog_infos))
408    }
409}
410
411#[derive(Debug, Hash, Eq, PartialEq)]
412pub(crate) struct NlLinkId(u32, TcAttachType, u16, u32);
413
414#[derive(Debug)]
415pub(crate) struct NlLink {
416    if_index: u32,
417    attach_type: TcAttachType,
418    priority: u16,
419    handle: u32,
420}
421
422impl Link for NlLink {
423    type Id = NlLinkId;
424
425    fn id(&self) -> Self::Id {
426        NlLinkId(self.if_index, self.attach_type, self.priority, self.handle)
427    }
428
429    fn detach(self) -> Result<(), ProgramError> {
430        unsafe {
431            netlink_qdisc_detach(
432                self.if_index as i32,
433                self.attach_type,
434                self.priority,
435                self.handle,
436            )
437        }
438        .map_err(ProgramError::NetlinkError)?;
439        Ok(())
440    }
441}
442
443id_as_key!(NlLink, NlLinkId);
444
445#[derive(Debug, Hash, Eq, PartialEq)]
446pub(crate) enum TcLinkIdInner {
447    FdLinkId(<FdLink as Link>::Id),
448    NlLinkId(<NlLink as Link>::Id),
449}
450
451#[derive(Debug)]
452pub(crate) enum TcLinkInner {
453    Fd(FdLink),
454    NlLink(NlLink),
455}
456
457impl Link for TcLinkInner {
458    type Id = TcLinkIdInner;
459
460    fn id(&self) -> Self::Id {
461        match self {
462            Self::Fd(link) => TcLinkIdInner::FdLinkId(link.id()),
463            Self::NlLink(link) => TcLinkIdInner::NlLinkId(link.id()),
464        }
465    }
466
467    fn detach(self) -> Result<(), ProgramError> {
468        match self {
469            Self::Fd(link) => link.detach(),
470            Self::NlLink(link) => link.detach(),
471        }
472    }
473}
474
475id_as_key!(TcLinkInner, TcLinkIdInner);
476
477impl<'a> TryFrom<&'a SchedClassifierLink> for &'a FdLink {
478    type Error = LinkError;
479
480    fn try_from(value: &'a SchedClassifierLink) -> Result<Self, Self::Error> {
481        if let TcLinkInner::Fd(fd) = value.inner() {
482            Ok(fd)
483        } else {
484            Err(LinkError::InvalidLink)
485        }
486    }
487}
488
489impl_try_into_fdlink!(SchedClassifierLink, TcLinkInner);
490
491impl TryFrom<FdLink> for SchedClassifierLink {
492    type Error = LinkError;
493
494    fn try_from(fd_link: FdLink) -> Result<Self, Self::Error> {
495        let info = bpf_link_get_info_by_fd(fd_link.fd.as_fd())?;
496        if info.type_ == (bpf_link_type::BPF_LINK_TYPE_TCX as u32) {
497            return Ok(Self::new(TcLinkInner::Fd(fd_link)));
498        }
499        Err(LinkError::InvalidLink)
500    }
501}
502
503define_link_wrapper!(
504    SchedClassifierLink,
505    SchedClassifierLinkId,
506    TcLinkInner,
507    TcLinkIdInner,
508    SchedClassifier,
509);
510
511impl SchedClassifierLink {
512    /// Constructs a [`SchedClassifierLink`] where the `if_name`, `attach_type`,
513    /// `priority` and `handle` are already known. This may have been found from a link created by
514    /// [`SchedClassifier::attach`], the output of the `tc filter` command or from the output of
515    /// another BPF loader.
516    ///
517    /// Note: If you create a link for a program that you do not own, detaching it may have
518    /// unintended consequences.
519    ///
520    /// # Errors
521    /// Returns [`io::Error`] if `if_name` is invalid. If the other parameters are invalid this call
522    /// will succeed, but calling [`SchedClassifierLink::detach`] will return [`TcError::NetlinkError`].
523    ///
524    /// # Examples
525    /// ```no_run
526    /// # use aya::programs::tc::SchedClassifierLink;
527    /// # use aya::programs::TcAttachType;
528    /// # #[derive(Debug, thiserror::Error)]
529    /// # enum Error {
530    /// #     #[error(transparent)]
531    /// #     IO(#[from] std::io::Error),
532    /// # }
533    /// # fn read_persisted_link_details() -> (&'static str, TcAttachType, u16, u32) {
534    /// #     ("eth0", TcAttachType::Ingress, 50, 1)
535    /// # }
536    /// // Get the link parameters from some external source. Where and how the parameters are
537    /// // persisted is up to your application.
538    /// let (if_name, attach_type, priority, handle) = read_persisted_link_details();
539    /// let new_tc_link = SchedClassifierLink::attached(if_name, attach_type, priority, handle)?;
540    ///
541    /// # Ok::<(), Error>(())
542    /// ```
543    pub fn attached(
544        if_name: &str,
545        attach_type: TcAttachType,
546        priority: u16,
547        handle: u32,
548    ) -> Result<Self, io::Error> {
549        let if_index = ifindex_from_ifname(if_name)?;
550        Ok(Self(Some(TcLinkInner::NlLink(NlLink {
551            if_index,
552            attach_type,
553            priority,
554            handle,
555        }))))
556    }
557
558    /// Returns the attach type.
559    pub fn attach_type(&self) -> Result<TcAttachType, ProgramError> {
560        if let TcLinkInner::NlLink(n) = self.inner() {
561            Ok(n.attach_type)
562        } else {
563            Err(TcError::InvalidLinkOperation.into())
564        }
565    }
566
567    /// Returns the allocated priority. If none was provided at attach time, this was allocated for you.
568    pub fn priority(&self) -> Result<u16, ProgramError> {
569        if let TcLinkInner::NlLink(n) = self.inner() {
570            Ok(n.priority)
571        } else {
572            Err(TcError::InvalidLinkOperation.into())
573        }
574    }
575
576    /// Returns the assigned handle. If none was provided at attach time, this was allocated for you.
577    pub fn handle(&self) -> Result<u32, ProgramError> {
578        if let TcLinkInner::NlLink(n) = self.inner() {
579            Ok(n.handle)
580        } else {
581            Err(TcError::InvalidLinkOperation.into())
582        }
583    }
584}
585
586/// Add the `clasct` qdisc to the given interface.
587///
588/// The `clsact` qdisc must be added to an interface before [`SchedClassifier`]
589/// programs can be attached.
590pub fn qdisc_add_clsact(if_name: &str) -> Result<(), TcError> {
591    let if_index = ifindex_from_ifname(if_name)?;
592    unsafe { netlink_qdisc_add_clsact(if_index as i32).map_err(TcError::NetlinkError) }
593}
594
595/// Detaches the programs with the given name.
596///
597/// # Errors
598///
599/// Returns [`io::ErrorKind::NotFound`] to indicate that no programs with the
600/// given name were found, so nothing was detached. Other error kinds indicate
601/// an actual failure while detaching a program.
602pub fn qdisc_detach_program(
603    if_name: &str,
604    attach_type: TcAttachType,
605    name: &str,
606) -> Result<(), TcError> {
607    let cstr = CString::new(name).map_err(TcError::NulError)?;
608    let if_index = ifindex_from_ifname(if_name)? as i32;
609
610    let sock = NetlinkSocket::open().map_err(NetlinkError::from)?;
611    let filter_info = netlink_find_filter_with_name(&sock, if_index, attach_type, &cstr)?;
612    // Check for errors before detaching any programs.
613    let filter_info: Vec<_> = filter_info.collect::<Result<_, _>>()?;
614    if filter_info.is_empty() {
615        return Err(TcError::IoError(io::Error::new(
616            io::ErrorKind::NotFound,
617            name.to_owned(),
618        )));
619    }
620
621    for (prio, handle) in filter_info {
622        unsafe { netlink_qdisc_detach(if_index, attach_type, prio, handle)? }
623    }
624
625    Ok(())
626}