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}