Lesson 12: eBPF TC Egress (Outbound Traffic Control)
📚 What is eBPF TC Egress?
TC (Traffic Control) is the Linux kernel's traffic control subsystem. eBPF TC Egress programs attach to the outbound direction of network interfaces for:
- ✅ Filtering outbound network packets (drop, forward, modify)
- ✅ Implementing outbound network policies (traffic shaping, rate limiting)
- ✅ Outbound traffic monitoring and statistics (traffic analysis, QoS)
- ✅ Outbound packet redirection (container networking, service mesh)
TC Egress Hook Point Location
Egress (outbound)
│
... ──────► Routing ──────► Forwarding ─►│ ──────► Network
TC Filter Interface
(BPF_PROG_TYPE_SCHED_CLS)🎯 Learning Objectives
- Understand how TC Egress programs work
- Learn to write outbound packet filtering programs
- Master application scenarios for outbound traffic control
- Implement an example that limits outbound ICMP packets
1. Differences Between TC Egress and Ingress
| Feature | TC Ingress | TC Egress |
|---|---|---|
| Direction | Inbound (receiving) | Outbound (sending) |
| Trigger Point | After packet arrives at NIC | Before packet leaves NIC |
| Typical Use | Firewall, inbound filtering | Traffic shaping, rate limiting |
| Visible Data | Packets from external sources | Packets generated by local host |
2. Kernel-Space Program: Filtering Outbound ICMP Packets
2.1 Complete Code
File: tc_egress.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
// TC return value definitions
#define TC_ACT_UNSPEC -1 // Use default behavior
#define TC_ACT_OK 0 // Allow through
#define TC_ACT_RECLASSIFY 1 // Reclassify
#define TC_ACT_SHOT 2 // Drop packet
#define TC_ACT_PIPE 3 // Pass to next action
#define ICMP_PROTOCOL 1
// Egress traffic filter function
// Note: libbpf < 1.0 uses SEC("classifier"), libbpf >= 1.0 can use SEC("tc")
SEC("classifier")
int tc_egress_filter(struct __sk_buff *skb)
{
// Step 1: Get packet start and end positions
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
// Step 2: Parse Ethernet header
struct ethhdr *eth = data;
// Boundary check: ensure no out-of-bounds access
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK; // Packet too small, allow through
// Step 3: Check if it's IP protocol (EtherType = 0x0800)
if (eth->h_proto != bpf_htons(0x0800))
return TC_ACT_OK; // Not IPv4, allow through
// Step 4: Parse IP header
struct iphdr *ip = data + sizeof(struct ethhdr);
// Boundary check: ensure IP header is within packet bounds
if ((void *)(ip + 1) > data_end)
return TC_ACT_OK;
// Step 5: Filter ICMP protocol
if (ip->protocol == ICMP_PROTOCOL) {
// Extract source and destination IP
__u32 src_ip = ip->saddr;
__u32 dst_ip = ip->daddr;
bpf_printk("TC Egress: Dropping ICMP packet: %pI4 -> %pI4\n",
&src_ip, &dst_ip);
// Drop ICMP packet
return TC_ACT_SHOT;
}
// Step 6: Allow other protocols
return TC_ACT_OK;
}2.2 Code Explanation
Key Point 1: Egress Processing Timing
SEC("classifier")
int tc_egress_filter(struct __sk_buff *skb)- TC Egress executes just before a packet leaves the network interface
- Can intercept all outbound traffic generated by the local host
- Suitable for implementing traffic shaping, rate limiting, etc.
Key Point 2: Section Name Compatibility
Important Note: Section name must be chosen based on libbpf version:
| libbpf Version | Recommended Section Name | Notes |
|---|---|---|
| < 1.0 | SEC("classifier") or SEC("tc") | Only supports this format |
| >= 1.0 | SEC("tc") or SEC("tc/egress") | Supports more explicit direction specification |
For TC egress programs, both ingress and egress use the same section name in older libbpf versions. The direction is specified when attaching via BPF_TC_EGRESS.
Key Point 3: Packet Source
In the Egress direction:
- Packets come from local applications (e.g., ping, curl)
- Source IP (
ip->saddr) is usually the local host IP - Destination IP (
ip->daddr) is the remote host IP
Key Point 4: Application Scenarios
Typical TC Egress applications:
- Traffic Shaping: Limit outbound bandwidth for specific applications
- Protocol Filtering: Block outbound traffic of certain protocols
- Data Leakage Prevention: Monitor and control outbound sensitive data
- QoS: Set priorities for different types of traffic
3. User-Space Program
3.1 Complete Code
File: tc_egress.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include <net/if.h>
#include "tc_egress.skel.h"
static volatile bool exiting = false;
static void sig_handler(int sig)
{
exiting = true;
}
int main(int argc, char **argv)
{
struct tc_egress_bpf *skel;
int err;
int ifindex;
LIBBPF_OPTS(bpf_tc_hook, hook);
LIBBPF_OPTS(bpf_tc_opts, opts_egress);
// Step 1: Check arguments (need to specify network interface name)
if (argc != 2) {
fprintf(stderr, "Usage: %s <ifname>\n", argv[0]);
fprintf(stderr, "Example: %s eth0\n", argv[0]);
return 1;
}
// Step 2: Get network interface index
ifindex = if_nametoindex(argv[1]);
if (ifindex == 0) {
fprintf(stderr, "Failed to get ifindex for %s: %s\n",
argv[1], strerror(errno));
return 1;
}
printf("Attaching TC egress filter to interface: %s (ifindex=%d)\n",
argv[1], ifindex);
// Step 3: Set signal handlers
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
// Step 4: Open and load BPF program
skel = tc_egress_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
// Step 5: Create TC hook (egress direction)
hook.ifindex = ifindex;
hook.attach_point = BPF_TC_EGRESS; // Outbound direction
err = bpf_tc_hook_create(&hook);
if (err && err != -EEXIST) { // -EEXIST means hook already exists, can ignore
fprintf(stderr, "Failed to create TC hook: %d\n", err);
goto cleanup;
}
// Step 6: Attach egress program
opts_egress.prog_fd = bpf_program__fd(skel->progs.tc_egress_filter);
err = bpf_tc_attach(&hook, &opts_egress);
if (err) {
fprintf(stderr, "Failed to attach TC egress program: %d\n", err);
goto cleanup;
}
printf("✓ Attached TC egress filter\n");
// Step 7: Main loop - wait for exit signal
printf("\nTC egress filter is running. Press Ctrl+C to exit.\n");
printf("Try: ping 8.8.8.8 (outgoing ICMP requests will be dropped)\n\n");
printf("View dropped packets: sudo cat /sys/kernel/debug/tracing/trace_pipe\n\n");
// Wait for exit signal
while (!exiting) {
sleep(1);
}
printf("\nDetaching TC egress filter...\n");
// Step 8: Cleanup egress
opts_egress.flags = opts_egress.prog_fd = opts_egress.prog_id = 0;
bpf_tc_detach(&hook, &opts_egress);
cleanup:
tc_egress_bpf__destroy(skel);
printf("TC egress filter detached successfully.\n");
return err != 0;
}3.2 Code Explanation
Key API Functions
bpf_tc_hook_create()- Create TC hookchook.ifindex = ifindex; hook.attach_point = BPF_TC_EGRESS; // Note: set to EGRESS bpf_tc_hook_create(&hook);bpf_tc_attach()- Attach eBPF programcopts.prog_fd = bpf_program__fd(skel->progs.tc_egress_filter); bpf_tc_attach(&hook, &opts);bpf_tc_detach()- Detach eBPF programcbpf_tc_detach(&hook, &opts);
4. Compilation and Execution
4.1 Compilation Steps
cd src/tc_egress
makeAfter successful compilation, the following files will be generated:
tc_egress- Executable program../.output/tc_egress.bpf.o- eBPF bytecode../.output/tc_egress.skel.h- Skeleton header file
4.2 Running Example
# Check network interface name
ip addr show
# Run TC egress filter (requires root privileges)
sudo ./tc_egress ens33 # Replace with your network interface name (e.g., eth0, ens33)
# Test in another terminal
ping 8.8.8.8 # ICMP request packets will be dropped (ping will fail)
curl https://google.com # TCP traffic passes normallyExpected Behavior:
- ping command will fail because ICMP request packets are dropped on egress
- HTTP/HTTPS access works normally because only ICMP protocol is filtered
4.3 View Kernel Logs
# View bpf_printk output
sudo cat /sys/kernel/debug/tracing/trace_pipeExpected output:
tc_egress-12345 [001] .... 123456.789: TC Egress: Dropping ICMP packet: 192.168.1.100 -> 8.8.8.85. Practical Exercises
Exercise 1: Basic - Count Outbound Traffic
Task: Use a BPF Map to count the number of outbound packets for each protocol (TCP, UDP, ICMP).
Hint:
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 256); // 256 protocol numbers
__type(key, __u32);
__type(value, __u64);
} protocol_stats SEC(".maps");
// In tc_egress_filter:
__u32 proto = ip->protocol;
__u64 *count = bpf_map_lookup_elem(&protocol_stats, &proto);
if (count) {
__sync_fetch_and_add(count, 1);
}Exercise 2: Intermediate - Limit Outbound HTTP Traffic
Task: Drop all outbound TCP packets with destination port 80 or 443.
Hint:
struct tcphdr *tcp = (void *)ip + sizeof(*ip);
if ((void *)(tcp + 1) > data_end)
return TC_ACT_OK;
if (ip->protocol == 6) { // TCP
__u16 dport = bpf_ntohs(tcp->dest);
if (dport == 80 || dport == 443) {
bpf_printk("TC Egress: Blocking HTTP(S) to port %d\n", dport);
return TC_ACT_SHOT;
}
}Exercise 3: Advanced - Implement Simple Rate Limiting
Task: Use timestamps and counters to limit outbound traffic to a maximum of 100 packets per second.
Hint:
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct rate_limit_info);
} rate_limit SEC(".maps");
struct rate_limit_info {
__u64 last_time;
__u64 count;
};
// Check rate in the program6. Common Questions
Q1: How to view attached TC egress programs?
# View TC filters
sudo tc filter show dev eth0 egress
# Using bpftool
sudo bpftool prog list
sudo bpftool net listQ2: Does the Egress filter impact performance?
- TC Egress executes in the kernel network stack with some performance overhead
- But compared to userspace firewalls (like iptables), performance is better
- For high-performance needs, consider XDP (but XDP only supports ingress)
Q3: Can I use Ingress and Egress simultaneously?
Yes! You can attach both ingress and egress programs to the same interface:
// Create ingress hook
hook.attach_point = BPF_TC_INGRESS;
bpf_tc_hook_create(&hook);
bpf_tc_attach(&hook, &opts_ingress);
// Create egress hook
hook.attach_point = BPF_TC_EGRESS;
bpf_tc_hook_create(&hook);
bpf_tc_attach(&hook, &opts_egress);Q4: Can TC Egress modify packets?
Yes! Using helper functions like bpf_skb_store_bytes() you can modify packet contents:
// Modify destination IP
__u32 new_ip = bpf_htonl(0x08080808); // 8.8.8.8
bpf_skb_store_bytes(skb, offset, &new_ip, sizeof(new_ip), 0);7. Advanced Egress Application Scenarios
7.1 Traffic Shaping
Control outbound bandwidth for specific applications:
// Using token bucket algorithm
if (!has_tokens()) {
return TC_ACT_SHOT; // Drop packets exceeding rate limit
}
consume_token();
return TC_ACT_OK;7.2 Data Leakage Prevention (DLP)
Monitor and block outbound sensitive data:
// Check packet contents
if (contains_sensitive_data(skb)) {
bpf_printk("TC Egress: Blocked sensitive data leak\n");
return TC_ACT_SHOT;
}7.3 Service Mesh
Redirect outbound traffic to a proxy:
// Redirect to sidecar proxy
return bpf_redirect(proxy_ifindex, 0);8. Reference Resources
Summary
Through this lesson, you should have mastered:
✅ Basic concepts and working principles of TC Egress programs ✅ How to write outbound packet filtering logic ✅ Advanced application scenarios for TC Egress ✅ How to use TC Egress APIs ✅ How to debug and test TC Egress programs
Next Steps:
- Learn XDP (eXpress Data Path) for even higher performance packet processing
- Explore combined use of TC and XDP
- Deep dive into eBPF applications in container networking (e.g., Cilium)