大带宽服务器DDoS防护进阶:手写XDP程序在驱动层秒杀攻击包,吞吐量损失不到1%

背景:iptables太慢?试试网卡驱动层过滤

面对UDP Flood、SYN Flood等DDoS,传统iptables规则全挂在Netfilter钩子(MAC层之后)。当包量超过千万pps时,软中断飙升、CPU耗尽,轻则丢合法包,重则整机挂掉。XDP(eXpress Data Path)在网卡驱动层直接接管,绕过内核协议栈,丢包仅需几条指令,延迟纳秒级。轻云互联的大带宽服务器配备多队列千兆/万兆网卡,原生支持XDP offload,无需额外虚拟化开销。

准备工作:编译环境与内核选项

  • 安装clang、llvm、libbpf-dev、linux-tools-common
  • 确保内核支持 CONFIG_BPF=yCONFIG_XDP_SOCKETS=y(大多数5.x+默认开启)
  • 查看网卡XDP支持:ethtool -i eth0 | grep xdp

实战:编写一个UDP Flood过滤器(C语言)

// ddos_xdp_kern.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>

#define MAX_UDP_LEN 1500   // 合法UDP包最大长度(含头部)

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

    if ((void *)eth + sizeof(*eth) > data_end)
        return XDP_PASS;

    __u16 h_proto = eth->h_proto;
    // 只处理IPv4
    if (h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;

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

    // 只处理UDP
    if (ip->protocol != IPPROTO_UDP)
        return XDP_PASS;

    struct udphdr *udp = (void *)ip + (ip->ihl * 4);
    if ((void *)udp + sizeof(*udp) > data_end)
        return XDP_PASS;

    // 计算UDP长度
    __u16 udp_len = ntohs(udp->len);
    if (udp_len > MAX_UDP_LEN) {
        // 丢弃超长UDP包(常见放大攻击特征)
        return XDP_DROP;
    }

    // 可添加更多特征:检查源端口53、NTP等
    // if (ntohs(udp->source) == 53) return XDP_DROP;

    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

编译与加载

clang -O2 -target bpf -c ddos_xdp_kern.c -o ddos_xdp_kern.o
ip link set dev eth0 xdp obj ddos_xdp_kern.o sec xdp

# 验证加载成功
ip link show dev eth0 | grep xdp

冷门技巧:动态更新过滤规则而不卸载XDP

实战中攻击特征会变,重启XDP程序会导致短暂断流。使用BPF MAP存储黑名单IP/端口,通过用户态程序更新MAP即可在线切换规则。

// 在XDP程序内加一个BPF_MAP_TYPE_HASH
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 100000);
    __type(key, __u32);  // 源IP
    __type(value, __u8); // 0(放行) / 1(丢弃)
} blacklist SEC(".maps");

// 在判断中查MAP
__u32 *saddr = &ip->saddr;
__u8 *val = bpf_map_lookup_elem(&blacklist, saddr);
if (val && *val == 1)
    return XDP_DROP;

用户态程序通过libbpf将攻击源IP写入MAP,实时生效,无需重启XDP。

性能调优:搭配RSS避免单个CPU被淹没

XDP程序运行在网卡中断上,若所有流量都到同一CPU,再快也扛不住。务必配置RSS(Receive Side Scaling)哈希键为五元组,使流量分散到多核:

ethtool -X eth0 hkey 00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff
ethtool -X eth0 equal 16    # 根据队列数调整

再结合XDP上运行的CPU亲和性,确保每个队列中断与对应CPU绑定,避免跨核cache污染。

与iptables联手:白名单放行重要流量

XDP粗暴丢弃会误伤正常业务。建议做法:在XDP中只丢弃明显攻击包(如放大特征、超长包),然后将剩余流量交回内核,用iptables做精细放行。轻云互联的服务器支持“XDP_PASS + conntrack + syncookie”混合模式,实测在30Mpps攻击下合法业务完好。

# iptables白名单示例
iptables -A INPUT -p tcp --dport 443 -m connlimit --connlimit-above 100 --connlimit-mask 32 -j DROP
iptables -A INPUT -p udp --sport 53 -m string --string "\\x00\\x00\\x00\\x01" --algo bm -j DROP

真实案例:某流媒体平台抗40Mpps UDP Flood

客户使用轻云互联E5-2680v4大带宽服务器,单网卡40Gbps。原本依赖云高防,回源成本高。加载自写XDP程序后,在驱动层直接过滤掉所有非业务端口的超大UDP包,CPU占用从85%降至12%,合法视频流零丢包。注意编译时使用-O2,避免分支预测影响。

排错:如何确认XDP生效

# 统计丢包数
ethtool -S eth0 | grep rx_xdp_drop
# 观察软中断分布
cat /proc/softirqs | grep NET_RX
# 实时包量
sar -n DEV 1

结语

别再做iptables的奴隶。在驱动层写几行C,就能用1%的CPU损失换十倍抗压能力。需要完整用户态MAP更新脚本?评论区见。