Linux XDP + eBPF:零拷贝数据包处理与 DDoS 防护实战

一、为什么需要 XDP

传统 Linux 网络栈在处理高流量时存在根本性瓶颈:数据包必须经过网卡驱动 → 内核 sk_buff 分配 → 协议栈逐层分发 → 用户态 socket read。这条路径在 10Gbps+ 场景下,即使零拷贝优化,每个包仍需数微秒的内核处理时间。

XDP (eXPress Data Path) 通过在网络驱动层挂载 eBPF 程序,实现了网卡收到数据包后、尚未分配 sk_buff 之前就进行处理的能力。这使得:

  • 包过滤在最早时机完成,无需分配 sk_buff 内存
  • 包重定向(XDP_REDIRECT)到不同 CPU 或网卡,实现线速转发
  • 包丢弃(XDP_DROP)在驱动层直接丢弃恶意流量

性能对比(单机单核,64B UDP 小包):

方案 吞吐量 (Mpps) 延迟 (μs)
iptables DROP ~0.5 -
DPDK (内核旁路) ~100+ <1
XDP_DROP ~24 <4

XDP 以接近 DPDK 的性能,保留了与 Linux 内核协议栈的完整集成。

二、XDP 程序架构

2.1 加载模式

XDP 有三种加载方式:

  • Native XDP:驱动原生支持,延迟最低(最佳)
  • Offloaded XDP:程序加载到网卡硬件(SmartNIC)
  • Generic XDP:在 sk_buff 层级兜底运行,用于不支持原生 XDP 的场景

主流驱动(i40、ixgbe、mlx5、virtio_net、ena)均已支持 Native XDP。

2.2 程序生命周期

网卡收包 → 驱动收包Poll → BPF JIT执行XDP程序 → 返回动作码
                                        ↓
                    ┌───────────────────┼───────────────────┐
                    ↓                   ↓                   ↓
              XDP_PASS           XDP_DROP          XDP_REDIRECT
           (交给协议栈)         (驱动层丢弃)         (转发到目标)
                    ↓                                   ↓
              XDP_TX (回发到入网卡)

2.3 BPF 辅助函数

XDP 程序可用的核心 helper 函数:

// 访问数据包字节
void *xdp_md_data = (void *)(long)md->data;
void *xdp_md_end = (void *)(long)md->data_end;

// 重写数据包并校验边界
bpf_xdp_adjust_head(ctx, -delta);  // 扩展头部空间(用于封装)
bpf_xdp_adjust_tail(ctx, delta);   // 调整尾部

// 重定向到目标CPU或网卡
bpf_redirect_map(&cpu_map, cpu, XDP_DROP);

// 查找映射表(防火墙/路由)
bpf_map_lookup_elem(&rules_map, &key);

2.4 返回码语义

返回码 含义 性能
XDP_ABORTED 程序异常,丢包 —
XDP_DROP 驱动层直接丢弃 最快
XDP_PASS 交出给内核协议栈 标准路径
XDP_TX 从入网卡发送出去 高
XDP_REDIRECT 转发到指定 CPU/网卡 高

三、从 BPF C 到 eBPF 字节码

3.1 最小 XDP 程序

// xdp_drop.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

SEC("xdp")
int xdp_drop_all(struct xdp_md *ctx)
{
    return XDP_DROP;
}

3.2 自定义编译(不用 BCC/libbpf)

# 编译为 BPF 目标文件
clang -O2 -g -target bpf -c xdp_drop.c -o xdp_drop.o

# 验证器会自动拒绝越界访问:XDP 程序必须检查每个指针访问
# 违例示例(会被拒绝):
#   void *data = (void *)(long)ctx->data;
#   char *p = data;
#   for (int i = 0; i < 100; i++) {  // 死循环被拒绝
#       sum += p[i];                   // 未校验边界被拒绝
#   }

正确写法:

SEC("xdp")
int xdp_drop_ip(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    struct ethhdr *eth = data;

    // 边界检查 — 验证器强制要求
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
        return XDP_PASS;

    struct iphdr *iph = data + sizeof(*eth);
    if ((void *)(iph + 1) > data_end)
        return XDP_PASS;

    // 丢弃所有 TCP SYN 包(示例)
    if (iph->protocol == IPPROTO_TCP) {
        struct tcphdr *tcph = (void *)iph + sizeof(*iph);
        if ((void *)(tcph + 1) > data_end)
            return XDP_PASS;
        if (tcph->syn)
            return XDP_DROP;
    }

    return XDP_PASS;
}

四、实战:L3/L4 动态防火墙

4.1 Go + CGO 管理型 xdpfw(libbpfgo)

// main.go 片段
package main

