Skip to main content

aya_friday/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_from_fdlink, impl_try_into_fdlink, load_program_without_attach_type,
18        query,
19    },
20    sys::{
21        BpfLinkCreateArgs, LinkTarget, NetlinkError, NetlinkSocket, ProgQueryTarget, SyscallError,
22        bpf_link_create, bpf_link_update, bpf_prog_get_fd_by_id, netlink_find_filter_with_name,
23        netlink_qdisc_add_clsact, netlink_qdisc_attach, 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/// A TC filter handle in `major:minor` form.
143///
144/// Matches the `M:N` syntax accepted by `tc(8)`. Use [`TcHandle::AUTO_ASSIGN`]
145/// to ask the kernel to allocate one.
146#[derive(Debug, Clone, Copy, Default, Hash, Eq, PartialEq)]
147#[doc(alias = "tcm_handle")]
148pub struct TcHandle {
149    /// Upper 16 bits when encoded as a u32.
150    major: u16,
151    /// Lower 16 bits when encoded as a u32.
152    minor: u16,
153}
154
155impl TcHandle {
156    /// Sentinel that asks the kernel to allocate a handle at attach time.
157    ///
158    /// Equal to [`Default::default`]. The allocated value is exposed by
159    /// [`SchedClassifierLink::handle`] after the program is attached.
160    pub const AUTO_ASSIGN: Self = Self { major: 0, minor: 0 };
161
162    /// Const equivalent of `Self { major, minor }`.
163    pub const fn new(major: u16, minor: u16) -> Self {
164        Self { major, minor }
165    }
166}
167
168impl From<TcHandle> for u32 {
169    fn from(TcHandle { major, minor }: TcHandle) -> Self {
170        (Self::from(major) << 16) | Self::from(minor)
171    }
172}
173
174impl From<u32> for TcHandle {
175    fn from(value: u32) -> Self {
176        Self {
177            major: (value >> 16) as u16,
178            minor: value as u16,
179        }
180    }
181}
182
183/// Options for [`SchedClassifier`] attach via netlink.
184#[derive(Debug, Default, Hash, Eq, PartialEq)]
185pub struct NlOptions {
186    /// Priority assigned to tc program with lower number = higher priority.
187    /// If set to default (0), the system chooses the next highest priority or 49152 if no filters exist yet
188    pub priority: u16,
189    /// Handle used to uniquely identify a program at a given priority level.
190    ///
191    /// Defaults to [`TcHandle::AUTO_ASSIGN`], which lets the kernel pick one.
192    pub handle: TcHandle,
193    /// `classid` bound to this filter (also known as `flowid` in `tc(8)`).
194    ///
195    /// In direct-action mode the major 16 bits of the resulting class id come
196    /// from this attribute and the minor 16 bits come from the program at run
197    /// time via `__sk_buff::tc_classid`. This split requires Linux 4.6 (commit
198    /// [`3a461da1d03e`]).
199    ///
200    /// When [`None`], no attribute is sent and the filter is not bound to a
201    /// class.
202    ///
203    /// [`3a461da1d03e`]: https://github.com/torvalds/linux/commit/3a461da1d03e7a857edfa6a002040d07e118c639
204    #[doc(alias = "TCA_BPF_CLASSID")]
205    pub classid: Option<TcHandle>,
206}
207
208impl SchedClassifier {
209    /// The type of the program according to the kernel.
210    pub const PROGRAM_TYPE: ProgramType = ProgramType::SchedClassifier;
211
212    /// Loads the program inside the kernel.
213    pub fn load(&mut self) -> Result<(), ProgramError> {
214        let Self { data } = self;
215        load_program_without_attach_type(BPF_PROG_TYPE_SCHED_CLS, data)
216    }
217
218    /// Attaches the program to the given `interface`.
219    ///
220    /// On kernels >= 6.6.0, it will attempt to use the TCX interface and attach as
221    /// the last TCX program. On older kernels, it will fallback to using the
222    /// legacy netlink interface.
223    ///
224    /// For finer grained control over link ordering use [`SchedClassifier::attach_with_options`].
225    ///
226    /// The returned value can be used to detach, see [`SchedClassifier::detach`].
227    ///
228    /// # Errors
229    ///
230    /// When attaching fails, [`ProgramError::SyscallError`] is returned for
231    /// kernels `>= 6.6.0`, and [`TcError::NetlinkError`] is returned for
232    /// older kernels. A common cause of netlink attachment failure is not having added
233    /// the `clsact` qdisc to the given interface, seeĀ [`qdisc_add_clsact`]
234    ///
235    pub fn attach(
236        &mut self,
237        interface: &str,
238        attach_type: TcAttachType,
239    ) -> Result<SchedClassifierLinkId, ProgramError> {
240        if !matches!(attach_type, TcAttachType::Custom(_)) && KernelVersion::at_least(6, 6, 0) {
241            self.attach_with_options(
242                interface,
243                attach_type,
244                TcAttachOptions::TcxOrder(LinkOrder::default()),
245            )
246        } else {
247            self.attach_with_options(
248                interface,
249                attach_type,
250                TcAttachOptions::Netlink(NlOptions::default()),
251            )
252        }
253    }
254
255    /// Attaches the program to the given `interface` with options defined in [`TcAttachOptions`].
256    ///
257    /// The returned value can be used to detach, see [`SchedClassifier::detach`].
258    ///
259    /// # Link Pinning (TCX mode, kernel >= 6.6)
260    ///
261    /// Links can be pinned to bpffs for atomic replacement across process restarts.
262    ///
263    /// ```no_run
264    /// # use std::{io, path::Path};
265    ///
266    /// # use aya::{
267    /// #     programs::{
268    /// #         LinkOrder, SchedClassifier, TcAttachType,
269    /// #         links::{FdLink, LinkError, PinnedLink},
270    /// #         tc::TcAttachOptions,
271    /// #     },
272    /// #     sys::SyscallError,
273    /// # };
274    ///
275    /// # let mut bpf = aya::Ebpf::load(&[])?;
276    /// # let prog: &mut SchedClassifier = bpf.program_mut("prog").unwrap().try_into()?;
277    /// # prog.load()?;
278    /// let pin_path = "/sys/fs/bpf/my_link";
279    ///
280    /// let link_id = match PinnedLink::from_pin(pin_path) {
281    ///     Ok(old) => {
282    ///         let link = FdLink::from(old).try_into()?;
283    ///         prog.attach_to_link(link)?
284    ///     }
285    ///     Err(LinkError::SyscallError(SyscallError { io_error, .. }))
286    ///         if io_error.kind() == io::ErrorKind::NotFound =>
287    ///     {
288    ///         prog.attach_with_options(
289    ///             "eth0",
290    ///             TcAttachType::Ingress,
291    ///             TcAttachOptions::TcxOrder(LinkOrder::default()),
292    ///         )?
293    ///     }
294    ///     Err(e) => return Err(e.into()),
295    /// };
296    ///
297    /// let link = prog.take_link(link_id)?;
298    /// let fd_link: FdLink = link.try_into()?;
299    /// fd_link.pin(pin_path)?;
300    /// # Ok::<(), Box<dyn std::error::Error>>(())
301    /// ```
302    ///
303    /// # Errors
304    ///
305    /// [`TcError::NetlinkError`] is returned if attaching fails. A common cause
306    /// of failure is not having added the `clsact` qdisc to the given
307    /// interface, see [`qdisc_add_clsact`].
308    pub fn attach_with_options(
309        &mut self,
310        interface: &str,
311        attach_type: TcAttachType,
312        options: TcAttachOptions,
313    ) -> Result<SchedClassifierLinkId, ProgramError> {
314        let if_index = ifindex_from_ifname(interface).map_err(TcError::IoError)?;
315        self.do_attach(if_index, attach_type, options, true)
316    }
317
318    /// Atomically replaces the program referenced by the provided link.
319    ///
320    /// Ownership of the link will transfer to this program.
321    pub fn attach_to_link(
322        &mut self,
323        link: SchedClassifierLink,
324    ) -> Result<SchedClassifierLinkId, ProgramError> {
325        let prog_fd = self.fd()?;
326        let prog_fd = prog_fd.as_fd();
327        match link.into_inner() {
328            TcLinkInner::Fd(link) => {
329                let fd = link.fd;
330                let link_fd = fd.as_fd();
331
332                bpf_link_update(link_fd.as_fd(), prog_fd, None, 0).map_err(|io_error| {
333                    SyscallError {
334                        call: "bpf_link_update",
335                        io_error,
336                    }
337                })?;
338
339                self.data
340                    .links
341                    .insert(SchedClassifierLink::new(TcLinkInner::Fd(FdLink::new(fd))))
342            }
343            TcLinkInner::NlLink(NlLink {
344                if_index,
345                attach_type,
346                priority,
347                handle,
348                classid,
349            }) => self.do_attach(
350                if_index,
351                attach_type,
352                TcAttachOptions::Netlink(NlOptions {
353                    priority,
354                    handle,
355                    classid,
356                }),
357                false,
358            ),
359        }
360    }
361
362    fn do_attach(
363        &mut self,
364        if_index: u32,
365        attach_type: TcAttachType,
366        options: TcAttachOptions,
367        create: bool,
368    ) -> Result<SchedClassifierLinkId, ProgramError> {
369        let prog_fd = self.fd()?;
370        let prog_fd = prog_fd.as_fd();
371
372        match options {
373            TcAttachOptions::Netlink(options) => {
374                let name = self.data.name.as_deref().unwrap_or_default();
375                // TODO: avoid this unwrap by adding a new error variant.
376                let name = CString::new(name).unwrap();
377                let (priority, handle) = unsafe {
378                    netlink_qdisc_attach(
379                        if_index as i32,
380                        &attach_type,
381                        prog_fd,
382                        &name,
383                        options.priority,
384                        options.handle,
385                        options.classid,
386                        create,
387                    )
388                }
389                .map_err(TcError::NetlinkError)?;
390
391                self.data
392                    .links
393                    .insert(SchedClassifierLink::new(TcLinkInner::NlLink(NlLink {
394                        if_index,
395                        attach_type,
396                        priority,
397                        handle,
398                        classid: options.classid,
399                    })))
400            }
401            TcAttachOptions::TcxOrder(options) => {
402                let link_fd = bpf_link_create(
403                    prog_fd,
404                    LinkTarget::IfIndex(if_index),
405                    attach_type.tcx_attach_type()?,
406                    options.flags.bits(),
407                    Some(BpfLinkCreateArgs::Tcx(&options.link_ref)),
408                )
409                .map_err(|io_error| SyscallError {
410                    call: "bpf_mprog_attach",
411                    io_error,
412                })?;
413
414                self.data
415                    .links
416                    .insert(SchedClassifierLink::new(TcLinkInner::Fd(FdLink::new(
417                        link_fd,
418                    ))))
419            }
420        }
421    }
422
423    /// Creates a program from a pinned entry on a bpffs.
424    ///
425    /// Existing links will not be populated. To work with existing links you should use [`crate::programs::links::PinnedLink`].
426    ///
427    /// On drop, any managed links are detached and the program is unloaded. This will not result in
428    /// the program being unloaded from the kernel if it is still pinned.
429    pub fn from_pin<P: AsRef<Path>>(path: P) -> Result<Self, ProgramError> {
430        let data = ProgramData::from_pinned_path(path, VerifierLogLevel::default())?;
431        Ok(Self { data })
432    }
433
434    /// Queries a given interface for attached TCX programs.
435    ///
436    /// # Example
437    ///
438    /// ```no_run
439    /// # use aya::programs::tc::{TcAttachType, SchedClassifier};
440    /// # #[derive(Debug, thiserror::Error)]
441    /// # enum Error {
442    /// #     #[error(transparent)]
443    /// #     Program(#[from] aya::programs::ProgramError),
444    /// # }
445    /// let (revision, programs) = SchedClassifier::query_tcx("eth0", TcAttachType::Ingress)?;
446    /// # Ok::<(), Error>(())
447    /// ```
448    pub fn query_tcx(
449        interface: &str,
450        attach_type: TcAttachType,
451    ) -> Result<(u64, Vec<ProgramInfo>), ProgramError> {
452        let if_index = ifindex_from_ifname(interface).map_err(TcError::IoError)?;
453
454        let (revision, prog_ids) = query(
455            ProgQueryTarget::IfIndex(if_index),
456            attach_type.tcx_attach_type()?,
457            0,
458            &mut None,
459        )?;
460
461        let prog_infos = prog_ids
462            .into_iter()
463            .map(|prog_id| {
464                let prog_fd = bpf_prog_get_fd_by_id(prog_id)?;
465                let prog_info = ProgramInfo::new_from_fd(prog_fd.as_fd())?;
466                Ok::<ProgramInfo, ProgramError>(prog_info)
467            })
468            .collect::<Result<_, _>>()?;
469
470        Ok((revision, prog_infos))
471    }
472}
473
474#[derive(Debug, Hash, Eq, PartialEq)]
475pub(crate) struct NlLinkId(u32, TcAttachType, u16, TcHandle);
476
477#[derive(Debug)]
478pub(crate) struct NlLink {
479    if_index: u32,
480    attach_type: TcAttachType,
481    priority: u16,
482    handle: TcHandle,
483    classid: Option<TcHandle>,
484}
485
486impl Link for NlLink {
487    type Id = NlLinkId;
488
489    fn id(&self) -> Self::Id {
490        NlLinkId(self.if_index, self.attach_type, self.priority, self.handle)
491    }
492
493    fn detach(self) -> Result<(), ProgramError> {
494        unsafe {
495            netlink_qdisc_detach(
496                self.if_index as i32,
497                self.attach_type,
498                self.priority,
499                self.handle,
500            )
501        }
502        .map_err(ProgramError::NetlinkError)?;
503        Ok(())
504    }
505}
506
507id_as_key!(NlLink, NlLinkId);
508
509#[derive(Debug, Hash, Eq, PartialEq)]
510pub(crate) enum TcLinkIdInner {
511    FdLinkId(<FdLink as Link>::Id),
512    NlLinkId(<NlLink as Link>::Id),
513}
514
515#[derive(Debug)]
516pub(crate) enum TcLinkInner {
517    Fd(FdLink),
518    NlLink(NlLink),
519}
520
521impl Link for TcLinkInner {
522    type Id = TcLinkIdInner;
523
524    fn id(&self) -> Self::Id {
525        match self {
526            Self::Fd(link) => TcLinkIdInner::FdLinkId(link.id()),
527            Self::NlLink(link) => TcLinkIdInner::NlLinkId(link.id()),
528        }
529    }
530
531    fn detach(self) -> Result<(), ProgramError> {
532        match self {
533            Self::Fd(link) => link.detach(),
534            Self::NlLink(link) => link.detach(),
535        }
536    }
537}
538
539id_as_key!(TcLinkInner, TcLinkIdInner);
540
541impl<'a> TryFrom<&'a SchedClassifierLink> for &'a FdLink {
542    type Error = LinkError;
543
544    fn try_from(value: &'a SchedClassifierLink) -> Result<Self, Self::Error> {
545        if let TcLinkInner::Fd(fd) = value.inner() {
546            Ok(fd)
547        } else {
548            Err(LinkError::InvalidLink)
549        }
550    }
551}
552
553impl_try_into_fdlink!(SchedClassifierLink, TcLinkInner);
554impl_try_from_fdlink!(
555    SchedClassifierLink,
556    TcLinkInner,
557    bpf_link_type::BPF_LINK_TYPE_TCX
558);
559
560define_link_wrapper!(
561    SchedClassifierLink,
562    SchedClassifierLinkId,
563    TcLinkInner,
564    TcLinkIdInner,
565    SchedClassifier,
566);
567
568impl SchedClassifierLink {
569    /// Constructs a [`SchedClassifierLink`] where the `if_name`, `attach_type`,
570    /// `priority` and `handle` are already known. This may have been found from a link created by
571    /// [`SchedClassifier::attach`], the output of the `tc filter` command or from the output of
572    /// another BPF loader.
573    ///
574    /// Note: If you create a link for a program that you do not own, detaching it may have
575    /// unintended consequences.
576    ///
577    /// # Errors
578    /// Returns [`io::Error`] if `if_name` is invalid. If the other parameters are invalid this call
579    /// will succeed, but calling [`SchedClassifierLink::detach`] will return [`TcError::NetlinkError`].
580    ///
581    /// # Examples
582    /// ```no_run
583    /// # use aya::programs::tc::{SchedClassifierLink, TcHandle};
584    /// # use aya::programs::TcAttachType;
585    /// # #[derive(Debug, thiserror::Error)]
586    /// # enum Error {
587    /// #     #[error(transparent)]
588    /// #     IO(#[from] std::io::Error),
589    /// # }
590    /// # fn read_persisted_link_details() -> (&'static str, TcAttachType, u16, TcHandle, Option<TcHandle>) {
591    /// #     ("eth0", TcAttachType::Ingress, 50, TcHandle::new(0, 1), None)
592    /// # }
593    /// // Get the link parameters from some external source. Where and how the parameters are
594    /// // persisted is up to your application.
595    /// let (if_name, attach_type, priority, handle, classid) = read_persisted_link_details();
596    /// let new_tc_link =
597    ///     SchedClassifierLink::attached(if_name, attach_type, priority, handle, classid)?;
598    ///
599    /// # Ok::<(), Error>(())
600    /// ```
601    pub fn attached(
602        if_name: &str,
603        attach_type: TcAttachType,
604        priority: u16,
605        handle: TcHandle,
606        classid: Option<TcHandle>,
607    ) -> Result<Self, io::Error> {
608        let if_index = ifindex_from_ifname(if_name)?;
609        Ok(Self(Some(TcLinkInner::NlLink(NlLink {
610            if_index,
611            attach_type,
612            priority,
613            handle,
614            classid,
615        }))))
616    }
617
618    /// Returns the attach type.
619    pub fn attach_type(&self) -> Result<TcAttachType, ProgramError> {
620        if let TcLinkInner::NlLink(n) = self.inner() {
621            Ok(n.attach_type)
622        } else {
623            Err(TcError::InvalidLinkOperation.into())
624        }
625    }
626
627    /// Returns the allocated priority. If none was provided at attach time, this was allocated for you.
628    pub fn priority(&self) -> Result<u16, ProgramError> {
629        if let TcLinkInner::NlLink(n) = self.inner() {
630            Ok(n.priority)
631        } else {
632            Err(TcError::InvalidLinkOperation.into())
633        }
634    }
635
636    /// Returns the assigned handle. If none was provided at attach time, this was allocated for you.
637    pub fn handle(&self) -> Result<TcHandle, ProgramError> {
638        if let TcLinkInner::NlLink(n) = self.inner() {
639            Ok(n.handle)
640        } else {
641            Err(TcError::InvalidLinkOperation.into())
642        }
643    }
644
645    /// Returns the `classid` bound to this filter, or [`None`] if the filter
646    /// is not bound to a class. See [`NlOptions::classid`].
647    pub fn classid(&self) -> Result<Option<TcHandle>, ProgramError> {
648        if let TcLinkInner::NlLink(n) = self.inner() {
649            Ok(n.classid)
650        } else {
651            Err(TcError::InvalidLinkOperation.into())
652        }
653    }
654}
655
656/// Add the `clasct` qdisc to the given interface.
657///
658/// The `clsact` qdisc must be added to an interface before [`SchedClassifier`]
659/// programs can be attached.
660pub fn qdisc_add_clsact(if_name: &str) -> Result<(), TcError> {
661    let if_index = ifindex_from_ifname(if_name)?;
662    unsafe { netlink_qdisc_add_clsact(if_index as i32).map_err(TcError::NetlinkError) }
663}
664
665/// Detaches the programs with the given name.
666///
667/// # Errors
668///
669/// Returns [`io::ErrorKind::NotFound`] to indicate that no programs with the
670/// given name were found, so nothing was detached. Other error kinds indicate
671/// an actual failure while detaching a program.
672pub fn qdisc_detach_program(
673    if_name: &str,
674    attach_type: TcAttachType,
675    name: &str,
676) -> Result<(), TcError> {
677    let cstr = CString::new(name).map_err(TcError::NulError)?;
678    let if_index = ifindex_from_ifname(if_name)? as i32;
679
680    let sock = NetlinkSocket::open().map_err(NetlinkError::from)?;
681    let filter_info = netlink_find_filter_with_name(&sock, if_index, attach_type, &cstr)?;
682    // Check for errors before detaching any programs.
683    let filter_info: Vec<_> = filter_info.collect::<Result<_, _>>()?;
684    if filter_info.is_empty() {
685        return Err(TcError::IoError(io::Error::new(
686            io::ErrorKind::NotFound,
687            name.to_owned(),
688        )));
689    }
690
691    for (prio, handle) in filter_info {
692        unsafe { netlink_qdisc_detach(if_index, attach_type, prio, handle)? }
693    }
694
695    Ok(())
696}