Repository: aya-rs/aya (language: rust)

Issue:
Add TCX link support 
Following the recent addition of [link support for TC programs](https://lwn.net/Articles/938632/) we should add the necessary bits to support this in aya as well. 

The [commit adding support to the cilium ebpf library](https://github.com/cilium/ebpf/commit/417f8a264d2e691649ede9b995fb334e6c58ca71) can sort of pave the way for us here 


Discussion / hints:

Fix patch:
diff --git a/aya/src/programs/cgroup_device.rs b/aya/src/programs/cgroup_device.rs
index 78b01d05c..87c5d4a26 100644
--- a/aya/src/programs/cgroup_device.rs
+++ b/aya/src/programs/cgroup_device.rs
@@ -8,7 +8,7 @@ use crate::{
         bpf_prog_get_fd_by_id, define_link_wrapper, load_program, query, CgroupAttachMode, FdLink,
         Link, ProgAttachLink, ProgramData, ProgramError, ProgramFd,
     },
-    sys::{bpf_link_create, LinkTarget, SyscallError},
+    sys::{bpf_link_create, LinkTarget, ProgQueryTarget, SyscallError},
     util::KernelVersion,
 };
 
@@ -77,6 +77,7 @@ impl CgroupDevice {
                 BPF_CGROUP_DEVICE,
                 None,
                 mode.into(),
+                None,
             )
             .map_err(|(_, io_error)| SyscallError {
                 call: "bpf_link_create",
@@ -119,7 +120,12 @@ impl CgroupDevice {
     /// Queries the cgroup for attached programs.
     pub fn query<T: AsFd>(target_fd: T) -> Result<Vec<CgroupDeviceLink>, ProgramError> {
         let target_fd = target_fd.as_fd();
-        let prog_ids = query(target_fd, BPF_CGROUP_DEVICE, 0, &mut None)?;
+        let (_, prog_ids) = query(
+            ProgQueryTarget::Fd(target_fd),
+            BPF_CGROUP_DEVICE,
+            0,
+            &mut None,
+        )?;
 
         prog_ids
             .into_iter()
diff --git a/aya/src/programs/cgroup_skb.rs b/aya/src/programs/cgroup_skb.rs
index 5054a2592..637e897ae 100644
--- a/aya/src/programs/cgroup_skb.rs
+++ b/aya/src/programs/cgroup_skb.rs
@@ -105,6 +105,7 @@ impl CgroupSkb {
                 attach_type,
                 None,
                 mode.into(),
+                None,
             )
             .map_err(|(_, io_error)| SyscallError {
                 call: "bpf_link_create",
diff --git a/aya/src/programs/cgroup_sock.rs b/aya/src/programs/cgroup_sock.rs
index 878c617f0..cc5db22fe 100644
--- a/aya/src/programs/cgroup_sock.rs
+++ b/aya/src/programs/cgroup_sock.rs
@@ -83,6 +83,7 @@ impl CgroupSock {
                 attach_type,
                 None,
                 mode.into(),
+                None,
             )
             .map_err(|(_, io_error)| SyscallError {
                 call: "bpf_link_create",
diff --git a/aya/src/programs/cgroup_sock_addr.rs b/aya/src/programs/cgroup_sock_addr.rs
index 876037b31..a31d8cfde 100644
--- a/aya/src/programs/cgroup_sock_addr.rs
+++ b/aya/src/programs/cgroup_sock_addr.rs
@@ -84,6 +84,7 @@ impl CgroupSockAddr {
                 attach_type,
                 None,
                 mode.into(),
+                None,
             )
             .map_err(|(_, io_error)| SyscallError {
                 call: "bpf_link_create",
diff --git a/aya/src/programs/cgroup_sockopt.rs b/aya/src/programs/cgroup_sockopt.rs
index c67dcc5b1..ee9d40b23 100644
--- a/aya/src/programs/cgroup_sockopt.rs
+++ b/aya/src/programs/cgroup_sockopt.rs
@@ -81,6 +81,7 @@ impl CgroupSockopt {
                 attach_type,
                 None,
                 mode.into(),
+                None,
             )
             .map_err(|(_, io_error)| SyscallError {
                 call: "bpf_link_create",
diff --git a/aya/src/programs/cgroup_sysctl.rs b/aya/src/programs/cgroup_sysctl.rs
index 94ccd47fd..fd997aa13 100644
--- a/aya/src/programs/cgroup_sysctl.rs
+++ b/aya/src/programs/cgroup_sysctl.rs
@@ -76,6 +76,7 @@ impl CgroupSysctl {
                 BPF_CGROUP_SYSCTL,
                 None,
                 mode.into(),
+                None,
             )
             .map_err(|(_, io_error)| SyscallError {
                 call: "bpf_link_create",
diff --git a/aya/src/programs/extension.rs b/aya/src/programs/extension.rs
index 558bef054..68d77f380 100644
--- a/aya/src/programs/extension.rs
+++ b/aya/src/programs/extension.rs
@@ -103,6 +103,7 @@ impl Extension {
             BPF_CGROUP_INET_INGRESS,
             Some(btf_id),
             0,
+            None,
         )
         .map_err(|(_, io_error)| SyscallError {
             call: "bpf_link_create",
@@ -140,6 +141,7 @@ impl Extension {
             BPF_CGROUP_INET_INGRESS,
             Some(btf_id),
             0,
+            None,
         )
         .map_err(|(_, io_error)| SyscallError {
             call: "bpf_link_create",
diff --git a/aya/src/programs/links.rs b/aya/src/programs/links.rs
index e9afaa876..9e8ac1c0e 100644
--- a/aya/src/programs/links.rs
+++ b/aya/src/programs/links.rs
@@ -10,9 +10,12 @@ use std::{
 use thiserror::Error;
 
 use crate::{
-    generated::{bpf_attach_type, BPF_F_ALLOW_MULTI, BPF_F_ALLOW_OVERRIDE},
+    generated::{
+        bpf_attach_type, BPF_F_AFTER, BPF_F_ALLOW_MULTI, BPF_F_ALLOW_OVERRIDE, BPF_F_BEFORE,
+        BPF_F_ID, BPF_F_LINK, BPF_F_REPLACE,
+    },
     pin::PinError,
-    programs::{ProgramError, ProgramFd},
+    programs::{MultiProgLink, MultiProgram, ProgramError, ProgramFd, ProgramId},
     sys::{bpf_get_object, bpf_pin_object, bpf_prog_attach, bpf_prog_detach, SyscallError},
 };
 
@@ -116,7 +119,7 @@ pub struct FdLinkId(pub(crate) RawFd);
 ///
 /// # Example
 ///
-///```no_run
+/// ```no_run
 /// # let mut bpf = Ebpf::load_file("ebpf_programs.o")?;
 /// use aya::{Ebpf, programs::{links::FdLink, KProbe}};
 ///
@@ -329,7 +332,7 @@ macro_rules! define_link_wrapper {
         pub struct $wrapper(Option<$base>);
 
         #[allow(dead_code)]
-        // allow dead code since currently XDP is the only consumer of inner and
+        // allow dead code since currently XDP/TC are the only consumers of inner and
         // into_inner
         impl $wrapper {
             fn new(base: $base) -> $wrapper {
@@ -394,6 +397,125 @@ pub enum LinkError {
     SyscallError(#[from] SyscallError),
 }
 
+#[derive(Debug)]
+pub(crate) enum LinkRef {
+    Id(u32),
+    Fd(RawFd),
+}
+
+bitflags::bitflags! {
+    /// Flags which are use to build a set of MprogOptions.
+    #[derive(Clone, Copy, Debug, Default)]
+    pub(crate) struct MprogFlags: u32 {
+        const REPLACE = BPF_F_REPLACE;
+        const BEFORE = BPF_F_BEFORE;
+        const AFTER = BPF_F_AFTER;
+        const ID = BPF_F_ID;
+        const LINK = BPF_F_LINK;
+    }
+}
+
+/// Arguments required for interacting with the kernel's multi-prog API.
+///
+/// # Minimum kernel version
+///
+/// The minimum kernel version required to use this feature is 6.6.0.
+///
+/// # Example
+///
+/// ```no_run
+/// # let mut bpf = aya::Ebpf::load(&[])?;
+/// use aya::programs::{tc, SchedClassifier, TcAttachType, tc::TcAttachOptions, LinkOrder};
+///
+/// let prog: &mut SchedClassifier = bpf.program_mut("redirect_ingress").unwrap().try_into()?;
+/// prog.load()?;
+/// let options = TcAttachOptions::TcxOrder(LinkOrder::first());
+/// prog.attach_with_options("eth0", TcAttachType::Ingress, options)?;
+///
+/// # Ok::<(), aya::EbpfError>(())
+/// ```
+#[derive(Debug)]
+pub struct LinkOrder {
+    pub(crate) link_ref: LinkRef,
+    pub(crate) flags: MprogFlags,
+}
+
+/// Ensure that default link ordering is to be attached last.
+impl Default for LinkOrder {
+    fn default() -> Self {
+        Self {
+            link_ref: LinkRef::Fd(0),
+            flags: MprogFlags::AFTER,
+        }
+    }
+}
+
+impl LinkOrder {
+    /// Attach before all other links.
+    pub fn first() -> Self {
+        Self {
+            link_ref: LinkRef::Id(0),
+            flags: MprogFlags::BEFORE,
+        }
+    }
+
+    /// Attach after all other links.
+    pub fn last() -> Self {
+        Self {
+            link_ref: LinkRef::Id(0),
+            flags: MprogFlags::AFTER,
+        }
+    }
+
+    /// Attach before the given link.
+    pub fn before_link<L: MultiProgLink>(link: &L) -> Result<Self, LinkError> {
+        Ok(Self {
+            link_ref: LinkRef::Fd(link.fd()?.as_raw_fd()),
+            flags: MprogFlags::BEFORE | MprogFlags::LINK,
+        })
+    }
+
+    /// Attach after the given link.
+    pub fn after_link<L: MultiProgLink>(link: &L) -> Result<Self, LinkError> {
+        Ok(Self {
+            link_ref: LinkRef::Fd(link.fd()?.as_raw_fd()),
+            flags: MprogFlags::AFTER | MprogFlags::LINK,
+        })
+    }
+
+    /// Attach before the given program.
+    pub fn before_program<P: MultiProgram>(program: &P) -> Result<Self, ProgramError> {
+        Ok(Self {
+            link_ref: LinkRef::Fd(program.fd()?.as_raw_fd()),
+            flags: MprogFlags::BEFORE,
+        })
+    }
+
+    /// Attach after the given program.
+    pub fn after_program<P: MultiProgram>(program: &P) -> Result<Self, ProgramError> {
+        Ok(Self {
+            link_ref: LinkRef::Fd(program.fd()?.as_raw_fd()),
+            flags: MprogFlags::AFTER,
+        })
+    }
+
+    /// Attach before the program with the given id.
+    pub fn before_program_id(id: ProgramId) -> Self {
+        Self {
+            link_ref: LinkRef::Id(id.0),
+            flags: MprogFlags::BEFORE | MprogFlags::ID,
+        }
+    }
+
+    /// Attach after the program with the given id.
+    pub fn after_program_id(id: ProgramId) -> Self {
+        Self {
+            link_ref: LinkRef::Id(id.0),
+            flags: MprogFlags::AFTER | MprogFlags::ID,
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::{cell::RefCell, fs::File, rc::Rc};
diff --git a/aya/src/programs/lirc_mode2.rs b/aya/src/programs/lirc_mode2.rs
index cc035d1c6..fecd261d1 100644
--- a/aya/src/programs/lirc_mode2.rs
+++ b/aya/src/programs/lirc_mode2.rs
@@ -7,7 +7,7 @@ use crate::{
         load_program, query, CgroupAttachMode, Link, ProgramData, ProgramError, ProgramFd,
         ProgramInfo,
     },
-    sys::{bpf_prog_attach, bpf_prog_detach, bpf_prog_get_fd_by_id},
+    sys::{bpf_prog_attach, bpf_prog_detach, bpf_prog_get_fd_by_id, ProgQueryTarget},
 };
 
 /// A program used to decode IR into key events for a lirc device.
@@ -100,7 +100,7 @@ impl LircMode2 {
     /// Queries the lirc device for attached programs.
     pub fn query<T: AsFd>(target_fd: T) -> Result<Vec<LircLink>, ProgramError> {
         let target_fd = target_fd.as_fd();
-        let prog_ids = query(target_fd, BPF_LIRC_MODE2, 0, &mut None)?;
+        let (_, prog_ids) = query(ProgQueryTarget::Fd(target_fd), BPF_LIRC_MODE2, 0, &mut None)?;
 
         prog_ids
             .into_iter()
diff --git a/aya/src/programs/mod.rs b/aya/src/programs/mod.rs
index fcca69fa8..00d61b01d 100644
--- a/aya/src/programs/mod.rs
+++ b/aya/src/programs/mod.rs
@@ -72,7 +72,7 @@ pub mod xdp;
 use std::{
     ffi::CString,
     io,
-    os::fd::{AsFd, AsRawFd, BorrowedFd},
+    os::fd::{AsFd, BorrowedFd},
     path::{Path, PathBuf},
     sync::Arc,
 };
@@ -80,6 +80,7 @@ use std::{
 use info::impl_info;
 pub use info::{loaded_programs, ProgramInfo, ProgramType};
 use libc::ENOSPC;
+use tc::SchedClassifierLink;
 use thiserror::Error;
 
 // re-export the main items needed to load and attach
@@ -94,7 +95,7 @@ pub use crate::programs::{
     fentry::FEntry,
     fexit::FExit,
     kprobe::{KProbe, KProbeError},
-    links::{CgroupAttachMode, Link},
+    links::{CgroupAttachMode, Link, LinkOrder},
     lirc_mode2::LircMode2,
     lsm::Lsm,
     perf_event::{PerfEvent, PerfEventScope, PerfTypeId, SamplePolicy},
@@ -120,7 +121,7 @@ use crate::{
     sys::{
         bpf_btf_get_fd_by_id, bpf_get_object, bpf_link_get_fd_by_id, bpf_link_get_info_by_fd,
         bpf_load_program, bpf_pin_object, bpf_prog_get_fd_by_id, bpf_prog_query, iter_link_ids,
-        retry_with_verifier_logs, EbpfLoadProgramAttrs, SyscallError,
+        retry_with_verifier_logs, EbpfLoadProgramAttrs, ProgQueryTarget, SyscallError,
     },
     util::KernelVersion,
     VerifierLogLevel,
@@ -238,7 +239,20 @@ impl AsFd for ProgramFd {
     }
 }
 
-/// The various eBPF programs.
+/// A [`Program`] identifier.
+pub struct ProgramId(u32);
+
+impl ProgramId {
+    /// Create a new program id.  
+    ///  
+    /// This method is unsafe since it doesn't check that the given `id` is a
+    /// valid program id.
+    pub unsafe fn new(id: u32) -> Self {
+        Self(id)
+    }
+}
+
+/// eBPF program type.
 #[derive(Debug)]
 pub enum Program {
     /// A [`KProbe`] program
@@ -668,28 +682,30 @@ fn load_program<T: Link>(
 }
 
 pub(crate) fn query(
-    target_fd: BorrowedFd<'_>,
+    target: ProgQueryTarget<'_>,
     attach_type: bpf_attach_type,
     query_flags: u32,
     attach_flags: &mut Option<u32>,
-) -> Result<Vec<u32>, ProgramError> {
+) -> Result<(u64, Vec<u32>), ProgramError> {
     let mut prog_ids = vec![0u32; 64];
     let mut prog_cnt = prog_ids.len() as u32;
+    let mut revision = 0;
 
     let mut retries = 0;
 
     loop {
         match bpf_prog_query(
-            target_fd.as_fd().as_raw_fd(),
+            &target,
             attach_type,
             query_flags,
             attach_flags.as_mut(),
             &mut prog_ids,
             &mut prog_cnt,
+            &mut revision,
         ) {
             Ok(_) => {
                 prog_ids.resize(prog_cnt as usize, 0);
-                return Ok(prog_ids);
+                return Ok((revision, prog_ids));
             }
             Err((_, io_error)) => {
                 if retries == 0 && io_error.raw_os_error() == Some(ENOSPC) {
@@ -797,6 +813,57 @@ impl_fd!(
     CgroupDevice,
 );
 
+/// Trait implemented by the [`Program`] types which support the kernel's
+/// [generic multi-prog API](https://github.com/torvalds/linux/commit/053c8e1f235dc3f69d13375b32f4209228e1cb96).
+///
+/// # Minimum kernel version
+///
+/// The minimum kernel version required to use this feature is 6.6.0.
+pub trait MultiProgram {
+    /// Borrows the file descriptor.
+    fn fd(&self) -> Result<BorrowedFd<'_>, ProgramError>;
+}
+
+macro_rules! impl_multiprog_fd {
+    ($($struct_name:ident),+ $(,)?) => {
+        $(
+            impl MultiProgram for $struct_name {
+                fn fd(&self) -> Result<BorrowedFd<'_>, ProgramError> {
+                    Ok(self.fd()?.as_fd())
+                }
+            }
+        )+
+    }
+}
+
+impl_multiprog_fd!(SchedClassifier);
+
+/// Trait implemented by the [`Link`] types which support the kernel's
+/// [generic multi-prog API](https://github.com/torvalds/linux/commit/053c8e1f235dc3f69d13375b32f4209228e1cb96).
+///
+/// # Minimum kernel version
+///
+/// The minimum kernel version required to use this feature is 6.6.0.
+pub trait MultiProgLink {
+    /// Borrows the file descriptor.
+    fn fd(&self) -> Result<BorrowedFd<'_>, LinkError>;
+}
+
+macro_rules! impl_multiproglink_fd {
+    ($($struct_name:ident),+ $(,)?) => {
+        $(
+            impl MultiProgLink for $struct_name {
+                fn fd(&self) -> Result<BorrowedFd<'_>, LinkError> {
+                    let link: &FdLink = self.try_into()?;
+                    Ok(link.fd.as_fd())
+                }
+            }
+        )+
+    }
+}
+
+impl_multiproglink_fd!(SchedClassifierLink);
+
 macro_rules! impl_program_pin{
     ($($struct_name:ident),+ $(,)?) => {
         $(
diff --git a/aya/src/programs/perf_attach.rs b/aya/src/programs/perf_attach.rs
index f58189448..89f68ace4 100644
--- a/aya/src/programs/perf_attach.rs
+++ b/aya/src/programs/perf_attach.rs
@@ -75,11 +75,18 @@ pub(crate) fn perf_attach(
     fd: crate::MockableFd,
 ) -> Result<PerfLinkInner, ProgramError> {
     if FEATURES.bpf_perf_link() {
-        let link_fd = bpf_link_create(prog_fd, LinkTarget::Fd(fd.as_fd()), BPF_PERF_EVENT, None, 0)
-            .map_err(|(_, io_error)| SyscallError {
-                call: "bpf_link_create",
-                io_error,
-            })?;
+        let link_fd = bpf_link_create(
+            prog_fd,
+            LinkTarget::Fd(fd.as_fd()),
+            BPF_PERF_EVENT,
+            None,
+            0,
+            None,
+        )
+        .map_err(|(_, io_error)| SyscallError {
+            call: "bpf_link_create",
+            io_error,
+        })?;
         Ok(PerfLinkInner::FdLink(FdLink::new(link_fd)))
     } else {
         perf_attach_either(prog_fd, fd, None)
diff --git a/aya/src/programs/sk_lookup.rs b/aya/src/programs/sk_lookup.rs
index dab0ebc77..065001ce0 100644
--- a/aya/src/programs/sk_lookup.rs
+++ b/aya/src/programs/sk_lookup.rs
@@ -65,11 +65,18 @@ impl SkLookup {
         let prog_fd = prog_fd.as_fd();
         let netns_fd = netns.as_fd();
 
-        let link_fd = bpf_link_create(prog_fd, LinkTarget::Fd(netns_fd), BPF_SK_LOOKUP, None, 0)
-            .map_err(|(_, io_error)| SyscallError {
-                call: "bpf_link_create",
-                io_error,
-            })?;
+        let link_fd = bpf_link_create(
+            prog_fd,
+            LinkTarget::Fd(netns_fd),
+            BPF_SK_LOOKUP,
+            None,
+            0,
+            None,
+        )
+        .map_err(|(_, io_error)| SyscallError {
+            call: "bpf_link_create",
+            io_error,
+        })?;
         self.data
             .links
             .insert(SkLookupLink::new(FdLink::new(link_fd)))
diff --git a/aya/src/programs/sock_ops.rs b/aya/src/programs/sock_ops.rs
index 60f4e08f0..62573215c 100644
--- a/aya/src/programs/sock_ops.rs
+++ b/aya/src/programs/sock_ops.rs
@@ -75,6 +75,7 @@ impl SockOps {
                 attach_type,
                 None,
                 mode.into(),
+                None,
             )
             .map_err(|(_, io_error)| SyscallError {
                 call: "bpf_link_create",
diff --git a/aya/src/programs/tc.rs b/aya/src/programs/tc.rs
index c2dd5a13a..9f55fe92b 100644
--- a/aya/src/programs/tc.rs
+++ b/aya/src/programs/tc.rs
@@ -8,16 +8,24 @@ use std::{
 
 use thiserror::Error;
 
+use super::{FdLink, ProgramInfo};
 use crate::{
     generated::{
-        bpf_prog_type::BPF_PROG_TYPE_SCHED_CLS, 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,
+        TC_H_CLSACT, TC_H_MIN_EGRESS, TC_H_MIN_INGRESS,
+    },
+    programs::{
+        define_link_wrapper, load_program, query, Link, LinkError, LinkOrder, ProgramData,
+        ProgramError,
     },
-    programs::{define_link_wrapper, load_program, Link, 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,
+        netlink_qdisc_detach, LinkTarget, ProgQueryTarget, SyscallError,
     },
-    util::{ifindex_from_ifname, tc_handler_make},
+    util::{ifindex_from_ifname, tc_handler_make, KernelVersion},
     VerifierLogLevel,
 };
 
@@ -89,21 +97,49 @@ pub enum TcError {
     /// 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 parent(&self) -> u32 {
+    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
-#[derive(Default)]
-pub struct TcOptions {
+/// 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,
@@ -118,25 +154,46 @@ impl SchedClassifier {
         load_program(BPF_PROG_TYPE_SCHED_CLS, &mut self.data)
     }
 
-    /// Attaches the program to the given `interface` using the default options.
+    /// 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
     ///
-    /// [`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`]
+    /// 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> {
-        self.attach_with_options(interface, attach_type, TcOptions::default())
+        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 [`TcOptions`].
+    /// Attaches the program to the given `interface` with options defined in [`TcAttachOptions`].
     ///
     /// The returned value can be used to detach, see [SchedClassifier::detach].
     ///
@@ -150,11 +207,11 @@ impl SchedClassifier {
         &mut self,
         interface: &str,
         attach_type: TcAttachType,
-        options: TcOptions,
+        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 as i32, attach_type, options, true)
+        self.do_attach(if_index, attach_type, options, true)
     }
 
     /// Atomically replaces the program referenced by the provided link.
@@ -164,46 +221,98 @@ impl SchedClassifier {
         &mut self,
         link: SchedClassifierLink,
     ) -> Result<SchedClassifierLinkId, ProgramError> {
-        let TcLink {
-            if_index,
-            attach_type,
-            priority,
-            handle,
-        } = link.into_inner();
-        self.do_attach(if_index, attach_type, TcOptions { priority, handle }, false)
+        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: i32,
+        if_index: u32,
         attach_type: TcAttachType,
-        options: TcOptions,
+        options: TcAttachOptions,
         create: bool,
     ) -> Result<SchedClassifierLinkId, ProgramError> {
         let prog_fd = self.fd()?;
         let prog_fd = prog_fd.as_fd();
-        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,
-                &attach_type,
-                prog_fd,
-                &name,
-                options.priority,
-                options.handle,
-                create,
-            )
-        }
-        .map_err(|io_error| TcError::NetlinkError { io_error })?;
 
-        self.data.links.insert(SchedClassifierLink::new(TcLink {
-            if_index,
-            attach_type,
-            priority,
-            handle,
-        }))
+        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,
+                    ))))
+            }
+        }
     }
 
     /// Detaches the program.
@@ -234,42 +343,153 @@ impl SchedClassifier {
         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 TcLinkId(i32, TcAttachType, u16, u32);
+pub(crate) struct NlLinkId(u32, TcAttachType, u16, u32);
 
 #[derive(Debug)]
-struct TcLink {
-    if_index: i32,
+pub(crate) struct NlLink {
+    if_index: u32,
     attach_type: TcAttachType,
     priority: u16,
     handle: u32,
 }
 
-impl Link for TcLink {
-    type Id = TcLinkId;
+impl Link for NlLink {
+    type Id = NlLinkId;
 
     fn id(&self) -> Self::Id {
-        TcLinkId(self.if_index, self.attach_type, self.priority, self.handle)
+        NlLinkId(self.if_index, self.attach_type, self.priority, self.handle)
     }
 
     fn detach(self) -> Result<(), ProgramError> {
         unsafe {
-            netlink_qdisc_detach(self.if_index, &self.attach_type, self.priority, self.handle)
+            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,
-    TcLink,
-    TcLinkId
+    TcLinkInner,
+    TcLinkIdInner
 );
 
 impl SchedClassifierLink {
@@ -311,27 +531,39 @@ impl SchedClassifierLink {
         handle: u32,
     ) -> Result<Self, io::Error> {
         let if_index = ifindex_from_ifname(if_name)?;
-        Ok(Self(Some(TcLink {
-            if_index: if_index as i32,
+        Ok(Self(Some(TcLinkInner::NlLink(NlLink {
+            if_index,
             attach_type,
             priority,
             handle,
-        })))
+        }))))
     }
 
     /// Returns the attach type.
-    pub fn attach_type(&self) -> TcAttachType {
-        self.inner().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) -> u16 {
-        self.inner().priority
+    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) -> u32 {
-        self.inner().handle
+    pub fn handle(&self) -> Result<u32, ProgramError> {
+        if let TcLinkInner::NlLink(n) = self.inner() {
+            Ok(n.handle)
+        } else {
+            Err(TcError::InvalidLinkOperation.into())
+        }
     }
 }
 
diff --git a/aya/src/programs/xdp.rs b/aya/src/programs/xdp.rs
index 5d49f4f11..66b32b324 100644
--- a/aya/src/programs/xdp.rs
+++ b/aya/src/programs/xdp.rs
@@ -140,7 +140,7 @@ impl Xdp {
             // if the program has been loaded, i.e. there is an fd. We get one by:
             // - Using `Xdp::from_pin` that sets `expected_attach_type`
             // - Calling `Xdp::attach` that sets `expected_attach_type`, as geting an `Xdp`
-            //   instance trhough `Xdp:try_from(Program)` does not set any fd.
+            //   instance through `Xdp:try_from(Program)` does not set any fd.
             // So, in all cases where we have an fd, we have an expected_attach_type. Thus, if we
             // reach this point, expected_attach_type is guaranteed to be Some(_).
             let attach_type = self.data.expected_attach_type.unwrap();
@@ -150,6 +150,7 @@ impl Xdp {
                 attach_type,
                 None,
                 flags.bits(),
+                None,
             )
             .map_err(|(_, io_error)| SyscallError {
                 call: "bpf_link_create",
diff --git a/aya/src/sys/bpf.rs b/aya/src/sys/bpf.rs
index 2d3052460..a405b3bee 100644
--- a/aya/src/sys/bpf.rs
+++ b/aya/src/sys/bpf.rs
@@ -30,6 +30,7 @@ use crate::{
         },
         copy_instructions,
     },
+    programs::links::LinkRef,
     sys::{syscall, SysResult, Syscall, SyscallError},
     util::KernelVersion,
     Btf, Pod, VerifierLogLevel, BPF_OBJ_NAME_LEN, FEATURES,
@@ -385,6 +386,7 @@ pub(crate) fn bpf_link_create(
     attach_type: bpf_attach_type,
     btf_id: Option<u32>,
     flags: u32,
+    link_ref: Option<&LinkRef>,
 ) -> SysResult<crate::MockableFd> {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
 
@@ -399,11 +401,32 @@ pub(crate) fn bpf_link_create(
         }
     };
     attr.link_create.attach_type = attach_type as u32;
-    attr.link_create.flags = flags;
+
     if let Some(btf_id) = btf_id {
         attr.link_create.__bindgen_anon_3.target_btf_id = btf_id;
     }
 
+    attr.link_create.flags = flags;
+
+    // since kernel 6.6
+    match link_ref {
+        Some(LinkRef::Fd(fd)) => {
+            attr.link_create
+                .__bindgen_anon_3
+                .tcx
+                .__bindgen_anon_1
+                .relative_fd = fd.to_owned() as u32;
+        }
+        Some(LinkRef::Id(id)) => {
+            attr.link_create
+                .__bindgen_anon_3
+                .tcx
+                .__bindgen_anon_1
+                .relative_id = id.to_owned();
+        }
+        None => {}
+    };
+
     // SAFETY: BPF_LINK_CREATE returns a new file descriptor.
     unsafe { fd_sys_bpf(bpf_cmd::BPF_LINK_CREATE, &mut attr) }
 }
@@ -475,25 +498,39 @@ pub(crate) fn bpf_prog_detach(
     Ok(())
 }
 
+#[derive(Debug)]
+pub(crate) enum ProgQueryTarget<'a> {
+    Fd(BorrowedFd<'a>),
+    IfIndex(u32),
+}
+
 pub(crate) fn bpf_prog_query(
-    target_fd: RawFd,
+    target: &ProgQueryTarget<'_>,
     attach_type: bpf_attach_type,
     query_flags: u32,
     attach_flags: Option<&mut u32>,
     prog_ids: &mut [u32],
     prog_cnt: &mut u32,
+    revision: &mut u64,
 ) -> SysResult<i64> {
     let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
 
-    attr.query.__bindgen_anon_1.target_fd = target_fd as u32;
+    match target {
+        ProgQueryTarget::Fd(fd) => {
+            attr.query.__bindgen_anon_1.target_fd = fd.as_raw_fd() as u32;
+        }
+        ProgQueryTarget::IfIndex(ifindex) => {
+            attr.query.__bindgen_anon_1.target_ifindex = *ifindex;
+        }
+    }
     attr.query.attach_type = attach_type as u32;
     attr.query.query_flags = query_flags;
     attr.query.__bindgen_anon_2.prog_cnt = prog_ids.len() as u32;
     attr.query.prog_ids = prog_ids.as_mut_ptr() as u64;
-
     let ret = sys_bpf(bpf_cmd::BPF_PROG_QUERY, &mut attr);
 
     *prog_cnt = unsafe { attr.query.__bindgen_anon_2.prog_cnt };
+    *revision = unsafe { attr.query.revision };
 
     if let Some(attach_flags) = attach_flags {
         *attach_flags = unsafe { attr.query.attach_flags };
@@ -826,7 +863,7 @@ pub(crate) fn is_perf_link_supported() -> bool {
         let fd = fd.as_fd();
         matches!(
             // Uses an invalid target FD so we get EBADF if supported.
-            bpf_link_create(fd, LinkTarget::IfIndex(u32::MAX), bpf_attach_type::BPF_PERF_EVENT, None, 0),
+            bpf_link_create(fd, LinkTarget::IfIndex(u32::MAX), bpf_attach_type::BPF_PERF_EVENT, None, 0, None),
             // Returns EINVAL if unsupported. EBADF if supported.
             Err((_, e)) if e.raw_os_error() == Some(libc::EBADF),
         )
diff --git a/aya/src/sys/netlink.rs b/aya/src/sys/netlink.rs
index 522155552..11b99471d 100644
--- a/aya/src/sys/netlink.rs
+++ b/aya/src/sys/netlink.rs
@@ -146,7 +146,7 @@ pub(crate) unsafe fn netlink_qdisc_attach(
     req.tc_info.tcm_family = AF_UNSPEC as u8;
     req.tc_info.tcm_handle = handle; // auto-assigned, if zero
     req.tc_info.tcm_ifindex = if_index;
-    req.tc_info.tcm_parent = attach_type.parent();
+    req.tc_info.tcm_parent = attach_type.tc_parent();
     req.tc_info.tcm_info = tc_handler_make((priority as u32) << 16, htons(ETH_P_ALL as u16) as u32);
 
     let attrs_buf = request_attributes(&mut req, nlmsg_len);
@@ -207,7 +207,7 @@ pub(crate) unsafe fn netlink_qdisc_detach(
     req.tc_info.tcm_family = AF_UNSPEC as u8;
     req.tc_info.tcm_handle = handle; // auto-assigned, if zero
     req.tc_info.tcm_info = tc_handler_make((priority as u32) << 16, htons(ETH_P_ALL as u16) as u32);
-    req.tc_info.tcm_parent = attach_type.parent();
+    req.tc_info.tcm_parent = attach_type.tc_parent();
     req.tc_info.tcm_ifindex = if_index;
 
     sock.send(&bytes_of(&req)[..req.header.nlmsg_len as usize])?;
@@ -236,7 +236,7 @@ pub(crate) unsafe fn netlink_find_filter_with_name(
     req.tc_info.tcm_family = AF_UNSPEC as u8;
     req.tc_info.tcm_handle = 0; // auto-assigned, if zero
     req.tc_info.tcm_ifindex = if_index;
-    req.tc_info.tcm_parent = attach_type.parent();
+    req.tc_info.tcm_parent = attach_type.tc_parent();
 
     let sock = NetlinkSocket::open()?;
     sock.send(&bytes_of(&req)[..req.header.nlmsg_len as usize])?;
diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt
index 52ebb6bbc..3d5c5874a 100644
--- a/xtask/public-api/aya.txt
+++ b/xtask/public-api/aya.txt
@@ -3907,6 +3907,9 @@ pub fn aya::programs::kprobe::KProbeLink::try_from(fd_link: aya::programs::links
 impl core::convert::TryFrom<aya::programs::links::FdLink> for aya::programs::perf_event::PerfEventLink
 pub type aya::programs::perf_event::PerfEventLink::Error = aya::programs::links::LinkError
 pub fn aya::programs::perf_event::PerfEventLink::try_from(fd_link: aya::programs::links::FdLink) -> core::result::Result<Self, Self::Error>
+impl core::convert::TryFrom<aya::programs::links::FdLink> for aya::programs::tc::SchedClassifierLink
+pub type aya::programs::tc::SchedClassifierLink::Error = aya::programs::links::LinkError
+pub fn aya::programs::tc::SchedClassifierLink::try_from(fd_link: aya::programs::links::FdLink) -> core::result::Result<Self, Self::Error>
 impl core::convert::TryFrom<aya::programs::links::FdLink> for aya::programs::trace_point::TracePointLink
 pub type aya::programs::trace_point::TracePointLink::Error = aya::programs::links::LinkError
 pub fn aya::programs::trace_point::TracePointLink::try_from(fd_link: aya::programs::links::FdLink) -> core::result::Result<Self, Self::Error>
@@ -3919,6 +3922,9 @@ pub fn aya::programs::xdp::XdpLink::try_from(fd_link: aya::programs::links::FdLi
 impl core::convert::TryFrom<aya::programs::perf_event::PerfEventLink> for aya::programs::links::FdLink
 pub type aya::programs::links::FdLink::Error = aya::programs::links::LinkError
 pub fn aya::programs::links::FdLink::try_from(value: aya::programs::perf_event::PerfEventLink) -> core::result::Result<Self, Self::Error>
+impl core::convert::TryFrom<aya::programs::tc::SchedClassifierLink> for aya::programs::links::FdLink
+pub type aya::programs::links::FdLink::Error = aya::programs::links::LinkError
+pub fn aya::programs::links::FdLink::try_from(value: aya::programs::tc::SchedClassifierLink) -> core::result::Result<Self, Self::Error>
 impl core::convert::TryFrom<aya::programs::trace_point::TracePointLink> for aya::programs::links::FdLink
 pub type aya::programs::links::FdLink::Error = aya::programs::links::LinkError
 pub fn aya::programs::links::FdLink::try_from(value: aya::programs::trace_point::TracePointLink) -> core::result::Result<Self, Self::Error>
@@ -3930,6 +3936,9 @@ pub type aya::programs::links::FdLink::Error = aya::programs::links::LinkError
 pub fn aya::programs::links::FdLink::try_from(value: aya::programs::xdp::XdpLink) -> core::result::Result<Self, Self::Error>
 impl core::fmt::Debug for aya::programs::links::FdLink
 pub fn aya::programs::links::FdLink::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl<'a> core::convert::TryFrom<&'a aya::programs::tc::SchedClassifierLink> for &'a aya::programs::links::FdLink
+pub type &'a aya::programs::links::FdLink::Error = aya::programs::links::LinkError
+pub fn &'a aya::programs::links::FdLink::try_from(value: &'a aya::programs::tc::SchedClassifierLink) -> core::result::Result<Self, Self::Error>
 impl core::marker::Freeze for aya::programs::links::FdLink
 impl core::marker::Send for aya::programs::links::FdLink
 impl core::marker::Sync for aya::programs::links::FdLink
@@ -3985,6 +3994,42 @@ impl<T> core::borrow::BorrowMut<T> for aya::programs::links::FdLinkId where T: c
 pub fn aya::programs::links::FdLinkId::borrow_mut(&mut self) -> &mut T
 impl<T> core::convert::From<T> for aya::programs::links::FdLinkId
 pub fn aya::programs::links::FdLinkId::from(t: T) -> T
+pub struct aya::programs::links::LinkOrder
+impl aya::programs::links::LinkOrder
+pub fn aya::programs::links::LinkOrder::after_link<L: aya::programs::MultiProgLink>(link: &L) -> core::result::Result<Self, aya::programs::links::LinkError>
+pub fn aya::programs::links::LinkOrder::after_program<P: aya::programs::MultiProgram>(program: &P) -> core::result::Result<Self, aya::programs::ProgramError>
+pub fn aya::programs::links::LinkOrder::after_program_id(id: aya::programs::ProgramId) -> Self
+pub fn aya::programs::links::LinkOrder::before_link<L: aya::programs::MultiProgLink>(link: &L) -> core::result::Result<Self, aya::programs::links::LinkError>
+pub fn aya::programs::links::LinkOrder::before_program<P: aya::programs::MultiProgram>(program: &P) -> core::result::Result<Self, aya::programs::ProgramError>
+pub fn aya::programs::links::LinkOrder::before_program_id(id: aya::programs::ProgramId) -> Self
+pub fn aya::programs::links::LinkOrder::first() -> Self
+pub fn aya::programs::links::LinkOrder::last() -> Self
+impl core::default::Default for aya::programs::links::LinkOrder
+pub fn aya::programs::links::LinkOrder::default() -> Self
+impl core::fmt::Debug for aya::programs::links::LinkOrder
+pub fn aya::programs::links::LinkOrder::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::marker::Freeze for aya::programs::links::LinkOrder
+impl core::marker::Send for aya::programs::links::LinkOrder
+impl core::marker::Sync for aya::programs::links::LinkOrder
+impl core::marker::Unpin for aya::programs::links::LinkOrder
+impl core::panic::unwind_safe::RefUnwindSafe for aya::programs::links::LinkOrder
+impl core::panic::unwind_safe::UnwindSafe for aya::programs::links::LinkOrder
+impl<T, U> core::convert::Into<U> for aya::programs::links::LinkOrder where U: core::convert::From<T>
+pub fn aya::programs::links::LinkOrder::into(self) -> U
+impl<T, U> core::convert::TryFrom<U> for aya::programs::links::LinkOrder where U: core::convert::Into<T>
+pub type aya::programs::links::LinkOrder::Error = core::convert::Infallible
+pub fn aya::programs::links::LinkOrder::try_from(value: U) -> core::result::Result<T, <T as core::convert::TryFrom<U>>::Error>
+impl<T, U> core::convert::TryInto<U> for aya::programs::links::LinkOrder where U: core::convert::TryFrom<T>
+pub type aya::programs::links::LinkOrder::Error = <U as core::convert::TryFrom<T>>::Error
+pub fn aya::programs::links::LinkOrder::try_into(self) -> core::result::Result<U, <U as core::convert::TryFrom<T>>::Error>
+impl<T> core::any::Any for aya::programs::links::LinkOrder where T: 'static + core::marker::Sized
+pub fn aya::programs::links::LinkOrder::type_id(&self) -> core::any::TypeId
+impl<T> core::borrow::Borrow<T> for aya::programs::links::LinkOrder where T: core::marker::Sized
+pub fn aya::programs::links::LinkOrder::borrow(&self) -> &T
+impl<T> core::borrow::BorrowMut<T> for aya::programs::links::LinkOrder where T: core::marker::Sized
+pub fn aya::programs::links::LinkOrder::borrow_mut(&mut self) -> &mut T
+impl<T> core::convert::From<T> for aya::programs::links::LinkOrder
+pub fn aya::programs::links::LinkOrder::from(t: T) -> T
 pub struct aya::programs::links::PinnedLink
 impl aya::programs::links::PinnedLink
 pub fn aya::programs::links::PinnedLink::from_pin<P: core::convert::AsRef<std::path::Path>>(path: P) -> core::result::Result<Self, aya::programs::links::LinkError>
@@ -5504,6 +5549,33 @@ pub fn aya::programs::socket_filter::SocketFilterLinkId::borrow_mut(&mut self) -
 impl<T> core::convert::From<T> for aya::programs::socket_filter::SocketFilterLinkId
 pub fn aya::programs::socket_filter::SocketFilterLinkId::from(t: T) -> T
 pub mod aya::programs::tc
+pub enum aya::programs::tc::TcAttachOptions
+pub aya::programs::tc::TcAttachOptions::Netlink(aya::programs::tc::NlOptions)
+pub aya::programs::tc::TcAttachOptions::TcxOrder(aya::programs::links::LinkOrder)
+impl core::fmt::Debug for aya::programs::tc::TcAttachOptions
+pub fn aya::programs::tc::TcAttachOptions::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::marker::Freeze for aya::programs::tc::TcAttachOptions
+impl core::marker::Send for aya::programs::tc::TcAttachOptions
+impl core::marker::Sync for aya::programs::tc::TcAttachOptions
+impl core::marker::Unpin for aya::programs::tc::TcAttachOptions
+impl core::panic::unwind_safe::RefUnwindSafe for aya::programs::tc::TcAttachOptions
+impl core::panic::unwind_safe::UnwindSafe for aya::programs::tc::TcAttachOptions
+impl<T, U> core::convert::Into<U> for aya::programs::tc::TcAttachOptions where U: core::convert::From<T>
+pub fn aya::programs::tc::TcAttachOptions::into(self) -> U
+impl<T, U> core::convert::TryFrom<U> for aya::programs::tc::TcAttachOptions where U: core::convert::Into<T>
+pub type aya::programs::tc::TcAttachOptions::Error = core::convert::Infallible
+pub fn aya::programs::tc::TcAttachOptions::try_from(value: U) -> core::result::Result<T, <T as core::convert::TryFrom<U>>::Error>
+impl<T, U> core::convert::TryInto<U> for aya::programs::tc::TcAttachOptions where U: core::convert::TryFrom<T>
+pub type aya::programs::tc::TcAttachOptions::Error = <U as core::convert::TryFrom<T>>::Error
+pub fn aya::programs::tc::TcAttachOptions::try_into(self) -> core::result::Result<U, <U as core::convert::TryFrom<T>>::Error>
+impl<T> core::any::Any for aya::programs::tc::TcAttachOptions where T: 'static + core::marker::Sized
+pub fn aya::programs::tc::TcAttachOptions::type_id(&self) -> core::any::TypeId
+impl<T> core::borrow::Borrow<T> for aya::programs::tc::TcAttachOptions where T: core::marker::Sized
+pub fn aya::programs::tc::TcAttachOptions::borrow(&self) -> &T
+impl<T> core::borrow::BorrowMut<T> for aya::programs::tc::TcAttachOptions where T: core::marker::Sized
+pub fn aya::programs::tc::TcAttachOptions::borrow_mut(&mut self) -> &mut T
+impl<T> core::convert::From<T> for aya::programs::tc::TcAttachOptions
+pub fn aya::programs::tc::TcAttachOptions::from(t: T) -> T
 pub enum aya::programs::tc::TcAttachType
 pub aya::programs::tc::TcAttachType::Custom(u32)
 pub aya::programs::tc::TcAttachType::Egress
@@ -5551,6 +5623,8 @@ impl<T> core::convert::From<T> for aya::programs::tc::TcAttachType
 pub fn aya::programs::tc::TcAttachType::from(t: T) -> T
 pub enum aya::programs::tc::TcError
 pub aya::programs::tc::TcError::AlreadyAttached
+pub aya::programs::tc::TcError::InvalidLinkOperation
+pub aya::programs::tc::TcError::InvalidTcxAttach(u32)
 pub aya::programs::tc::TcError::NetlinkError
 pub aya::programs::tc::TcError::NetlinkError::io_error: std::io::error::Error
 impl core::convert::From<aya::programs::tc::TcError> for aya::programs::ProgramError
@@ -5585,14 +5659,52 @@ impl<T> core::borrow::BorrowMut<T> for aya::programs::tc::TcError where T: core:
 pub fn aya::programs::tc::TcError::borrow_mut(&mut self) -> &mut T
 impl<T> core::convert::From<T> for aya::programs::tc::TcError
 pub fn aya::programs::tc::TcError::from(t: T) -> T
+pub struct aya::programs::tc::NlOptions
+pub aya::programs::tc::NlOptions::handle: u32
+pub aya::programs::tc::NlOptions::priority: u16
+impl core::cmp::Eq for aya::programs::tc::NlOptions
+impl core::cmp::PartialEq for aya::programs::tc::NlOptions
+pub fn aya::programs::tc::NlOptions::eq(&self, other: &aya::programs::tc::NlOptions) -> bool
+impl core::default::Default for aya::programs::tc::NlOptions
+pub fn aya::programs::tc::NlOptions::default() -> aya::programs::tc::NlOptions
+impl core::fmt::Debug for aya::programs::tc::NlOptions
+pub fn aya::programs::tc::NlOptions::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::hash::Hash for aya::programs::tc::NlOptions
+pub fn aya::programs::tc::NlOptions::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
+impl core::marker::StructuralPartialEq for aya::programs::tc::NlOptions
+impl core::marker::Freeze for aya::programs::tc::NlOptions
+impl core::marker::Send for aya::programs::tc::NlOptions
+impl core::marker::Sync for aya::programs::tc::NlOptions
+impl core::marker::Unpin for aya::programs::tc::NlOptions
+impl core::panic::unwind_safe::RefUnwindSafe for aya::programs::tc::NlOptions
+impl core::panic::unwind_safe::UnwindSafe for aya::programs::tc::NlOptions
+impl<Q, K> equivalent::Equivalent<K> for aya::programs::tc::NlOptions where Q: core::cmp::Eq + core::marker::Sized, K: core::borrow::Borrow<Q> + core::marker::Sized
+pub fn aya::programs::tc::NlOptions::equivalent(&self, key: &K) -> bool
+impl<T, U> core::convert::Into<U> for aya::programs::tc::NlOptions where U: core::convert::From<T>
+pub fn aya::programs::tc::NlOptions::into(self) -> U
+impl<T, U> core::convert::TryFrom<U> for aya::programs::tc::NlOptions where U: core::convert::Into<T>
+pub type aya::programs::tc::NlOptions::Error = core::convert::Infallible
+pub fn aya::programs::tc::NlOptions::try_from(value: U) -> core::result::Result<T, <T as core::convert::TryFrom<U>>::Error>
+impl<T, U> core::convert::TryInto<U> for aya::programs::tc::NlOptions where U: core::convert::TryFrom<T>
+pub type aya::programs::tc::NlOptions::Error = <U as core::convert::TryFrom<T>>::Error
+pub fn aya::programs::tc::NlOptions::try_into(self) -> core::result::Result<U, <U as core::convert::TryFrom<T>>::Error>
+impl<T> core::any::Any for aya::programs::tc::NlOptions where T: 'static + core::marker::Sized
+pub fn aya::programs::tc::NlOptions::type_id(&self) -> core::any::TypeId
+impl<T> core::borrow::Borrow<T> for aya::programs::tc::NlOptions where T: core::marker::Sized
+pub fn aya::programs::tc::NlOptions::borrow(&self) -> &T
+impl<T> core::borrow::BorrowMut<T> for aya::programs::tc::NlOptions where T: core::marker::Sized
+pub fn aya::programs::tc::NlOptions::borrow_mut(&mut self) -> &mut T
+impl<T> core::convert::From<T> for aya::programs::tc::NlOptions
+pub fn aya::programs::tc::NlOptions::from(t: T) -> T
 pub struct aya::programs::tc::SchedClassifier
 impl aya::programs::tc::SchedClassifier
 pub fn aya::programs::tc::SchedClassifier::attach(&mut self, interface: &str, attach_type: aya::programs::tc::TcAttachType) -> core::result::Result<aya::programs::tc::SchedClassifierLinkId, aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifier::attach_to_link(&mut self, link: aya::programs::tc::SchedClassifierLink) -> core::result::Result<aya::programs::tc::SchedClassifierLinkId, aya::programs::ProgramError>
-pub fn aya::programs::tc::SchedClassifier::attach_with_options(&mut self, interface: &str, attach_type: aya::programs::tc::TcAttachType, options: aya::programs::tc::TcOptions) -> core::result::Result<aya::programs::tc::SchedClassifierLinkId, aya::programs::ProgramError>
+pub fn aya::programs::tc::SchedClassifier::attach_with_options(&mut self, interface: &str, attach_type: aya::programs::tc::TcAttachType, options: aya::programs::tc::TcAttachOptions) -> core::result::Result<aya::programs::tc::SchedClassifierLinkId, aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifier::detach(&mut self, link_id: aya::programs::tc::SchedClassifierLinkId) -> core::result::Result<(), aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifier::from_pin<P: core::convert::AsRef<std::path::Path>>(path: P) -> core::result::Result<Self, aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifier::load(&mut self) -> core::result::Result<(), aya::programs::ProgramError>
+pub fn aya::programs::tc::SchedClassifier::query_tcx(interface: &str, attach_type: aya::programs::tc::TcAttachType) -> core::result::Result<(u64, alloc::vec::Vec<aya::programs::ProgramInfo>), aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifier::take_link(&mut self, link_id: aya::programs::tc::SchedClassifierLinkId) -> core::result::Result<aya::programs::tc::SchedClassifierLink, aya::programs::ProgramError>
 impl aya::programs::tc::SchedClassifier
 pub fn aya::programs::tc::SchedClassifier::fd(&self) -> core::result::Result<&aya::programs::ProgramFd, aya::programs::ProgramError>
@@ -5603,6 +5715,8 @@ pub fn aya::programs::tc::SchedClassifier::pin<P: core::convert::AsRef<std::path
 pub fn aya::programs::tc::SchedClassifier::unpin(self) -> core::result::Result<(), std::io::error::Error>
 impl aya::programs::tc::SchedClassifier
 pub fn aya::programs::tc::SchedClassifier::unload(&mut self) -> core::result::Result<(), aya::programs::ProgramError>
+impl aya::programs::MultiProgram for aya::programs::tc::SchedClassifier
+pub fn aya::programs::tc::SchedClassifier::fd(&self) -> core::result::Result<std::os::fd::owned::BorrowedFd<'_>, aya::programs::ProgramError>
 impl core::fmt::Debug for aya::programs::tc::SchedClassifier
 pub fn aya::programs::tc::SchedClassifier::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
 impl core::ops::drop::Drop for aya::programs::tc::SchedClassifier
@@ -5637,18 +5751,29 @@ impl<T> core::convert::From<T> for aya::programs::tc::SchedClassifier
 pub fn aya::programs::tc::SchedClassifier::from(t: T) -> T
 pub struct aya::programs::tc::SchedClassifierLink(_)
 impl aya::programs::tc::SchedClassifierLink
-pub fn aya::programs::tc::SchedClassifierLink::attach_type(&self) -> aya::programs::tc::TcAttachType
+pub fn aya::programs::tc::SchedClassifierLink::attach_type(&self) -> core::result::Result<aya::programs::tc::TcAttachType, aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifierLink::attached(if_name: &str, attach_type: aya::programs::tc::TcAttachType, priority: u16, handle: u32) -> core::result::Result<Self, std::io::error::Error>
-pub fn aya::programs::tc::SchedClassifierLink::handle(&self) -> u32
-pub fn aya::programs::tc::SchedClassifierLink::priority(&self) -> u16
+pub fn aya::programs::tc::SchedClassifierLink::handle(&self) -> core::result::Result<u32, aya::programs::ProgramError>
+pub fn aya::programs::tc::SchedClassifierLink::priority(&self) -> core::result::Result<u16, aya::programs::ProgramError>
+impl aya::programs::MultiProgLink for aya::programs::tc::SchedClassifierLink
+pub fn aya::programs::tc::SchedClassifierLink::fd(&self) -> core::result::Result<std::os::fd::owned::BorrowedFd<'_>, aya::programs::links::LinkError>
 impl aya::programs::links::Link for aya::programs::tc::SchedClassifierLink
 pub type aya::programs::tc::SchedClassifierLink::Id = aya::programs::tc::SchedClassifierLinkId
 pub fn aya::programs::tc::SchedClassifierLink::detach(self) -> core::result::Result<(), aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifierLink::id(&self) -> Self::Id
+impl core::convert::TryFrom<aya::programs::links::FdLink> for aya::programs::tc::SchedClassifierLink
+pub type aya::programs::tc::SchedClassifierLink::Error = aya::programs::links::LinkError
+pub fn aya::programs::tc::SchedClassifierLink::try_from(fd_link: aya::programs::links::FdLink) -> core::result::Result<Self, Self::Error>
+impl core::convert::TryFrom<aya::programs::tc::SchedClassifierLink> for aya::programs::links::FdLink
+pub type aya::programs::links::FdLink::Error = aya::programs::links::LinkError
+pub fn aya::programs::links::FdLink::try_from(value: aya::programs::tc::SchedClassifierLink) -> core::result::Result<Self, Self::Error>
 impl core::fmt::Debug for aya::programs::tc::SchedClassifierLink
 pub fn aya::programs::tc::SchedClassifierLink::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
 impl core::ops::drop::Drop for aya::programs::tc::SchedClassifierLink
 pub fn aya::programs::tc::SchedClassifierLink::drop(&mut self)
+impl<'a> core::convert::TryFrom<&'a aya::programs::tc::SchedClassifierLink> for &'a aya::programs::links::FdLink
+pub type &'a aya::programs::links::FdLink::Error = aya::programs::links::LinkError
+pub fn &'a aya::programs::links::FdLink::try_from(value: &'a aya::programs::tc::SchedClassifierLink) -> core::result::Result<Self, Self::Error>
 impl core::marker::Freeze for aya::programs::tc::SchedClassifierLink
 impl core::marker::Send for aya::programs::tc::SchedClassifierLink
 impl core::marker::Sync for aya::programs::tc::SchedClassifierLink
@@ -5704,33 +5829,6 @@ impl<T> core::borrow::BorrowMut<T> for aya::programs::tc::SchedClassifierLinkId
 pub fn aya::programs::tc::SchedClassifierLinkId::borrow_mut(&mut self) -> &mut T
 impl<T> core::convert::From<T> for aya::programs::tc::SchedClassifierLinkId
 pub fn aya::programs::tc::SchedClassifierLinkId::from(t: T) -> T
-pub struct aya::programs::tc::TcOptions
-pub aya::programs::tc::TcOptions::handle: u32
-pub aya::programs::tc::TcOptions::priority: u16
-impl core::default::Default for aya::programs::tc::TcOptions
-pub fn aya::programs::tc::TcOptions::default() -> aya::programs::tc::TcOptions
-impl core::marker::Freeze for aya::programs::tc::TcOptions
-impl core::marker::Send for aya::programs::tc::TcOptions
-impl core::marker::Sync for aya::programs::tc::TcOptions
-impl core::marker::Unpin for aya::programs::tc::TcOptions
-impl core::panic::unwind_safe::RefUnwindSafe for aya::programs::tc::TcOptions
-impl core::panic::unwind_safe::UnwindSafe for aya::programs::tc::TcOptions
-impl<T, U> core::convert::Into<U> for aya::programs::tc::TcOptions where U: core::convert::From<T>
-pub fn aya::programs::tc::TcOptions::into(self) -> U
-impl<T, U> core::convert::TryFrom<U> for aya::programs::tc::TcOptions where U: core::convert::Into<T>
-pub type aya::programs::tc::TcOptions::Error = core::convert::Infallible
-pub fn aya::programs::tc::TcOptions::try_from(value: U) -> core::result::Result<T, <T as core::convert::TryFrom<U>>::Error>
-impl<T, U> core::convert::TryInto<U> for aya::programs::tc::TcOptions where U: core::convert::TryFrom<T>
-pub type aya::programs::tc::TcOptions::Error = <U as core::convert::TryFrom<T>>::Error
-pub fn aya::programs::tc::TcOptions::try_into(self) -> core::result::Result<U, <U as core::convert::TryFrom<T>>::Error>
-impl<T> core::any::Any for aya::programs::tc::TcOptions where T: 'static + core::marker::Sized
-pub fn aya::programs::tc::TcOptions::type_id(&self) -> core::any::TypeId
-impl<T> core::borrow::Borrow<T> for aya::programs::tc::TcOptions where T: core::marker::Sized
-pub fn aya::programs::tc::TcOptions::borrow(&self) -> &T
-impl<T> core::borrow::BorrowMut<T> for aya::programs::tc::TcOptions where T: core::marker::Sized
-pub fn aya::programs::tc::TcOptions::borrow_mut(&mut self) -> &mut T
-impl<T> core::convert::From<T> for aya::programs::tc::TcOptions
-pub fn aya::programs::tc::TcOptions::from(t: T) -> T
 pub fn aya::programs::tc::qdisc_add_clsact(if_name: &str) -> core::result::Result<(), std::io::error::Error>
 pub fn aya::programs::tc::qdisc_detach_program(if_name: &str, attach_type: aya::programs::tc::TcAttachType, name: &str) -> core::result::Result<(), std::io::error::Error>
 pub mod aya::programs::tp_btf
@@ -7208,6 +7306,8 @@ impl<T> core::convert::From<T> for aya::programs::tc::TcAttachType
 pub fn aya::programs::tc::TcAttachType::from(t: T) -> T
 pub enum aya::programs::TcError
 pub aya::programs::TcError::AlreadyAttached
+pub aya::programs::TcError::InvalidLinkOperation
+pub aya::programs::TcError::InvalidTcxAttach(u32)
 pub aya::programs::TcError::NetlinkError
 pub aya::programs::TcError::NetlinkError::io_error: std::io::error::Error
 impl core::convert::From<aya::programs::tc::TcError> for aya::programs::ProgramError
@@ -7894,6 +7994,42 @@ impl<T> core::borrow::BorrowMut<T> for aya::programs::kprobe::KProbe where T: co
 pub fn aya::programs::kprobe::KProbe::borrow_mut(&mut self) -> &mut T
 impl<T> core::convert::From<T> for aya::programs::kprobe::KProbe
 pub fn aya::programs::kprobe::KProbe::from(t: T) -> T
+pub struct aya::programs::LinkOrder
+impl aya::programs::links::LinkOrder
+pub fn aya::programs::links::LinkOrder::after_link<L: aya::programs::MultiProgLink>(link: &L) -> core::result::Result<Self, aya::programs::links::LinkError>
+pub fn aya::programs::links::LinkOrder::after_program<P: aya::programs::MultiProgram>(program: &P) -> core::result::Result<Self, aya::programs::ProgramError>
+pub fn aya::programs::links::LinkOrder::after_program_id(id: aya::programs::ProgramId) -> Self
+pub fn aya::programs::links::LinkOrder::before_link<L: aya::programs::MultiProgLink>(link: &L) -> core::result::Result<Self, aya::programs::links::LinkError>
+pub fn aya::programs::links::LinkOrder::before_program<P: aya::programs::MultiProgram>(program: &P) -> core::result::Result<Self, aya::programs::ProgramError>
+pub fn aya::programs::links::LinkOrder::before_program_id(id: aya::programs::ProgramId) -> Self
+pub fn aya::programs::links::LinkOrder::first() -> Self
+pub fn aya::programs::links::LinkOrder::last() -> Self
+impl core::default::Default for aya::programs::links::LinkOrder
+pub fn aya::programs::links::LinkOrder::default() -> Self
+impl core::fmt::Debug for aya::programs::links::LinkOrder
+pub fn aya::programs::links::LinkOrder::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::marker::Freeze for aya::programs::links::LinkOrder
+impl core::marker::Send for aya::programs::links::LinkOrder
+impl core::marker::Sync for aya::programs::links::LinkOrder
+impl core::marker::Unpin for aya::programs::links::LinkOrder
+impl core::panic::unwind_safe::RefUnwindSafe for aya::programs::links::LinkOrder
+impl core::panic::unwind_safe::UnwindSafe for aya::programs::links::LinkOrder
+impl<T, U> core::convert::Into<U> for aya::programs::links::LinkOrder where U: core::convert::From<T>
+pub fn aya::programs::links::LinkOrder::into(self) -> U
+impl<T, U> core::convert::TryFrom<U> for aya::programs::links::LinkOrder where U: core::convert::Into<T>
+pub type aya::programs::links::LinkOrder::Error = core::convert::Infallible
+pub fn aya::programs::links::LinkOrder::try_from(value: U) -> core::result::Result<T, <T as core::convert::TryFrom<U>>::Error>
+impl<T, U> core::convert::TryInto<U> for aya::programs::links::LinkOrder where U: core::convert::TryFrom<T>
+pub type aya::programs::links::LinkOrder::Error = <U as core::convert::TryFrom<T>>::Error
+pub fn aya::programs::links::LinkOrder::try_into(self) -> core::result::Result<U, <U as core::convert::TryFrom<T>>::Error>
+impl<T> core::any::Any for aya::programs::links::LinkOrder where T: 'static + core::marker::Sized
+pub fn aya::programs::links::LinkOrder::type_id(&self) -> core::any::TypeId
+impl<T> core::borrow::Borrow<T> for aya::programs::links::LinkOrder where T: core::marker::Sized
+pub fn aya::programs::links::LinkOrder::borrow(&self) -> &T
+impl<T> core::borrow::BorrowMut<T> for aya::programs::links::LinkOrder where T: core::marker::Sized
+pub fn aya::programs::links::LinkOrder::borrow_mut(&mut self) -> &mut T
+impl<T> core::convert::From<T> for aya::programs::links::LinkOrder
+pub fn aya::programs::links::LinkOrder::from(t: T) -> T
 pub struct aya::programs::LircMode2
 impl aya::programs::lirc_mode2::LircMode2
 pub fn aya::programs::lirc_mode2::LircMode2::attach<T: std::os::fd::owned::AsFd>(&mut self, lircdev: T) -> core::result::Result<aya::programs::lirc_mode2::LircLinkId, aya::programs::ProgramError>
@@ -8071,6 +8207,31 @@ impl<T> core::borrow::BorrowMut<T> for aya::programs::ProgramFd where T: core::m
 pub fn aya::programs::ProgramFd::borrow_mut(&mut self) -> &mut T
 impl<T> core::convert::From<T> for aya::programs::ProgramFd
 pub fn aya::programs::ProgramFd::from(t: T) -> T
+pub struct aya::programs::ProgramId(_)
+impl aya::programs::ProgramId
+pub unsafe fn aya::programs::ProgramId::new(id: u32) -> Self
+impl core::marker::Freeze for aya::programs::ProgramId
+impl core::marker::Send for aya::programs::ProgramId
+impl core::marker::Sync for aya::programs::ProgramId
+impl core::marker::Unpin for aya::programs::ProgramId
+impl core::panic::unwind_safe::RefUnwindSafe for aya::programs::ProgramId
+impl core::panic::unwind_safe::UnwindSafe for aya::programs::ProgramId
+impl<T, U> core::convert::Into<U> for aya::programs::ProgramId where U: core::convert::From<T>
+pub fn aya::programs::ProgramId::into(self) -> U
+impl<T, U> core::convert::TryFrom<U> for aya::programs::ProgramId where U: core::convert::Into<T>
+pub type aya::programs::ProgramId::Error = core::convert::Infallible
+pub fn aya::programs::ProgramId::try_from(value: U) -> core::result::Result<T, <T as core::convert::TryFrom<U>>::Error>
+impl<T, U> core::convert::TryInto<U> for aya::programs::ProgramId where U: core::convert::TryFrom<T>
+pub type aya::programs::ProgramId::Error = <U as core::convert::TryFrom<T>>::Error
+pub fn aya::programs::ProgramId::try_into(self) -> core::result::Result<U, <U as core::convert::TryFrom<T>>::Error>
+impl<T> core::any::Any for aya::programs::ProgramId where T: 'static + core::marker::Sized
+pub fn aya::programs::ProgramId::type_id(&self) -> core::any::TypeId
+impl<T> core::borrow::Borrow<T> for aya::programs::ProgramId where T: core::marker::Sized
+pub fn aya::programs::ProgramId::borrow(&self) -> &T
+impl<T> core::borrow::BorrowMut<T> for aya::programs::ProgramId where T: core::marker::Sized
+pub fn aya::programs::ProgramId::borrow_mut(&mut self) -> &mut T
+impl<T> core::convert::From<T> for aya::programs::ProgramId
+pub fn aya::programs::ProgramId::from(t: T) -> T
 pub struct aya::programs::ProgramInfo(_)
 impl aya::programs::ProgramInfo
 pub fn aya::programs::ProgramInfo::btf_id(&self) -> core::option::Option<u32>
@@ -8168,10 +8329,11 @@ pub struct aya::programs::SchedClassifier
 impl aya::programs::tc::SchedClassifier
 pub fn aya::programs::tc::SchedClassifier::attach(&mut self, interface: &str, attach_type: aya::programs::tc::TcAttachType) -> core::result::Result<aya::programs::tc::SchedClassifierLinkId, aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifier::attach_to_link(&mut self, link: aya::programs::tc::SchedClassifierLink) -> core::result::Result<aya::programs::tc::SchedClassifierLinkId, aya::programs::ProgramError>
-pub fn aya::programs::tc::SchedClassifier::attach_with_options(&mut self, interface: &str, attach_type: aya::programs::tc::TcAttachType, options: aya::programs::tc::TcOptions) -> core::result::Result<aya::programs::tc::SchedClassifierLinkId, aya::programs::ProgramError>
+pub fn aya::programs::tc::SchedClassifier::attach_with_options(&mut self, interface: &str, attach_type: aya::programs::tc::TcAttachType, options: aya::programs::tc::TcAttachOptions) -> core::result::Result<aya::programs::tc::SchedClassifierLinkId, aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifier::detach(&mut self, link_id: aya::programs::tc::SchedClassifierLinkId) -> core::result::Result<(), aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifier::from_pin<P: core::convert::AsRef<std::path::Path>>(path: P) -> core::result::Result<Self, aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifier::load(&mut self) -> core::result::Result<(), aya::programs::ProgramError>
+pub fn aya::programs::tc::SchedClassifier::query_tcx(interface: &str, attach_type: aya::programs::tc::TcAttachType) -> core::result::Result<(u64, alloc::vec::Vec<aya::programs::ProgramInfo>), aya::programs::ProgramError>
 pub fn aya::programs::tc::SchedClassifier::take_link(&mut self, link_id: aya::programs::tc::SchedClassifierLinkId) -> core::result::Result<aya::programs::tc::SchedClassifierLink, aya::programs::ProgramError>
 impl aya::programs::tc::SchedClassifier
 pub fn aya::programs::tc::SchedClassifier::fd(&self) -> core::result::Result<&aya::programs::ProgramFd, aya::programs::ProgramError>
@@ -8182,6 +8344,8 @@ pub fn aya::programs::tc::SchedClassifier::pin<P: core::convert::AsRef<std::path
 pub fn aya::programs::tc::SchedClassifier::unpin(self) -> core::result::Result<(), std::io::error::Error>
 impl aya::programs::tc::SchedClassifier
 pub fn aya::programs::tc::SchedClassifier::unload(&mut self) -> core::result::Result<(), aya::programs::ProgramError>
+impl aya::programs::MultiProgram for aya::programs::tc::SchedClassifier
+pub fn aya::programs::tc::SchedClassifier::fd(&self) -> core::result::Result<std::os::fd::owned::BorrowedFd<'_>, aya::programs::ProgramError>
 impl core::fmt::Debug for aya::programs::tc::SchedClassifier
 pub fn aya::programs::tc::SchedClassifier::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
 impl core::ops::drop::Drop for aya::programs::tc::SchedClassifier
@@ -8831,6 +8995,14 @@ impl aya::programs::links::Link for aya::programs::xdp::XdpLink
 pub type aya::programs::xdp::XdpLink::Id = aya::programs::xdp::XdpLinkId
 pub fn aya::programs::xdp::XdpLink::detach(self) -> core::result::Result<(), aya::programs::ProgramError>
 pub fn aya::programs::xdp::XdpLink::id(&self) -> Self::Id
+pub trait aya::programs::MultiProgLink
+pub fn aya::programs::MultiProgLink::fd(&self) -> core::result::Result<std::os::fd::owned::BorrowedFd<'_>, aya::programs::links::LinkError>
+impl aya::programs::MultiProgLink for aya::programs::tc::SchedClassifierLink
+pub fn aya::programs::tc::SchedClassifierLink::fd(&self) -> core::result::Result<std::os::fd::owned::BorrowedFd<'_>, aya::programs::links::LinkError>
+pub trait aya::programs::MultiProgram
+pub fn aya::programs::MultiProgram::fd(&self) -> core::result::Result<std::os::fd::owned::BorrowedFd<'_>, aya::programs::ProgramError>
+impl aya::programs::MultiProgram for aya::programs::tc::SchedClassifier
+pub fn aya::programs::tc::SchedClassifier::fd(&self) -> core::result::Result<std::os::fd::owned::BorrowedFd<'_>, aya::programs::ProgramError>
 pub fn aya::programs::loaded_programs() -> impl core::iter::traits::iterator::Iterator<Item = core::result::Result<aya::programs::ProgramInfo, aya::programs::ProgramError>>
 pub mod aya::sys
 #[non_exhaustive] pub enum aya::sys::Stats


Test patch:
diff --git a/test/integration-ebpf/Cargo.toml b/test/integration-ebpf/Cargo.toml
index 102198aec..2649871b1 100644
--- a/test/integration-ebpf/Cargo.toml
+++ b/test/integration-ebpf/Cargo.toml
@@ -37,6 +37,10 @@ path = "src/pass.rs"
 name = "test"
 path = "src/test.rs"
 
+[[bin]]
+name = "tcx"
+path = "src/tcx.rs"
+
 [[bin]]
 name = "relocations"
 path = "src/relocations.rs"
diff --git a/test/integration-ebpf/src/tcx.rs b/test/integration-ebpf/src/tcx.rs
new file mode 100644
index 000000000..5ed3c2118
--- /dev/null
+++ b/test/integration-ebpf/src/tcx.rs
@@ -0,0 +1,15 @@
+#![no_std]
+#![no_main]
+
+use aya_ebpf::{bindings::tcx_action_base::TCX_NEXT, macros::classifier, programs::TcContext};
+
+#[classifier]
+pub fn tcx_next(_ctx: TcContext) -> i32 {
+    TCX_NEXT
+}
+
+#[cfg(not(test))]
+#[panic_handler]
+fn panic(_info: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
diff --git a/test/integration-test/src/lib.rs b/test/integration-test/src/lib.rs
index fc31e6c28..c20926ddb 100644
--- a/test/integration-test/src/lib.rs
+++ b/test/integration-test/src/lib.rs
@@ -14,6 +14,7 @@ pub const LOG: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/log"));
 pub const MAP_TEST: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/map_test"));
 pub const NAME_TEST: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/name_test"));
 pub const PASS: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/pass"));
+pub const TCX: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/tcx"));
 pub const TEST: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/test"));
 pub const RELOCATIONS: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/relocations"));
 pub const TWO_PROGS: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/two_progs"));
diff --git a/test/integration-test/src/tests.rs b/test/integration-test/src/tests.rs
index db99e0959..61f490cf0 100644
--- a/test/integration-test/src/tests.rs
+++ b/test/integration-test/src/tests.rs
@@ -8,4 +8,5 @@ mod rbpf;
 mod relocations;
 mod ring_buf;
 mod smoke;
+mod tcx;
 mod xdp;
diff --git a/test/integration-test/src/tests/tcx.rs b/test/integration-test/src/tests/tcx.rs
new file mode 100644
index 000000000..a58565d0e
--- /dev/null
+++ b/test/integration-test/src/tests/tcx.rs
@@ -0,0 +1,102 @@
+use aya::{
+    programs::{tc::TcAttachOptions, LinkOrder, ProgramId, SchedClassifier, TcAttachType},
+    util::KernelVersion,
+    Ebpf,
+};
+use test_log::test;
+
+use crate::utils::NetNsGuard;
+
+#[test]
+fn tcx() {
+    let kernel_version = KernelVersion::current().unwrap();
+    if kernel_version < KernelVersion::new(6, 6, 0) {
+        eprintln!("skipping tcx_attach test on kernel {kernel_version:?}");
+        return;
+    }
+
+    let _netns = NetNsGuard::new();
+
+    // We need a dedicated `Ebpf` instance for each program that we load
+    // since TCX does not allow the same program ID to be attached multiple
+    // times to the same interface/direction.
+    //
+    // Variables declared within this macro are within a closure scope to avoid
+    // variable name conflicts.
+    //
+    // Yields a tuple of the `Ebpf` which must remain in scope for the duration
+    // of the test, and the link ID of the attached program.
+    macro_rules! attach_program_with_link_order_inner {
+        ($program_name:ident, $link_order:expr) => {
+            let mut ebpf = Ebpf::load(crate::TCX).unwrap();
+            let $program_name: &mut SchedClassifier =
+                ebpf.program_mut("tcx_next").unwrap().try_into().unwrap();
+            $program_name.load().unwrap();
+        };
+    }
+    macro_rules! attach_program_with_link_order {
+        ($program_name:ident, $link_order:expr) => {
+            attach_program_with_link_order_inner!($program_name, $link_order);
+            $program_name
+                .attach_with_options(
+                    "lo",
+                    TcAttachType::Ingress,
+                    TcAttachOptions::TcxOrder($link_order),
+                )
+                .unwrap();
+        };
+        ($program_name:ident, $link_id_name:ident, $link_order:expr) => {
+            attach_program_with_link_order_inner!($program_name, $link_order);
+            let $link_id_name = $program_name
+                .attach_with_options(
+                    "lo",
+                    TcAttachType::Ingress,
+                    TcAttachOptions::TcxOrder($link_order),
+                )
+                .unwrap();
+        };
+    }
+
+    attach_program_with_link_order!(default, LinkOrder::default());
+    attach_program_with_link_order!(first, LinkOrder::first());
+    attach_program_with_link_order!(last, last_link_id, LinkOrder::last());
+
+    let last_link = last.take_link(last_link_id).unwrap();
+
+    attach_program_with_link_order!(before_last, LinkOrder::before_link(&last_link).unwrap());
+    attach_program_with_link_order!(after_last, LinkOrder::after_link(&last_link).unwrap());
+
+    attach_program_with_link_order!(before_default, LinkOrder::before_program(default).unwrap());
+    attach_program_with_link_order!(after_default, LinkOrder::after_program(default).unwrap());
+
+    attach_program_with_link_order!(
+        before_first,
+        LinkOrder::before_program_id(unsafe { ProgramId::new(first.info().unwrap().id()) })
+    );
+    attach_program_with_link_order!(
+        after_first,
+        LinkOrder::after_program_id(unsafe { ProgramId::new(first.info().unwrap().id()) })
+    );
+
+    let expected_order = [
+        before_first,
+        first,
+        after_first,
+        before_default,
+        default,
+        after_default,
+        before_last,
+        last,
+        after_last,
+    ]
+    .iter()
+    .map(|program| program.info().unwrap().id())
+    .collect::<Vec<_>>();
+
+    let (revision, got_order) = SchedClassifier::query_tcx("lo", TcAttachType::Ingress).unwrap();
+    assert_eq!(revision, (expected_order.len() + 1) as u64);
+    assert_eq!(
+        got_order.iter().map(|p| p.id()).collect::<Vec<_>>(),
+        expected_order
+    );
+}