// #cgo LDFLAGS: -lbpf
// #include "xdpfw_kern.h"
import "C"
import (
    "github.com/cilium/ebpf"
    "github.com/cilium/ebpf/rlimit"
    "net"
    "log"
    "os"
    "os/signal"
)

func main() {
    // 提升资源限制
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // 加载编译好的 eBPF 对象
    spec, err := ebpf.LoadCollectionSpec("xdpfw_kern.o")
    if err != nil {
        log.Fatal(err)
    }

    var obj struct {
        Prog    *ebpf.Program `ebpf:"xdp_firewall"`
        SrcMap  *ebpf.Map     `ebpf:"src_rules"`
        PortMap *ebpf.Map     `ebpf:"port_rules"`
    }
    // 反序列化到 Go 结构
    // 加载到网卡
    link, err := link.AttachXDP(link.XDPOptions{
        Program:   obj.Prog,
        Interface: 2, // eth0 ifindex
    })

    // 动态下发规则
    go func() {
        for {
            select {
            case rule := <-ruleChan:
                obj.SrcMap.Put(rule.IP, rule.Action)
            }
        }
    }()

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, os.Interrupt)
    <-sig
    link.Close()
}

4.2 内核侧 BPF C 主机防火墙

// xdpfw_kern.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

// 规则动作:0=DROP, 1=PASS, 2=REDIRECT
struct bpf_map_def SEC("maps") src_rules = {
    .type = BPF_MAP_TYPE_LPM_TRIE,
    .key_size = sizeof(struct bpf_lpm_trie_key) + 4,
    .value_size = sizeof(__u8),
    .max_entries = 65535,
};

struct bpf_map_def SEC("maps") port_rules = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(__u16),        // port
    .value_size = sizeof(__u8),       // action
    .max_entries = 1024,
};

// 统计MAP
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 5);
} stats SEC(".maps");

SEC("xdp")
int xdp_firewall(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return update_stats(ctx, XDP_DROP);

    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return update_stats(ctx, XDP_PASS);

    struct iphdr *iph = data + sizeof(*eth);
    if ((void *)(iph + 1) > data_end)
        return update_stats(ctx, XDP_DROP);

    // 1. 检查源 IP 黑/白名单(LPM 最长前缀匹配)
    struct bpf_lpm_trie_key key = {
        .prefixlen = 32,
        .data = {iph->s_addr & 0xFF, (iph->s_addr >> 8) & 0xFF,
                 (iph->s_addr >> 16) & 0xFF, (iph->s_addr >> 24) & 0xFF},
    };
    __u8 *action = bpf_map_lookup_elem(&src_rules, &key);
    if (action && *action == 0)
        return update_stats(ctx, XDP_DROP);

    // 2. 传输层端口规则
    __u16 dst_port = 0;
    if (iph->protocol == IPPROTO_TCP) {
        struct tcphdr *tcph = (void *)iph + (iph->ihl * 4);
        if ((void *)(tcph + 1) > data_end)
            return update_stats(ctx, XDP_DROP);
        dst_port = bpf_ntohs(tcph->dest);
    } else if (iph->protocol == IPPROTO_UDP) {
        struct udphdr *udph = (void *)iph + (iph->ihl * 4);
        if ((void *)(udph + 1) > data_end)
            return update_stats(ctx, XDP_DROP);
        dst_port = bpf_ntohs(udph->dest);
    }

    if (dst_port) {
        __u8 *port_act = bpf_map_lookup_elem(&port_rules, &dst_port);
        if (port_act && *port_act == 0)
            return update_stats(ctx, XDP_DROP);
    }

    return update_stats(ctx, XDP_PASS);
}

五、实战:SYN Flood DDoS 防护

5.1 SYN Cookie vs XDP 专用防护

传统 SYN Cookie 由内核协议栈处理,CPU 消耗高。XDP SYN Cookie 在驱动层完成:

// syn_protect_kern.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

struct bpf_map_def SEC("maps") syn_count = {
    .type = BPF_MAP_TYPE_LRU_PERCPU_HASH,
    .key_size = sizeof(__u32),    // 源IP
    .value_size = sizeof(__u64),  // 计数值
    .max_entries = 100000,
};

#define SYN_RATE_LIMIT 100

SEC("xdp")
int syn_protect(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *iph = data + sizeof(*eth);
    if ((void *)(iph + 1) > data_end)
        return XDP_PASS;

    if (iph->protocol != IPPROTO_TCP)
        return XDP_PASS;

    struct tcphdr *tcph = (void *)iph + (iph->ihl * 4);
    if ((void *)(tcph + 1) > data_end)
        return XDP_PASS;

    // 仅关注 SYN 包(非 SYN-ACK)
    if (!tcph->syn || tcph->ack)
        return XDP_PASS;

    __u32 src_ip = iph->s_addr;
    __u64 *count = bpf_map_lookup_elem(&syn_count, &src_ip);
    __u64 new_count = 1;
    if (count) {
        new_count = *count + 1;
        if (new_count >= SYN_RATE_LIMIT)
            return XDP_DROP;
    }
    bpf_map_update_elem(&syn_count, &src_ip, &new_count, BPF_ANY);

    return XDP_PASS;
}

