标签: Netfilter

  • 深入 nftables 迁移网络黑洞排查:多 Base Chain 语义陷阱与 Docker 流量阻断实战

    近期在接手一批新上线的 Debian 12 宿主机时,遇到了一个极其隐蔽的网络黑洞问题。业务侧反馈,将服务从 CentOS 7 迁移到新环境后,宿主机自身网络一切正常,但 Docker 容器内的所有 Outbound 流量(包括 DNS 解析、外部 API 调用)全部超时。

    简单看一下背景和结论:为了对齐基础安全基线,系统组在新系统上摒弃了老旧的 iptables,转而使用原生的 nftables 编写了主机防火墙策略,并将 forward 链的默认策略设置为 drop故障的根本原因在于对 nftables 的 Base Chain(基础链)和 accept 动作语义理解不到位。 在配合 iptables-nft(Docker 默认的底层网络驱动)工作时,nftables 中不同表里的 Base Chain 会发生叠加。Docker 规则里的 ACCEPT 仅仅中断了当前链的匹配,报文随后又掉进了原生安全策略的 drop 陷阱中。

    把两种时代的产物混用,又不仔细看 Netfilter 底层 Hook 的运转机制,就像在同一个路口安排了两个互不理睬的交警,一个挥手让你走,另一个直接把你的车按死。

    现场还原与报错表现

    排查过程从最基本的抓包开始。在容器内执行 curl 8.8.8.8,同时在宿主机的几个关键网卡上抓包:

    # 容器内 veth 接口看到 SYN 发出,但没有 SYN-ACK
    tcpdump -i veth_xxx -nn host 8.8.8.8
    
    # docker0 网桥上能看到报文进入
    tcpdump -i docker0 -nn host 8.8.8.8
    
    # 物理网卡 eth0 上毫无动静
    tcpdump -i eth0 -nn host 8.8.8.8
    

    报文在路由判决后,准备进行转发(Forward)时凭空消失了。习惯性地敲下 iptables -nL FORWARD,看到 Docker 生成的规则依然健在:

    Chain FORWARD (policy DROP)
    target     prot opt source               destination         
    DOCKER-USER  all  --  0.0.0.0/0            0.0.0.0/0           
    DOCKER-ISOLATION-STAGE-1  all  --  0.0.0.0/0            0.0.0.0/0           
    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
    DOCKER     all  --  0.0.0.0/0            0.0.0.0/0           
    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
    

    表面上看,Docker 已经放行了跨网桥的流量。负责实施的同事一口咬定:“Docker 自己管理的 iptables 规则没有任何问题,肯定是内核路由参数 ip_forward 没开!” 然而 sysctl net.ipv4.ip_forward 明晃晃地显示着 1

    抽丝剥茧:nftables 里的“平行宇宙”

    问题出在哪里?在较新的发行版中,iptables 命令实际上只是 iptables-nft 的一个软链接。Docker 以为自己在操作传统的 iptables,实际上底层被翻译成了 nftables 的规则存入内核。

    此时我们看一眼主机上真正生效的全量规则表:nft list ruleset。 精简后的输出如下:

    # 这是 Docker 经由 iptables-nft 生成的表
    table ip filter {
        chain FORWARD {
            type filter hook forward priority filter; policy drop;
            jump DOCKER-USER
            jump DOCKER-ISOLATION-STAGE-1
            oifname "docker0" ct state related,established counter accept
            oifname "docker0" jump DOCKER
            iifname "docker0" oifname != "docker0" counter accept  # <-- 注意这里,Docker 决定 ACCEPT
            iifname "docker0" oifname "docker0" counter accept
        }
    }
    
    # 这是系统组手写的原生 nftables 主机防火墙
    table inet my_sec_firewall {
        chain base_forward {
            type filter hook forward priority filter; policy drop;  # <-- 这里是罪魁祸首
            ct state established,related accept
            # 这里仅仅放行了部分特定网段的内网互访,没有提及 docker0
        }
    }
    

    这里隐藏着一个巨大的语义陷阱:在旧的 iptables 架构中,一个包在一个 Table/Hook 中如果匹配到了 ACCEPT 规则,它的遍历就彻底结束了,直接进入下一阶段。但在 nftables 架构中,你可以定义无数个挂载在同一 Hook 点的 Base Chain(基础链)。

    上述配置中,ip filterinet my_sec_firewall 都注册了针对 forward hook 的 Base Chain,且优先级都是 filter(数值为 0)。

    当容器的流量进入 Netfilter 的 forward hook 时,发生了什么?

    1. 报文进入 Docker 的 FORWARD 链。

    2. 匹配到 iifname "docker0" oifname != "docker0" counter accept

    3. 关键点来了:在 nftables 中,Base Chain 里的 accept 叫做 Verdict: accept。它的完整语义是“停止遍历当前 Base Chain,允许该包继续走向下一个处于同等或更低优先级的 Base Chain”。

    4. 于是,报文带着 Docker 赐予的“通行证”,继续走进了系统组手写的 base_forward 链。

    5. base_forward 链左看右看,发现这条流量不符合任何放行规则,直接走默认策略 policy drop,报文被无情丢弃。

    这就是典型的“知其然而不知其所以然”。抄袭旧时代的防火墙规范,用新语法包装了一下,结果搞出了网络黑洞。

    现场 Debug 铁证:nftrace 的降维打击

    为了让同事彻底死心并理解这个过程,直接上 nftables 的杀手锏工具 nftrace 进行数据包流向跟踪。

    在我们的防火墙表里加一条 trace 规则:

    nft add rule inet my_sec_firewall base_forward meta nftrace set 1
    

    然后在另一个终端启动监听,并再次在容器内触发 curl 8.8.8.8

    nft monitor trace
    

    日志无情地揭露了报文的死亡现场:

    trace id 75b42d1f ip filter FORWARD packet: iif "docker0" oif "eth0" src 172.17.0.2 dst 8.8.8.8 ...
    trace id 75b42d1f ip filter FORWARD rule iifname "docker0" oifname != "docker0" counter packets 12 bytes 720 accept (verdict accept)
    ...
    trace id 75b42d1f inet my_sec_firewall base_forward packet: iif "docker0" oif "eth0" src 172.17.0.2 dst 8.8.8.8 ...
    trace id 75b42d1f inet my_sec_firewall base_forward rule meta nftrace set 1 (verdict continue)
    trace id 75b42d1f inet my_sec_firewall base_forward verdict drop  # <-- 在这里被默认策略处决!
    

    日志清楚地表明,报文先被 Docker 的链 accept,紧接着落入 my_sec_firewall 的链,并命中 drop 策略。

    解决代码与重构建议

    想要修复这个问题非常简单,既然它要过两道关,那就在原生安全策略里把 Docker 的网桥放行即可:

    nft add rule inet my_sec_firewall base_forward iifname "docker0" accept
    

    但是,作为架构师,这种补丁式的做法是不合格的。 因为 Docker 的网络隔离策略(比如容器间不可见、端口映射暴露限制)本身就非常复杂,如果强行用另一套独立表的策略去叠加,极易造成后续排查的灾难。

    最终的整改落地方案:

    1. 停止混用策略:如果系统中存在需要深度接管底层网络的组件(如 Docker、K8S kube-proxy),主机级的防火墙防护应尽量下放给外部设施(如云厂商的安全组、物理防火墙)。

    2. Hook 优先级规避:必须写本机策略时,确保你的防火墙 Base Chain 优先级数值不要和 Docker 的产生竞争。Docker 默认的 priority 是 0。如果你只做简单的黑名单前置拦截,可以建一个 priority -100 的链;如果你想要兜底,可以建一个 priority 100 的链。

    3. 放弃纯净洁癖:不要在运行了遗留 iptables/Docker 逻辑的机器上,强制推行所谓的“纯原生 nftables 架构”。要么让 Docker 禁用 iptables ("iptables": false in daemon.json) 完全靠你自己手写路由转发,要么老老实实顺从 iptables-nft 的兼容模式,把你的安全规则也用 iptables 语法追加进去。

    总结与排查清单

    在系统底层的迭代中,“兼容”往往是最危险的词汇。iptables-nft 给了一个完美的语法兼容幻觉,却暗改了多链并行的核心逻辑。

    同类问题速查清单:

    1. 辨别真伪 iptables:执行 update-alternatives --display iptables 确认系统当前底层是 iptables-legacy 还是 iptables-nft

    2. 全局视角查规则:抛弃 iptables -nL,排查网络不通时必须看全景:nft list ruleset,重点寻找包含 policy drop 的自定义 Base Chain。

    3. 理解 priority 与 accept 的关系:同一 Hook 点存在多个 Base Chain 时,accept 只是“出当前链”,不是“出整个 Hook”。只有 drop 才是真正的一票否决。

    4. 抓包查死因:如果 tcpdump 看到包进了某网卡但出不来,直接开启 nftables trace (meta nftrace set 1) 跟踪,看包死在哪个 Table 的哪条 Rule,比瞎猜高效百倍。

  • 深入 eBPF/XDP 网络雪崩排查:Netfilter 软中断打满引发的丢包与 XDP 内核级加速防御实战

    高并发下 Netfilter 必然成为性能瓶颈。排查某次网关节点大面积丢包时,确认系海量小包打满 ksoftirqdnf_conntrack 溢出导致。直接抛弃 iptables 方案,通过 eBPF 挂载 XDP 程序在网卡驱动层(SKB 分配前)进行拦截与转发,CPU 软中断开销骤降 80%,99线延迟从 200ms 恢复至 2ms,系统吞吐量提升三个数量级。

    故障现场:ksoftirqd 榨干 CPU 与 Conntrack 溢出

    某次生产环境的高并发突发流量下,K8S Ingress 节点(OS: Ubuntu 22.04, 内核 Linux 5.15)出现大面积请求超时。前端监控显示 99 线延迟飙升至 200ms 以上,甚至出现 502/504 错误。

    登录宿主机,第一眼看系统负载:

    $ uptime
     10:14:32 up 45 days, 14:20,  2 users,  load average: 84.12, 75.33, 60.10
    

    Load Average 极高,敲击键盘都有迟滞感。直接看 CPU 消耗,top 里的 si(Soft Interrupt)指标在多个核心上死死顶在 100%,相关的进程全是 ksoftirqd/n

    %Cpu(s):  1.5 us,  3.2 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi, 95.3 si,  0.0 st
      PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
       14 root      20   0       0      0      0 R  99.9   0.0  10:23.12 ksoftirqd/1
       20 root      20   0       0      0      0 R  99.9   0.0   9:14.05 ksoftirqd/2
    

    与此同时,dmesg 中正在疯狂刷屏经典报错:

    $ dmesg -T | tail -n 5
    [Thu Oct 12 10:15:01 2023] nf_conntrack: nf_conntrack: table full, dropping packet
    [Thu Oct 12 10:15:01 2023] nf_conntrack: nf_conntrack: table full, dropping packet
    

    典型的网络软中断风暴 + 连接跟踪表打满。虽然通过 sysctl -w net.netfilter.nf_conntrack_max=2097152 临时缓解了丢包,但这只是扬汤止沸,软中断依然居高不下,节点的网络栈已经处于半瘫痪状态。

    为什么传统的 iptables/Netfilter 在高并发下必然雪崩?

    要理解这场雪崩,必须拆解 Linux 传统的网络收包路径。

    当网卡收到一个数据包时,硬中断触发后,真正的重头戏在软中断 NET_RX_SOFTIRQ。此时,内核会为每个数据包调用 __alloc_skb() 分配一个 sk_buff 结构体。这个结构体极其庞大(通常包含数百个字段),高频的内存分配和释放本身就是巨大的开销。

    紧接着,包会进入内核协议栈,穿越 Netfilter 的重重关卡(PREROUTING, INPUT, FORWARD 等)。如果是 K8S 环境,kube-proxy 写入的数万条 iptables 规则会以线性或树状(ipset)进行匹配。最致命的是 Conntrack(连接跟踪) 机制。每次建连,内核都要加锁更新连接状态表。当 PPS(每秒包数)达到数十万级别时,nf_conntrack 的自旋锁竞争会导致 CPU 缓存命中率暴跌,最终表现为 ksoftirqd 吃满 CPU,后续的包连 sk_buff 都分配不到,直接在网卡 Ring Buffer 处被丢弃。

    可观测性介入:用 eBPF/bpftrace 精准定位丢包点

    在实施改造前,我们需要硬核的数据佐证。只看 dmesg 不够,到底包是在协议栈的哪一步被 Drop 的? 利用 bpftrace 编写一行脚本,直接 Hook 内核的 kfree_skb 函数(内核丢弃数据包时通常会调用它),并打印调用栈:

    # 依赖环境: bpftrace 0.14.0+
    $ bpftrace -e 'kprobe:kfree_skb /comm == "ksoftirqd/1"/ { @[kstack] = count(); }'
    

    运行 10 秒后 Ctrl+C 停止,输出的核心堆栈如下:

    @[
        kfree_skb+1
        nf_conntrack_in+1345
        ipv4_conntrack_in+28
        nf_hook_slow+66
        ip_rcv+165
        __netif_receive_skb_core+2180
        net_rx_action+354
        __do_softirq+215
        run_ksoftirqd+42
    ]: 45210
    

    数据确凿:短短 10 秒内,在 nf_conntrack_in 链路下触发了 4.5 万次 kfree_skb。传统的防御方案(如加机器、调大 sysctl 参数)在百万级 PPS 面前毫无招架之力。必须进行降维打击——绕过 sk_buff 和 Netfilter。

    降维打击:XDP (eXpress Data Path) 零拷贝拦截实战

    XDP 是基于 eBPF 的一项技术,它允许我们在网卡驱动层,即数据包刚通过 DMA 拷贝到内存,尚未分配 sk_buff 之前,执行我们自定义的 eBPF 程序。

    排查过程中,我们发现异常流量具有明显的端口和 IP 聚集特征。直接编写 XDP 程序,对恶意流量执行 XDP_DROP,对合法突发流量直接在驱动层打标或放行。

    以下是精简后的 XDP C 代码(xdp_filter.c),实现对特定目标端口(如 8080)的异常小包直接 Drop:

    #include <linux/bpf.h>
    #include <linux/if_ether.h>
    #include <linux/ip.h>
    #include <linux/tcp.h>
    #include <linux/in.h>
    #include <bpf/bpf_helpers.h>
    
    SEC("xdp")
    int xdp_drop_prog(struct xdp_md *ctx) {
        // 获取数据包的起止指针
        void *data_end = (void *)(long)ctx->data_end;
        void *data     = (void *)(long)ctx->data;
    
        // 解析以太网头部
        struct ethhdr *eth = data;
        if (data + sizeof(*eth) > data_end)
            return XDP_PASS;
    
        // 仅处理 IPv4
        if (eth->h_proto != bpf_htons(ETH_P_IP))
            return XDP_PASS;
    
        // 解析 IP 头部
        struct iphdr *iph = data + sizeof(*eth);
        if ((void *)iph + sizeof(*iph) > data_end)
            return XDP_PASS;
    
        // 解析 TCP 头部
        if (iph->protocol == IPPROTO_TCP) {
            struct tcphdr *tcph = (void *)iph + sizeof(*iph);
            if ((void *)tcph + sizeof(*tcph) > data_end)
                return XDP_PASS;
    
            // 如果目标端口是 8080,直接在网卡驱动层丢弃 (模拟黑洞)
            if (tcph->dest == bpf_htons(8080)) {
                // 可在此处加入 eBPF Map 统计丢包数量
                return XDP_DROP;
            }
        }
    
        // 其他数据包正常进入协议栈
        return XDP_PASS;
    }
    
    char _license[] SEC("license") = "GPL";
    

    编译与挂载: 利用 Clang 将 C 代码编译为 BPF 字节码,并通过 iproute2 工具直接挂载到宿主机物理网卡(如 eth0)。

    # 编译 (需安装 clang 12+ 和 linux-headers)
    $ clang -O2 -g -Wall -target bpf -c xdp_filter.c -o xdp_filter.o
    
    # 以 Native 模式挂载到 eth0
    $ ip link set dev eth0 xdp obj xdp_filter.o sec xdp
    
    # 查看挂载状态
    $ ip link show eth0
    2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp/id:45 qdisc mq state UP mode DEFAULT group default qlen 1000
        link/ether 00:16:3e:xx:xx:xx brd ff:ff:ff:ff:ff:ff
        prog/xdp id 45 tag 8fxxxxxx
    

    效果对比: 挂载 XDP 后,恶意流量在网卡驱动层即被截断,根本不会触发 alloc_skb,更不会进入 Netfilter。

    • ksoftirqd 的 CPU 占用率从 100% 瞬间暴降至 15% 左右。

    • dmesgnf_conntrack 报错消失。

    • 合法业务流量的 99 线延迟恢复到健康的 2ms 范围内。

    常见问题 (FAQ)

    Q1:XDP 的 Generic 模式和 Native 模式有什么性能差异? Generic 模式(xdpgeneric)是内核网络栈模拟的 XDP,此时 sk_buff 已经分配,性能提升有限,主要用于测试或不支持 XDP 的网卡驱动。Native 模式(xdp)是在网卡驱动层实现,包刚放入内存就触发,零拷贝,性能是 Generic 模式的 4-5 倍。生产环境必须确保网卡驱动(如 ixgbe, mlx5)支持 Native XDP。

    Q2:eBPF Map 并发读写时如何保证数据一致性? 在多核并发场景下,统计包量等操作直接更新普通的 Array/Hash Map 会有竞态问题。应当使用 BPF_MAP_TYPE_PERCPU_ARRAYBPF_MAP_TYPE_PERCPU_HASH。这种 Map 会为每个 CPU 核心维护独立的数据副本,更新时无锁,用户态读取时再遍历所有 CPU 的值进行汇总。

    Q3:使用 Cilium 替换 kube-proxy 后,NodePort 流量依然有延迟,如何排查? Cilium 默认并不全量开启底层 XDP 加速。如果 NodePort 流量仍有延迟,需检查 Cilium Agent 配置是否启用了 bpf-node-portkube-proxy-replacement=strict。可以通过 cilium status 查看 XDP 加速状态,并使用 cilium bpf nat list 确认底层的 eBPF NAT 表是否正常接管了 iptables 规则。如果网卡不支持 Native XDP,Cilium 会退化到 TC (Traffic Control) 层的 eBPF hook,性能会打折扣。

  • 深入 eBPF/XDP 实战:从 Netfilter 软中断打满看 XDP 快速拦截与 kfree_skb 丢包追踪

    传统 iptables/Netfilter 在千万级 PPS 场景下必然成为软中断杀手,协议栈过深的遍历路径是高并发网关的性能毒药。本文直接给出基于 eBPF/XDP 的网络防刷与加速方案,在网卡驱动层(甚至硬件卸载)直接丢弃恶意包,将 CPU si 开销降低 80%,并结合 tracepoint:skb:kfree_skb 彻底终结内核丢包“黑盒”排查。

    案发现场:Netfilter 成为性能瓶颈

    某次生产环境流量突增,某业务 Ingress 网关(Ubuntu 22.04, Kernel 5.15.0-88-generic)QPS 并没有成倍放大,但 P99 延迟直接从 20ms 飙升到了 500ms,部分节点甚至出现 SSH 登录卡顿。

    第一反应看负载,直接上 mpstat -P ALL 1,发现网卡队列绑定的几个 CPU 核心 si(SoftIRQ)直接被打满到了 100%。

    抓取热点函数 perf top -a,霸榜的调用链异常清晰:

      18.52%  [kernel]  [k] nf_hook_slow
      15.21%  [kernel]  [k] ip_rcv
      12.33%  [kernel]  [k] kmem_cache_alloc
      10.14%  [kernel]  [k] __netif_receive_skb_core
    

    典型的 CC 攻击/恶意扫段特征。大量无效的小包涌入,虽然在 iptables/Netfilter 层面配置了 DROP 规则,但由于 iptables 挂载在 PREROUTING 等 Hook 点,数据包走到这里时,内核已经为每一个包分配了 sk_buff 结构体,并走完了复杂的 L2 和 L3 早期协议栈处理

    在动辄几百万 PPS 的冲击下,频繁的 kmem_cache_alloc 和 Netfilter 规则链遍历直接榨干了 CPU。我们需要在更底层“掐断”这些流量。

    为什么 XDP 能在千万级 PPS 下实现防刷降级?

    常规的数据包接收路径是:网卡 -> DMA 拷贝到 Ring Buffer -> 触发硬中断 -> NAPI 轮询拉取 -> 分配 sk_buff -> __netif_receive_skb_core -> 网络协议栈 (Netfilter/IP/TCP 等)。

    XDP(eXpress Data Path)之所以快,根本原因在于它的 Hook 点位于 网络驱动层分配 sk_buff 之前。 当网卡通过 DMA 将数据放入内存后,XDP BPF 程序直接读取这段连续的原始内存(xdp_md),如果是恶意包,直接返回 XDP_DROP,网卡驱动会原地回收页面。没有 skb 内存分配,没有协议栈解析,没有上下文切换。

    XDP 黑名单拦截实战代码

    我们使用 BPF Map 来维护一个高频攻击 IP 黑名单,在 XDP 层直接匹配并丢弃。 以下是精简后的核心 C 代码(xdp_drop.c):

    #include <linux/bpf.h>
    #include <linux/in.h>
    #include <linux/if_ether.h>
    #include <linux/if_packet.h>
    #include <linux/if_vlan.h>
    #include <linux/ip.h>
    #include <bpf/bpf_helpers.h>
    
    // 定义一个 BPF Hash Map 存储黑名单 IP
    struct {
        __uint(type, BPF_MAP_TYPE_HASH);
        __uint(max_entries, 10000);
        __type(key, __u32);   // IPv4 Address
        __type(value, __u32); // Drop counter
    } blacklist SEC(".maps");
    
    SEC("xdp")
    int xdp_drop_prog(struct xdp_md *ctx) {
        void *data_end = (void *)(long)ctx->data_end;
        void *data = (void *)(long)ctx->data;
    
        // 边界检查(必须,否则 eBPF 验证器会拒绝加载)
        struct ethhdr *eth = data;
        if ((void *)(eth + 1) > data_end)
            return XDP_PASS;
    
        if (eth->h_proto != __constant_htons(ETH_P_IP))
            return XDP_PASS;
    
        struct iphdr *iph = data + sizeof(struct ethhdr);
        if ((void *)(iph + 1) > data_end)
            return XDP_PASS;
    
        __u32 src_ip = iph->saddr;
    
        // 查询黑名单 Map
        __u32 *value = bpf_map_lookup_elem(&blacklist, &src_ip);
        if (value) {
            __sync_fetch_and_add(value, 1); // 原子递增拦截计数
            return XDP_DROP; // 核心:在驱动层直接丢弃
        }
    
        return XDP_PASS;
    }
    
    char _license[] SEC("license") = "GPL";
    

    编译与挂载:

    # 使用 clang 编译成 BPF 字节码
    clang -O2 -target bpf -c xdp_drop.c -o xdp_drop.o
    
    # 将 XDP 程序挂载到网卡 eth0 (推荐 Native 模式,如果网卡驱动支持)
    ip link set dev eth0 xdp obj xdp_drop.o sec xdp
    
    # 查看挂载状态
    ip link show eth0
    # 输出会包含: prog/xdp id 123 tag xxxxxxx
    

    此时再用 bpftool map 动态向 blacklist 中写入恶意 IP,被拦截的流量完全不会在 CPU si 中泛起波澜,系统 Load 瞬间恢复。

    丢包排查:用 bpftrace 追踪 kfree_skb 黑盒

    在上述流量清洗的过程中,常会遇到业务方反馈:“我的包明明发过去了,为什么网关没收到?”。此时,如果是协议栈内部某处静默丢包(如 MTU 不匹配、TCP 状态机异常、连接跟踪满),用 tcpdump 是看不出所以然的。

    内核丢弃数据包最终都会调用 kfree_skbconsume_skb(正常释放)。利用 eBPF 追踪 kfree_skb 是降维打击。

    在 Kernel 5.15 下,可以直接使用 bpftrace 一行命令定位丢包的确切内核调用栈:

    # 捕获 10 秒内所有因非正常原因丢包的内核栈并统计次数
    bpftrace -e '
    tracepoint:skb:kfree_skb {
        // args->reason 在 5.1x 较新内核引入,可直接区分丢包原因
        @[kstack] = count();
    }
    '
    

    如果你的内核支持 skb_drop_reason(Kernel 5.17+ 完善),甚至可以直接打印出人类可读的丢包枚举值。 在我们的排查过程中,通过上述命令输出了如下聚合栈:

    @[
        kfree_skb+1
        tcp_v4_rcv+1452
        ip_protocol_deliver_rcu+54
        ip_local_deliver_finish+108
        __netif_receive_skb_one_core+138
        process_backlog+164
        __napi_poll+42
        net_rx_action+582
    ]: 2450
    

    一针见血,包是在 tcp_v4_rcv 中被丢弃的。结合代码和偏移量,立刻定位到是处于 TIME_WAIT 状态的 socket 堆积,导致 PAWS(Protect Against Wrapped Sequence numbers)校验失败,触发了静默丢包。调整 net.ipv4.tcp_tw_reuse 和时间戳设置后,问题迎刃而解。没有 eBPF,这个问题在海量流量下排查至少需要拔几根头发。

    常见问题 (FAQ)

    Q1:XDP 有 Native 和 Generic 两种模式,性能差异多大? Native 模式下,XDP BPF 代码直接嵌入在网卡驱动的 NAPI poll 循环中执行,性能极高(线速丢包可达 10M~20M PPS)。而 Generic 模式(xdpgeneric)是作为回退方案,挂载在 sk_buff 分配之后、协议栈处理之前,性能大打折扣,失去了 XDP “零分配”的核心优势。实战中,如果网卡驱动(如 ixgbe, i40e, mlx5)支持,务必使用 Native 模式(xdpdrv)。

    Q2:加载 XDP 字节码时报错 bpf verifier errors,提示越界访问,怎么解决? eBPF 内核验证器(Verifier)极其严格,采用“防御性加载”策略。如果你在 C 代码中解析 IP 头部,但没有在使用指针前做边界检查(例如 if ((void *)(iph + 1) > data_end) return XDP_PASS;),验证器会认为该程序可能引发 Kernel Panic 并拒绝加载。必须为每一次网络包头部偏移读取增加严格的 data_end 边界校验。

    Q3:网关已经部署了 Cilium (基于 eBPF/XDP),我自己挂载的 XDP 会冲突吗? 会冲突。一个网卡的 RX 队列在同一时间点通常只能挂载一个 XDP 程序。如果强制挂载,后者的会覆盖前者,导致 Cilium 的网络路由与策略失效。在较新的内核中可以使用 libxdp 提供的多程序链(Multi-prog dispatcher)机制,将多个 XDP 程序按优先级串联(如将你的防刷 XDP 作为优先级最高的程序执行,如果 XDP_PASS,再交由 Cilium 的 XDP 程序处理)。

    Q4:为什么不用 TC (Traffic Control) BPF 做拦截? TC BPF 也是极好的网络控制点(支持 Ingress 和 Egress 双向),且能获取完整的 skb 上下文,功能比 XDP 更丰富(比如修改包长、克隆重定向)。但 TC Hook 点位于 skb 分配之后。如果你的首要目标是应对 L3/L4 层的洪水攻击或极限压榨 CPU 性能,选 XDP;如果是做复杂的流量整形、七层之前的深度负载均衡,选 TC。