Chapter 4: Traffic Control (TC) and Network Manipulation
4.1 XDP vs Traffic Control (TC)
While XDP provides optimal performance for ingress packet filtering, Traffic Control (TC) offers a broader scope for network manipulation. The primary distinctions are:
- Directionality: XDP executes exclusively on Ingress. TC supports attachment to both Ingress and Egress paths.
- Data Structures: XDP operates on raw, unparsed byte arrays. TC executes after the kernel has parsed the packet and allocated an
sk_buff(Socket Buffer) structure, granting the eBPF program access to richer networking metadata.
TC Return Codes
Similar to XDP, a TC eBPF program determines packet flow via specific return codes:
TC_ACT_PIPE: Passes the packet to the subsequent TC program or the default network stack.TC_ACT_SHOT: Drops the packet immediately.TC_ACT_OK: Terminates the TC pipeline and permits the packet to proceed.TC_ACT_REDIRECT: Redirects the packet to an alternate network interface.TC_ACT_STOLEN: Indicates the eBPF program has consumed the packet, effectively dropping it from the kernel's perspective.
4.2 Exercise: Project Generation
Generate a TC workspace using the classifier template:
cargo generate -n tc_interceptor -d program_type=classifier https://github.com/aya-rs/aya-template
4.3 Exercise: Egress Traffic Interception
This exercise implements a TC program that monitors outbound traffic and logs connections targeting standard web ports (80 and 443).
Step 1: Dependencies
Include the network-types crate in the eBPF package:
cd tc_interceptor/tc_interceptor-ebpf
cargo add network-types
Step 2: The Classifier Implementation
Replace tc_interceptor-ebpf/src/main.rs with the following logic:
#![allow(unused)] #![no_std] #![no_main] fn main() { use aya_ebpf::{ macros::classifier, programs::TcContext, bindings::{TC_ACT_PIPE, TC_ACT_SHOT}, }; use aya_log_ebpf::info; use network_types::{ eth::{EthHdr, EtherType}, ip::{Ipv4Hdr, IpProto}, tcp::TcpHdr, }; #[classifier] pub fn tc_interceptor(ctx: TcContext) -> i32 { match try_tc_interceptor(ctx) { Ok(ret) => ret, Err(_) => TC_ACT_SHOT, } } #[inline(always)] unsafe fn ptr_at<T>(ctx: &TcContext, offset: usize) -> Result<*const T, ()> { let start = ctx.data(); let end = ctx.data_end(); let len = core::mem::size_of::<T>(); if start + offset + len > end { return Err(()); } Ok((start + offset) as *const T) } fn try_tc_interceptor(ctx: TcContext) -> Result<i32, ()> { let ethhdr: *const EthHdr = unsafe { ptr_at(&ctx, 0)? }; match unsafe { (*ethhdr).ether_type() } { Ok(EtherType::Ipv4) => {} _ => return Ok(TC_ACT_PIPE), } let ipv4hdr: *const Ipv4Hdr = unsafe { ptr_at(&ctx, EthHdr::LEN)? }; let dest_addr = u32::from_be_bytes(unsafe { (*ipv4hdr).dst_addr }); let proto = unsafe { (*ipv4hdr).proto() }; if !matches!(proto, Ok(IpProto::Tcp)) { return Ok(TC_ACT_PIPE); } let tcphdr: *const TcpHdr = unsafe { ptr_at(&ctx, EthHdr::LEN + 20)? }; let dest_port = u16::from_be_bytes(unsafe { (*tcphdr).dest }); if dest_port == 80 || dest_port == 443 { info!(&ctx, "OUTBOUND web traffic to IP: {:i} on Port: {}", dest_addr, dest_port); } Ok(TC_ACT_PIPE) } #[cfg(not(test))] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} } #[unsafe(link_section = "license")] #[unsafe(no_mangle)] static LICENSE: [u8; 13] = *b"Dual MIT/GPL\0"; }
4.4 Exercise: Egress Configuration
By default, the userspace loader may attach the program to the Ingress path. Modify tc_interceptor/src/main.rs to enforce Egress attachment:
#![allow(unused)] fn main() { tc::qdisc_add_clsact(&iface)?; let program: &mut dyn Program = bpf.program_mut("tc_interceptor").unwrap(); let tc_program: &mut aya::programs::tc::SchedClassifier = program.try_into()?; tc_program.load()?; // Attach to Egress tc_program.attach(&iface, aya::programs::tc::TcAttachType::Egress)?; }
Execute the agent and trigger an outbound connection via curl -I https://google.com to observe the trace logs.