5.2 结合 BPF 环形缓冲区上报攻击事件

struct bpf_map_def SEC("maps") events = {
    .type = BPF_MAP_TYPE_RINGBUF,
    .max_entries = 256 * 1024,
};

struct event {
    __u32 src_ip;
    __u64 count;
    __u8 action;
};

SEC("xdp")
int syn_protect_advanced(struct xdp_md *ctx)
{
    // ... 限流逻辑 ...
    if (new_count >= SYN_RATE_LIMIT) {
        struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
        if (e) {
            e->src_ip = src_ip;
            e->count = new_count;
            e->action = 1;
            bpf_ringbuf_submit(e, 0);
        }
        return XDP_DROP;
    }
    return XDP_PASS;
}

六、XDP_REDIRECT 与 AF_XDP 联合架构

6.1 XDP_REDIRECT 到 AF_XDP

当 XDP 程序需要放行复杂包到用户态深度检测时,可用 XDP_REDIRECT 转发到 AF_XDP socket,绕过整个内核协议栈:

struct bpf_map_def SEC("maps") xsks_map = {
    .type = BPF_MAP_TYPE_XSKMAP,
    .key_size = sizeof(__u32),
    .value_size = sizeof(__u32),
    .max_entries = 64,
};

SEC("xdp")
int xdp_redirect_dns(struct xdp_md *ctx)
{
    if (dst_port == 53) {
        return bpf_redirect_map(&xsks_map, ctx->rx_queue_index, XDP_DROP);
    }
    return XDP_PASS;
}

6.2 AF_XDP 用户态零拷贝收包

// af_xdp_user.c
#include <xdp/xsk.h>
#include <linux/if_link.h>
#include <linux/if_ether.h>
#include <net/if.h>

#define NUM_FRAMES 4096
#define FRAME_SIZE XSK_UMEM__DEFAULT_FRAME_SIZE
#define RX_BATCH 32

int main(int argc, char **argv)
{
    // 1. 分配大页 buf
    void *buf_mem = mmap(NULL, NUM_FRAMES * FRAME_SIZE,
                         PROT_READ | PROT_WRITE,
                         MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                         -1, 0);

    // 2. 注册 UMEM
    struct xsk_umem_config umem_cfg = {
        .fill_size = NUM_FRAMES / 2,
        .comp_size = NUM_FRAMES / 2,
        .frame_size = FRAME_SIZE,
        .frame_headroom = XSK_UMEM__DEFAULT_FRAME_HEADROOM,
    };

    struct xsk_umem *umem;
    xsk_umem__create(&umem, buf_mem, NUM_FRAMES * FRAME_SIZE,
                     &fq, &cq, &umem_cfg);

    // 3. 创建 Socket 绑定到网卡队列
    struct xsk_socket_config xsk_cfg = {
        .rx_size = XSK_RING_CONS__DEFAULT_NUM_DESCS,
        .tx_size = XSK_RING_PROD__DEFAULT_NUM_DESCS,
        .libxdp_flags = XSK_LIBXDP_FLAGS__INHIBIT_PROG_LOAD,
        .xdp_flags = XDP_FLAGS_DRV_MODE,
        .bind_flags = XDP_USE_NEED_WAKEUP,
    };

    struct xsk_socket *xsk;
    xsk_socket__create(&xsk, "eth0", 0, umem, &rx_ring, &tx_ring, &xsk_cfg);

    // 4. RX 循环
    while (1) {
        // 从 fill ring 补充分配
        stock = xsk_prod_nb_free(&fq, 32);
        if (stock > 0) {
            xsk_ring_prod__reserve(&fq, stock, &idx);
            for (int i = 0; i < stock; i++)
                *xsk_ring_prod__fill_addr(&fq, idx++) = alloc_frame();
            xsk_ring_prod__submit(&fq, stock);
        }

        // 收包
        rcvd = xsk_ring_cons__peek(&rx_ring, RX_BATCH, &idx);
        if (!rcvd) {
            if (xsk_ring_prod__needs_wakeup(&fq))
                poll(&fds, 1, -1);
            continue;
        }

        for (int i = 0; i < rcvd; i++) {
            struct xdp_desc *desc = xsk_ring_cons__rx_desc(&rx_ring, idx++);
            void *pkt = xsk_umem__get_data(buf_mem, desc->addr);
            process_packet(pkt, desc->len);
        }
        xsk_ring_cons__release(&rx_ring, rcvd);
    }
}

