From 6e800b25952c09425672cd2295df1db249faa130 Mon Sep 17 00:00:00 2001 From: Sven Cowart Date: Fri, 14 Nov 2025 20:15:47 -0800 Subject: [PATCH] doc(aya): document tcx link pinning for atomic program replacement Add documentation and integration test for SchedClassifier link pinning, which enables zero-downtime program updates in production environments. - Add link pinning example to SchedClassifier::attach_with_options() showing atomic replacement workflow for TCX mode (kernel >= 6.6). - Add pin_tcx_link() integration test verifying link persistence across program unload and atomic replacement capability. --- aya/src/programs/tc.rs | 39 ++++++++++++++++- test/integration-test/src/tests/load.rs | 56 ++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/aya/src/programs/tc.rs b/aya/src/programs/tc.rs index 58a0ecbf..71c8c746 100644 --- a/aya/src/programs/tc.rs +++ b/aya/src/programs/tc.rs @@ -205,12 +205,47 @@ impl SchedClassifier { /// /// The returned value can be used to detach, see [SchedClassifier::detach]. /// + /// # Link Pinning (TCX mode, kernel >= 6.6) + /// + /// Links can be pinned to bpffs for atomic replacement across process restarts. + /// + /// ```no_run + /// # use std::{io, path::Path}; + /// # use aya::programs::{tc, SchedClassifier, TcAttachType, tc::TcAttachOptions, LinkOrder, links::{FdLink, PinnedLink}, LinkError}; + /// # use aya::sys::SyscallError; + /// # let mut bpf = aya::Ebpf::load(&[])?; + /// # let prog: &mut SchedClassifier = bpf.program_mut("prog").unwrap().try_into()?; + /// # prog.load()?; + /// let pin_path = "/sys/fs/bpf/my_link"; + /// + /// let link_id = match PinnedLink::from_pin(pin_path) { + /// Ok(old) => { + /// let link = FdLink::from(old).try_into()?; + /// prog.attach_to_link(link)? + /// } + /// Err(LinkError::SyscallError(SyscallError { io_error, .. })) + /// if io_error.kind() == io::ErrorKind::NotFound => + /// { + /// prog.attach_with_options( + /// "eth0", + /// TcAttachType::Ingress, + /// TcAttachOptions::TcxOrder(LinkOrder::default()), + /// )? + /// } + /// Err(e) => return Err(e.into()), + /// }; + /// + /// let link = prog.take_link(link_id)?; + /// let fd_link: FdLink = link.try_into()?; + /// fd_link.pin(pin_path)?; + /// # Ok::<(), Box>(()) + /// ``` + /// /// # 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`] - /// + /// interface, see [`qdisc_add_clsact`]. pub fn attach_with_options( &mut self, interface: &str, diff --git a/test/integration-test/src/tests/load.rs b/test/integration-test/src/tests/load.rs index 26046824..3889e240 100644 --- a/test/integration-test/src/tests/load.rs +++ b/test/integration-test/src/tests/load.rs @@ -6,11 +6,13 @@ use aya::{ maps::Array, pin::PinError, programs::{ - FlowDissector, KProbe, ProbeKind, Program, ProgramError, TracePoint, UProbe, Xdp, XdpFlags, + FlowDissector, KProbe, LinkOrder, ProbeKind, Program, ProgramError, SchedClassifier, + TcAttachType, TracePoint, UProbe, Xdp, XdpFlags, flow_dissector::{FlowDissectorLink, FlowDissectorLinkId}, kprobe::{KProbeLink, KProbeLinkId}, links::{FdLink, LinkError, PinnedLink}, loaded_links, loaded_programs, + tc::TcAttachOptions, trace_point::{TracePointLink, TracePointLinkId}, uprobe::{UProbeLink, UProbeLinkId}, xdp::{XdpLink, XdpLinkId}, @@ -394,6 +396,58 @@ fn pin_link() { assert_unloaded(program_name); } +#[test_log::test] +fn pin_tcx_link() { + // TCX links require kernel >= 6.6 + let kernel_version = KernelVersion::current().unwrap(); + if kernel_version < KernelVersion::new(6, 6, 0) { + eprintln!("skipping pin_tcx_link test on kernel {kernel_version:?}"); + return; + } + + use crate::utils::NetNsGuard; + let _netns = NetNsGuard::new(); + + let program_name = "tcx_next"; + let pin_path = "/sys/fs/bpf/aya-tcx-test-lo"; + let mut bpf = Ebpf::load(crate::TCX).unwrap(); + let prog: &mut SchedClassifier = bpf.program_mut(program_name).unwrap().try_into().unwrap(); + prog.load().unwrap(); + + let link_id = prog + .attach_with_options( + "lo", + TcAttachType::Ingress, + TcAttachOptions::TcxOrder(LinkOrder::default()), + ) + .unwrap(); + let link = prog.take_link(link_id).unwrap(); + assert_loaded(program_name); + + let fd_link: FdLink = link.try_into().unwrap(); + fd_link.pin(pin_path).unwrap(); + + // Because of the pin, the program is still attached + prog.unload().unwrap(); + assert_loaded(program_name); + + // Load a new program and atomically replace the old one using attach_to_link + let mut bpf = Ebpf::load(crate::TCX).unwrap(); + let prog: &mut SchedClassifier = bpf.program_mut(program_name).unwrap().try_into().unwrap(); + prog.load().unwrap(); + + let old_link = PinnedLink::from_pin(pin_path).unwrap(); + let link = FdLink::from(old_link).try_into().unwrap(); + let _link_id = prog.attach_to_link(link).unwrap(); + + assert_loaded(program_name); + + // Clean up: remove the stale pin file and drop the bpf instance (which drops the program and link) + remove_file(pin_path).unwrap(); + drop(bpf); + assert_unloaded(program_name); +} + trait PinProgramOps { fn pin>(&mut self, path: P) -> Result<(), PinError>; fn unpin(&mut self) -> Result<(), std::io::Error>;