Skip to content

Lesson 11: eBPF TC Ingress (Inbound Traffic Control)

📚 What is eBPF TC Ingress?

TC (Traffic Control) is the Linux kernel's traffic control subsystem. eBPF TC Ingress programs attach to the inbound direction of network interfaces for:

  • Filtering inbound network packets (drop, forward, modify)
  • Implementing inbound network policies (firewall, access control)
  • Inbound traffic monitoring and statistics (traffic analysis, QoS)
  • Inbound packet redirection (container networking, service mesh)

TC Ingress Hook Point Location

                    Ingress (inbound)

    Network ──────────►  │  ──────► Routing ──────► ...
    Interface       TC Filter
                    (BPF_PROG_TYPE_SCHED_CLS)

🎯 Learning Objectives

  1. Understand how TC Ingress programs work
  2. Learn to write inbound packet filtering programs
  3. Master the meaning of TC program return values
  4. Implement a firewall example that drops inbound ICMP packets

1. TC Program Return Values

TC eBPF programs use return values to determine packet handling:

Return ValueMacroMeaning
-1TC_ACT_UNSPECUse default action configured from tc
0TC_ACT_OKAllow through, terminate pipeline and allow packet to proceed
1TC_ACT_RECLASSIFYTerminate pipeline and restart classification from beginning
2TC_ACT_SHOTDrop packet, terminate pipeline and drop packet
3TC_ACT_PIPEIterate to next action (if available)
4TC_ACT_STOLENPacket consumed, no further delivery
6TC_ACT_REDIRECTRedirect to another interface
Other-Unspecified return code

Most commonly used:

  • TC_ACT_OK (0) - Allow packet
  • TC_ACT_SHOT (2) - Drop packet

Reference: tc-bpf man page


2. Kernel-Space Program: Filtering Inbound ICMP Packets

2.1 Complete Code

File: tc_ingress.bpf.c

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

// Ingress traffic filter function
// Note: libbpf < 1.0 uses SEC("classifier"), libbpf >= 1.0 can use SEC("tc")
SEC("classifier")
int tc_ingress_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 Ingress: 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: struct __sk_buff

c
int tc_ingress_filter(struct __sk_buff *skb)
  • __sk_buff is the socket buffer structure provided by the kernel for eBPF
  • Contains packet metadata and data pointers
  • skb->data points to packet start
  • skb->data_end points to packet end

Key Point 2: Boundary Checks (Verifier Requirement)

c
if ((void *)(eth + 1) > data_end)
    return TC_ACT_OK;
  • Boundary checks are mandatory, otherwise eBPF verifier will reject loading
  • Must check bounds before every pointer access

Key Point 3: Section Name Compatibility

c
SEC("classifier")
int tc_ingress_filter(struct __sk_buff *skb)

Important Note: Section name must be chosen based on libbpf version:

libbpf VersionRecommended Section NameNotes
< 1.0SEC("classifier") or SEC("tc")Only supports this format
>= 1.0SEC("tc") or SEC("tc/ingress")Supports more explicit direction specification

If you encounter error: missing BPF prog type, check ELF section name, it means your libbpf version doesn't match the section name format.

Key Point 4: Byte Order Conversion

c
if (eth->h_proto != bpf_htons(0x0800))
  • Network byte order is big-endian
  • Host byte order may be little-endian
  • Use bpf_htons() for conversion

Key Point 5: Protocol Numbers

ProtocolNumberMacro
ICMP1IPPROTO_ICMP
TCP6IPPROTO_TCP
UDP17IPPROTO_UDP

3. User-Space Program

3.1 Complete Code

File: tc_ingress.c

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_ingress.skel.h"

static volatile bool exiting = false;

static void sig_handler(int sig)
{
    exiting = true;
}

int main(int argc, char **argv)
{
    struct tc_ingress_bpf *skel;
    int err;
    int ifindex;
    LIBBPF_OPTS(bpf_tc_hook, hook);
    LIBBPF_OPTS(bpf_tc_opts, opts_ingress);

    // 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 ingress 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_ingress_bpf__open_and_load();
    if (!skel) {
        fprintf(stderr, "Failed to open and load BPF skeleton\n");
        return 1;
    }

    // Step 5: Create TC hook (ingress direction)
    hook.ifindex = ifindex;
    hook.attach_point = BPF_TC_INGRESS;  // Inbound 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 ingress program
    opts_ingress.prog_fd = bpf_program__fd(skel->progs.tc_ingress_filter);
    err = bpf_tc_attach(&hook, &opts_ingress);
    if (err) {
        fprintf(stderr, "Failed to attach TC ingress program: %d\n", err);
        goto cleanup;
    }
    printf("✓ Attached TC ingress filter\n");

    // Step 7: Main loop - wait for exit signal
    printf("\nTC ingress filter is running. Press Ctrl+C to exit.\n");
    printf("Try: ping 8.8.8.8 (incoming ICMP replies 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 ingress filter...\n");

    // Step 8: Cleanup ingress
    opts_ingress.flags = opts_ingress.prog_fd = opts_ingress.prog_id = 0;
    bpf_tc_detach(&hook, &opts_ingress);

cleanup:
    tc_ingress_bpf__destroy(skel);
    printf("TC ingress filter detached successfully.\n");
    return err != 0;
}