七、生产环境优化与监控

7.1 多队列散列与 RSS

struct bpf_map_def SEC("maps") cpu_map = {
    .type = BPF_MAP_TYPE_CPUMAP,
    .key_size = sizeof(__u32),
    .value_size = sizeof(struct bpf_cpumap_val),
    .max_entries = 128,
};

SEC("xdp")
int xdp_load_balance(struct xdp_md *ctx)
{
    __u32 cpu = bpf_get_smp_processor_id() % num_possible_cpus();
    return bpf_redirect_map(&cpu_map, cpu, XDP_PASS);
}

7.2 BPF 程序调试与统计

# 查看程序加载状态
ip link show eth0 | grep xdp
bpftool net show

# 查看 map 内容
bpftool map dump id <map_id>

# 实时跟踪(debugfs)
cat /sys/kernel/debug/tracing/trace_pipe

7.3 核心性能指标(通过 PERCPU_ARRAY 收集每核统计)

struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __type(key, __u32);
    __type(value, struct stats);
    __uint(max_entries, 1);
} xdp_stats SEC(".maps");

struct stats {
    __u64 packets;
    __u64 bytes;
    __u64 dropped;
};

SEC("xdp")
int xdp_counter(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;

    __u32 key = 0;
    struct stats *s = bpf_map_lookup_elem(&xdp_stats, &key);
    if (s) {
        s->packets++;
        s->bytes += (data_end - data);
    }

    return XDP_PASS;
}

八、性能调优实践

8.1 尾调用拆分大程序

struct bpf_map_def SEC("maps") progs = {
    .type = BPF_MAP_TYPE_PROG_ARRAY,
    .key_size = sizeof(__u32),
    .value_size = sizeof(__u32),
    .max_entries = 32,
};

SEC("xdp")
int xdp_entry(struct xdp_md *ctx)
{
    __u32 key = PROTO_TCP;
    bpf_tail_call(ctx, &progs, key);
    return XDP_PASS;  // tail call 失败兜底
}

8.2 大流量场景 Hugepage 与 NUMA 调优

调优项 建议
AF_XDP UMEM 必须使用 2MB 大页(MAP_HUGETLB)
网卡 RSS 队列数 = vCPU 数,每队列独立 RX ring
BPF map 内存 max_entries * value_size 可达数百 MB,调大 RLIMIT_MEMLOCK
SO_REUSEPORT 多进程绑定同一端口,内核自动散列

8.3 DDoS 防护性能对比

实际生产环境(双路 EPYC 7742,Mellanox ConnectX-6 100Gbps):

攻击类型 流量规模 XDP_DROP 性能 iptables DROP
SYN Flood 40Mpps 24Mpps / 单核 0.5Mpps
UDP Flood 35Mpps 28Mpps / 单核 0.3Mpps
DNS Amplification 12Mpps 全速处理 CPU 打满

九、与 DPDK 的取舍

XDP 不是 DPDK 的替代品,而是不同的架构选择:

维度 XDP/eBPF DPDK
包过滤性能 ~24Mpps/核 ~100Mpps/核
吞吐量处理 中等 极高
内核集成 完整协议栈可用 完全旁路
动态更新 bpftool reload 毫秒级 通常需重启
低延迟场景 < 4μs < 1μs
适用场景 防火墙/负载均衡/安全 高频交易/运营商BRAS

最佳实践:两者可以共存。XDP 在入口做 DDoS 清洗,正常流量 XDP_PASS 交给内核或 DPDK 做深度处理。

十、总结与展望

XDP + eBPF 提供了一个高性能、可编程、与内核深度集成的数据面解决方案。它的核心价值在于:

  1. 编程灵活性:相比固定功能 SmartNIC,XDP 可动态加载/卸载逻辑,无需重启
  2. 性能足够:24Mpps 单机处理能力已能抵御绝大多数 DDoS 攻击
  3. 生态集成:与 tc、cgroup、iptables 等无缝协作,形成完整安全策略链

随着 Linux 6.x 内核持续增强 BPF 调度能力和网卡硬件卸载(如 NVIDIA BlueField DPU 的 XDP offload),XDP 正在成为云原生网络栈的标准组件。

# 快速体验三步
clang -O2 -g -Wall -target bpf -c xdp_drop.c -o xdp_drop.o
ip link set dev eth0 xdp obj xdp_drop.o sec xdp
ip link set dev eth0 xdp off

网络层的未来已来——现在就是 XDP 的时代。


关键词:XDP, eBPF, DDoS防护, 零拷贝, AF_XDP, 网络栈, Linux内核 标签:XDP, eBPF, Linux, 内核, 网络安全

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部