3.2 Code Explanation

Key API Functions

  1. if_nametoindex() - Get network interface index

    c
    ifindex = if_nametoindex("eth0");  // Returns interface index number
  2. bpf_tc_hook_create() - Create TC hook

    c
    hook.ifindex = ifindex;
    hook.attach_point = BPF_TC_INGRESS;
    bpf_tc_hook_create(&hook);
  3. bpf_tc_attach() - Attach eBPF program

    c
    opts.prog_fd = bpf_program__fd(skel->progs.tc_ingress_filter);
    bpf_tc_attach(&hook, &opts);
  4. bpf_tc_detach() - Detach eBPF program

    c
    bpf_tc_detach(&hook, &opts);

4. Compilation and Execution

4.1 Compilation Steps

bash
cd src/tc_ingress
make

After successful compilation, the following files will be generated:

  • tc_ingress - Executable program
  • ../.output/tc_ingress.bpf.o - eBPF bytecode
  • ../.output/tc_ingress.skel.h - Skeleton header file

4.2 Running Example

bash
# Check network interface name
ip addr show

# Run TC ingress filter (requires root privileges)
sudo ./tc_ingress ens33   # Replace with your network interface name (e.g., eth0, ens33)

# Test in another terminal
ping 8.8.8.8            # ICMP reply packets will be dropped (ping will fail or timeout)
curl https://google.com # TCP traffic passes normally

Expected Behavior:

  • ping command will fail or timeout because ICMP reply packets are dropped on ingress
  • HTTP/HTTPS access works normally because only ICMP protocol is filtered

4.3 View Kernel Logs

bash
# View bpf_printk output
sudo cat /sys/kernel/debug/tracing/trace_pipe

Expected output:

tc_ingress-12345 [001] .... 123456.789: TC Ingress: Dropping ICMP packet: 8.8.8.8 -> 192.168.1.100

5. Practical Exercises

Exercise 1: Basic - Print TCP Packet Information

Task: Modify the code to print source IP, destination IP, and destination port when inbound TCP packets are detected.

Hint:

c
if (ip->protocol == 6) {
    struct tcphdr *tcp = (void *)ip + sizeof(*ip);
    if ((void *)(tcp + 1) > data_end)
        return TC_ACT_OK;

    bpf_printk("TC Ingress: TCP packet from %pI4 to port %d\n",
               &ip->saddr, bpf_ntohs(tcp->dest));
}

Exercise 2: Intermediate - Drop Inbound Traffic on Specific Port

Task: Drop all inbound TCP packets with destination port 22 (SSH traffic).

Hint:

c
struct tcphdr *tcp = (void *)ip + sizeof(*ip);
if ((void *)(tcp + 1) > data_end)
    return TC_ACT_OK;

if (ip->protocol == 6 && tcp->dest == bpf_htons(22)) {
    bpf_printk("TC Ingress: Blocking SSH attempt\n");
    return TC_ACT_SHOT;  // Drop
}

Exercise 3: Advanced - Implement Simple IP Blacklist

Task: Use a BPF Map to store blacklisted IPs and block all inbound traffic from these IPs.


6. Common Questions

Q1: How to view attached TC ingress programs?

bash
# View TC filters
sudo tc filter show dev eth0 ingress

# Using bpftool
sudo bpftool prog list
sudo bpftool net list

Q2: How to manually remove TC ingress filters?

bash
# Remove all TC filters
sudo tc qdisc del dev eth0 clsact

# Or use tc filter delete
sudo tc filter del dev eth0 ingress

Q3: What's the difference between TC Ingress and XDP?

FeatureTC IngressXDP
Processing LocationEarly in kernel network stackDriver layer (earliest)
PerformanceHighVery high
FunctionalityMore features (can modify packets)More basic
Use CaseContainer networking, load balancingDDoS protection, high-performance filtering

Q4: Why are boundary checks needed?

The eBPF verifier requires boundary checks before accessing packet memory to prevent out-of-bounds access that could crash the kernel.


7. Reference Resources


Summary

Through this lesson, you should have mastered:

✅ Basic concepts and working principles of TC Ingress programs ✅ How to write inbound packet filtering logic ✅ Importance of boundary checks ✅ How to use TC Ingress APIs ✅ How to debug and test TC Ingress programs

Next Step: Learn Lesson 12 TC Egress to master outbound traffic control!

Released under the MIT License.