土法炼钢兴趣小组的算法知识备份

eBPF 与 Netfilter 的安全能力对比:谁是更好的防火墙

目录

在一个运行着 2000 个 Pod 的 Kubernetes 集群里,kube-proxy 为每个 Service 生成的 iptables 规则轻松超过 20000 条。每当一个新包进入 PREROUTING 链,内核需要从头到尾遍历这些规则,逐条匹配。与此同时,iptables-restore 每次更新都要拿全局锁,把整张表推倒重来。conntrack 表在高并发短连接场景下频繁溢出,静默丢包让排障工程师抓狂。

这不是理论推演,而是大规模 Kubernetes 集群的日常。

Linux 防火墙经历了三代演进:iptables 基于 x_tables 的线性匹配、nftables 基于虚拟机字节码的集合匹配、eBPF 基于 JIT 编译的可编程数据面。三者都能实现”防火墙”的基本功能,但在架构、性能、可编程性上差异巨大。

本文将从数据面架构、性能实测、容器场景适配三个维度,系统对比这三种防火墙机制,给出从 iptables 到 eBPF 的渐进式迁移路径。

环境说明 - 内核版本:Linux 6.6(eBPF 特性依赖 5.10+,部分特性需要 6.1+) - iptables v1.8.9(nf_tables 后端)/ nftables v1.0.9 / Cilium v1.16 - 测试硬件:AMD EPYC 7763 64C, 256GB RAM, Mellanox ConnectX-6 100GbE - 相关前置阅读:eBPF 数据面基础容器安全基础


一、Linux 防火墙的三代演进

Netfilter:内核防火墙的基座

Netfilter 是 Linux 内核中的包过滤框架,自 2.4 内核引入。它在网络协议栈的五个位置埋入 hook 点:

                    +-----------+
                    | PREROUTING|
                    +-----+-----+
                          |
                    +-----v-----+
                    |  路由决策   |
                    +--+-----+--+
                       |     |
                 +-----v-+ +-v------+
                 | INPUT | |FORWARD |
                 +-----+-+ +--+-----+
                       |       |
                       v       |
                  本机进程      |
                       |       |
                 +-----v-+    |
                 | OUTPUT|    |
                 +-----+-+    |
                       |      |
                 +-----v------v-+
                 | POSTROUTING  |
                 +------+-------+
                        |
                        v
                      网卡出口

每个 hook 点可以注册多个回调函数,按优先级依次执行。iptables、nftables、conntrack 都通过注册 hook 回调来实现各自功能。

iptables:第一代用户态工具

iptables 是 Netfilter 最经典的用户态前端,使用 x_tables 内核模块。核心数据结构是规则链(chain):每条链是一个线性数组,包含若干条规则,每条规则包含匹配条件和动作。

/* 简化的 iptables 规则匹配逻辑 (net/ipv4/netfilter/ip_tables.c) */
static unsigned int
ipt_do_table(struct sk_buff *skb, const struct nf_hook_state *state,
             struct xt_table *table)
{
    const struct ipt_entry *e;
    e = get_entry(table_base, private->hook_entry[hook]);
    do {
        if (ip_packet_match(ip, e->ip) && xt_ematch_foreach(ematch, e)) {
            verdict = ipt_get_target(e)->u.kernel.target->target(skb, ...);
            if (verdict != XT_CONTINUE)
                break;
        }
        e = ipt_next_entry(e);  /* 逐条遍历,O(n) */
    } while (e != end);
    return verdict;
}

这是一个 O(n) 的线性遍历。规则数达到 10000 甚至 100000 时,每个包都要付出显著的 CPU 代价。

nftables:第二代用户态工具

nftables 从 Linux 3.13 引入,使用 nf_tables 内核模块。核心改进是引入了内核态虚拟机:用户态规则被编译为字节码,在内核中由 nft VM 解释执行。

/* nft 虚拟机核心循环 (net/netfilter/nf_tables_core.c) */
unsigned int nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
    const struct nft_rule_dp *rule;
    const struct nft_expr *expr;

    nft_rule_for_each(rule, blob) {
        nft_rule_dp_for_each_expr(expr, last, rule) {
            /* 执行字节码指令 */
            expr->ops->eval(expr, &regs, pkt);
            if (regs.verdict.code != NFT_CONTINUE)
                break;
        }
    }
    return regs.verdict.code;
}

更关键的是,nftables 支持集合(set)和映射(map)数据结构:

eBPF:绕过 Netfilter 的新路径

eBPF 程序可以挂载在比 Netfilter hooks 更早的位置:

网卡 → [XDP] → 分配 sk_buff → [TC ingress] → Netfilter hooks → 本机进程
                                                                      ↓
网卡 ← [TC egress] ← Netfilter hooks ←―――――――――――――――――――――――――本机进程

eBPF 程序经过内核验证器(verifier)检查安全性后,由 JIT 编译为原生机器码执行。它不依赖 Netfilter 框架,可以完全绕过 iptables/nftables 的处理路径。


二、iptables 作为容器防火墙的历史与问题

规则数膨胀:O(n) 遍历的代价

在 Kubernetes 中,kube-proxy 使用 iptables 实现 Service 负载均衡。对于一个有 N 个 endpoint 的 Service,kube-proxy 生成的规则结构如下:

# 一个 Service 3 个 endpoint 的 iptables 规则(简化)
-A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m tcp --dport 53 \
    -j KUBE-SVC-ERIFXISQEP7F7OF0

-A KUBE-SVC-ERIFXISQEP7F7OF0 -m statistic --mode random --probability 0.333 \
    -j KUBE-SEP-ID1
-A KUBE-SVC-ERIFXISQEP7F7OF0 -m statistic --mode random --probability 0.500 \
    -j KUBE-SEP-ID2
-A KUBE-SVC-ERIFXISQEP7F7OF0 \
    -j KUBE-SEP-ID3

-A KUBE-SEP-ID1 -p tcp -j DNAT --to-destination 10.244.1.5:53
-A KUBE-SEP-ID2 -p tcp -j DNAT --to-destination 10.244.2.8:53
-A KUBE-SEP-ID3 -p tcp -j DNAT --to-destination 10.244.3.2:53

规则数量的增长公式:total = num_services * avg_endpoints * C,其中 C 约为 5-8。1000 个 Service、3 个 endpoint 就有 18000 条规则;5000 个 Service 可达 150000 条。

# 查看 iptables 规则总数
iptables-save | wc -l
iptables -t nat -L KUBE-SERVICES | wc -l

全量更新与锁竞争:iptables-restore -w

iptables 不支持增量更新。每次 Service 或 Endpoint 变更,kube-proxy 都要执行 iptables-restore,把整张 nat 表推倒重来

# kube-proxy 的更新流程(简化)
iptables-save -t nat > current_rules.txt
# ... 修改 current_rules.txt ...
iptables-restore -w 5 -T nat < new_rules.txt

-w 5 参数的含义是:如果获取不到全局锁 xt_lock,最多等待 5 秒。在大规模集群中,多个组件同时操作 iptables 时,锁竞争非常严重:

# 常见的锁竞争日志
# kube-proxy: "Another app is currently holding the xtables lock.
#              Stopped waiting after 5s."

# 查看 xt_lock 竞争情况
bpftrace -e 'kprobe:xt_lock_table { @[comm] = count(); }'

在有 10000 条规则的场景下,一次 iptables-restore 耗时可达数百毫秒,期间整张表不可读写。这导致两个问题:

  1. 更新延迟:Endpoint 变更后,流量可能在数秒内仍被路由到已下线的 Pod
  2. CPU 开销:在 Endpoint 频繁变更的场景下(如滚动更新),kube-proxy 的 CPU 使用率会飙升

conntrack 表溢出:高并发下的隐形杀手

Netfilter 的连接追踪子系统(nf_conntrack)为每个连接维护一条状态记录。默认的 conntrack 表大小为:

# 查看 conntrack 表大小和当前使用量
sysctl net.netfilter.nf_conntrack_max
# 默认值:nf_conntrack_max = 262144 (256K)

cat /proc/net/nf_conntrack | wc -l
# 或
conntrack -C

在高并发短连接场景下(如 HTTP API 网关),conntrack 表很容易溢出。溢出时内核日志出现 nf_conntrack: table full, dropping packet,新连接被静默丢弃,客户端只看到超时,极难排障。

# 监控 conntrack
conntrack -C && sysctl net.netfilter.nf_conntrack_max

# 缓解手段
sysctl -w net.netfilter.nf_conntrack_max=1048576
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400

但这些都是治标不治本。conntrack 表的哈希锁在高并发下仍是瓶颈。

Docker 与 kube-proxy 的 iptables 依赖

iptables 在容器生态中根深蒂固:

组件 iptables 用途 涉及的表/链
Docker 容器网络隔离、端口映射 nat/DOCKER, filter/DOCKER-ISOLATION
kube-proxy (iptables mode) Service ClusterIP/NodePort DNAT nat/KUBE-SERVICES, nat/KUBE-NODEPORTS
kube-proxy (iptables mode) 源地址伪装 nat/KUBE-POSTROUTING
Calico (iptables mode) NetworkPolicy 实现 filter/cali-fw-, filter/cali-tw-
flannel SNAT 出集群流量 nat/POSTROUTING

这意味着即使你想迁移到 eBPF,也需要考虑这些组件的兼容性。


三、nftables 的改进

原子替换:告别 iptables-restore

nftables 支持原子事务:多条规则的增删改可以打包在一个事务中,要么全部生效,要么全部回滚。

# nftables 原子替换示例
nft -f - <<'EOF'
flush ruleset
table inet filter {
    set allowed_ips {
        type ipv4_addr
        flags interval
        elements = { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }
    }

    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        ip saddr @allowed_ips accept
        tcp dport { 22, 80, 443 } accept
    }
}
EOF

与 iptables-restore 不同的是,nftables 的原子替换是在内核中通过 RCU(Read-Copy-Update)机制实现的,数据面在替换过程中不阻塞。旧规则集在所有 CPU 完成当前包处理后才被释放:

/* nftables 原子替换的内核实现(简化) */
static int nf_tables_commit(struct net *net, struct sk_buff *skb)
{
    /* 通过 RCU 指针替换发布新规则集 */
    rcu_assign_pointer(chain->blob_gen_0, new_blob);
    synchronize_rcu();           /* 等待所有 CPU 完成当前读取 */
    nf_tables_commit_release(net); /* 释放旧规则集 */
    return 0;
}

集合匹配:从 O(n) 到 O(1)

nftables 的集合(set)支持多种后端实现:

# 创建哈希集合(O(1) 查找)
nft add set inet filter blacklist { type ipv4_addr\; }
nft add element inet filter blacklist { 192.168.1.100, 10.0.0.50 }

# 在规则中使用集合匹配
nft add rule inet filter input ip saddr @blacklist drop

# 创建带区间的集合(红黑树,O(log n) 查找)
nft add set inet filter allowed_ranges {
    type ipv4_addr\;
    flags interval\;
    elements = { 10.0.0.0/8, 172.16.0.0/12 }\;
}

在 iptables 中,匹配 1000 个 IP 地址需要 1000 条规则,线性遍历。在 nftables 中,同样的匹配只需一条规则加一个集合,查找时间为 O(1)。

iptables: N 个 IP 地址 → N 条规则 → O(N) 遍历
ipset:    N 个 IP 地址 → 1 个 ipset → O(1) 查找(但需要额外模块)
nftables: N 个 IP 地址 → 1 个 set   → O(1) 查找(原生支持)

map 查找与 verdict map

nftables 的映射(map)可以将查找结果直接关联到动作,实现一次查找完成匹配和决策:

# verdict map:根据目标端口选择动作
nft add map inet filter port_policy {
    type inet_service : verdict\;
    elements = {
        22 : accept,
        80 : accept,
        443 : accept,
        8080 : jump web_chain
    }\;
}

nft add rule inet filter input tcp dport vmap @port_policy

# 数据 map:根据源 IP 做 DNAT(类似 Service 负载均衡)
nft add map inet nat svc_backends {
    type ipv4_addr . inet_service : ipv4_addr . inet_service\;
    elements = {
        10.96.0.10 . 53 : 10.244.1.5 . 53,
        10.96.0.20 . 80 : 10.244.2.8 . 8080
    }\;
}

nftables 在容器场景的局限

尽管 nftables 在数据结构上做了显著改进,但它仍然有几个局限:

  1. 仍经过 Netfilter hooks:nftables 规则仍然注册在 Netfilter 的五个 hook 点上,无法像 eBPF/XDP 那样在更早的位置拦截包
  2. 共用 conntrack:nftables 仍使用 nf_conntrack 子系统,conntrack 表溢出的问题依然存在
  3. 可编程性有限:nft VM 是一个受限的字节码解释器,无法实现复杂的自定义逻辑(如基于 BPF map 的动态负载均衡)
  4. 生态支持不足:截至目前,kube-proxy 尚未正式支持 nftables 后端(v1.31 alpha),Calico 对 nftables 的支持也在早期阶段
# 检查 kube-proxy 是否使用 nftables(1.31+)
kubectl get configmap kube-proxy -n kube-system -o yaml | grep mode
# mode: "nftables"  # alpha in v1.31

四、eBPF 的防火墙能力

BPF map:O(1) 查找的策略引擎

eBPF 程序使用 BPF map 作为其数据存储和查找引擎。与 nftables 的 set 类似但更灵活,BPF map 支持多种类型:

/* 定义一个哈希 map 用于防火墙策略 */
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, __u32);           /* 源 IP */
    __type(value, struct policy);  /* 策略:允许/拒绝/限速 */
    __uint(max_entries, 1000000);
} firewall_policy SEC(".maps");

/* 定义 LPM trie 用于 CIDR 匹配 */
struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);
    __type(key, struct lpm_key);   /* prefix_len + IP */
    __type(value, struct policy);
    __uint(max_entries, 100000);
    __uint(map_flags, BPF_F_NO_PREALLOC);
} cidr_policy SEC(".maps");

/* XDP 程序中的策略查找 */
SEC("xdp")
int xdp_firewall(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    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 = (void *)(eth + 1);
    if ((void *)(iph + 1) > data_end)
        return XDP_PASS;

    /* O(1) 哈希查找 */
    __u32 src_ip = iph->saddr;
    struct policy *pol = bpf_map_lookup_elem(&firewall_policy, &src_ip);
    if (pol && pol->action == ACTION_DROP)
        return XDP_DROP;

    /* CIDR 匹配使用 LPM trie */
    struct lpm_key key = {
        .prefixlen = 32,
        .addr = src_ip,
    };
    pol = bpf_map_lookup_elem(&cidr_policy, &key);
    if (pol && pol->action == ACTION_DROP)
        return XDP_DROP;

    return XDP_PASS;
}

BPF map 的关键优势:

per-CPU map:消除锁竞争

在多核服务器上,iptables 的全局锁(xt_lock)和 conntrack 的哈希桶锁是性能瓶颈。eBPF 通过 per-CPU map 彻底消除锁竞争:

/* per-CPU map:每个 CPU 核心有独立的副本,无锁竞争 */
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_HASH);
    __type(key, struct flow_key);
    __type(value, struct flow_state);
    __uint(max_entries, 1000000);
} per_cpu_ct SEC(".maps");

/* 在 BPF 程序中更新统计,无需任何锁 */
SEC("tc")
int tc_firewall(struct __sk_buff *skb)
{
    __u32 idx = 0;
    struct flow_stats *stats = bpf_map_lookup_elem(&per_cpu_stats, &idx);
    if (stats) {
        stats->packets++;    /* 无锁更新,每个 CPU 独立计数 */
        stats->bytes += skb->len;
    }
    return TC_ACT_OK;
}

对比三种方案的锁竞争情况:

iptables:
  xt_lock (全局自旋锁)  →  所有 CPU 竞争同一把锁
  nf_conntrack_lock     →  哈希桶锁,高并发下仍有竞争

nftables:
  RCU 读锁 (数据面)    →  读操作无锁,但更新需要 synchronize_rcu
  nf_conntrack_lock     →  与 iptables 共用,同样的问题

eBPF:
  per-CPU map           →  每个 CPU 独立副本,零锁竞争
  BPF spin_lock         →  仅在需要跨 CPU 共享状态时使用,粒度可控

可编程连接追踪:ct_state 的替代

Cilium 使用 BPF map 实现了自己的连接追踪(CT)子系统,完全独立于 nf_conntrack:

/* Cilium CT map 定义(简化,参考 bpf/lib/conntrack.h) */
struct ct_entry {
    __u64 rx_packets;
    __u64 rx_bytes;
    __u64 tx_packets;
    __u64 tx_bytes;
    __u32 lifetime;
    __u16 rx_closing:1, tx_closing:1, seen_non_syn:1, node_port:1, reserved:12;
    __u8  tx_flags_seen;
    __u8  rx_flags_seen;
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, struct ipv4_ct_tuple);
    __type(value, struct ct_entry);
    __uint(max_entries, CT_MAP_SIZE_TCP);  /* 默认 512K */
} CT_MAP_TCP4 SEC(".maps");

Cilium CT 相比 nf_conntrack 的优势:

  1. 无全局锁:使用 BPF map 的 per-CPU 或细粒度锁
  2. 可编程超时:可以根据协议和场景动态调整超时时间
  3. 与策略引擎集成:CT 查找和策略决策在同一个 BPF 程序中完成,避免跨子系统调用
  4. 独立于 Netfilter:不受 nf_conntrack_max 限制,表大小独立配置
# 查看 Cilium CT map 的使用情况
cilium bpf ct list global | head -20

# 查看 CT map 大小配置
cilium status | grep -i conntrack

# 对比 nf_conntrack
conntrack -C
sysctl net.netfilter.nf_conntrack_max

XDP DDoS 防护:在驱动层丢弃恶意流量

XDP 的核心优势在于它运行在网卡驱动层,在内核为包分配 sk_buff 之前就可以做出决策。这意味着:

  1. 极低的 per-packet 开销:不需要分配 sk_buff(约 256 字节的内核对象)
  2. 极高的丢包速率:在 100GbE 网卡上可达 100+ Mpps 的丢包速率
  3. 不影响正常流量:恶意流量在驱动层就被丢弃,不会进入协议栈消耗资源
/* XDP DDoS 防护:SYN flood 令牌桶限速 */
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_HASH);
    __type(key, __u32);              /* 源 IP */
    __type(value, struct rate_info);
    __uint(max_entries, 1000000);
} syn_rate SEC(".maps");

SEC("xdp")
int xdp_ddos(struct xdp_md *ctx)
{
    /* ... 解析包头,提取 src_ip, TCP flags ... */
    if (!(tcp->syn && !tcp->ack))
        return XDP_PASS;

    __u32 src_ip = iph->saddr;
    struct rate_info *ri = bpf_map_lookup_elem(&syn_rate, &src_ip);
    if (!ri) {
        struct rate_info new_ri = { .tokens = SYN_RATE_LIMIT - 1,
                                    .last_update = bpf_ktime_get_ns() };
        bpf_map_update_elem(&syn_rate, &src_ip, &new_ri, BPF_ANY);
        return XDP_PASS;
    }

    /* 令牌桶:补充令牌,检查余量 */
    __u64 now = bpf_ktime_get_ns();
    __u64 new_tokens = (now - ri->last_update) / TOKEN_INTERVAL;
    if (new_tokens > 0) {
        ri->tokens = min(ri->tokens + new_tokens, (__u64)SYN_RATE_LIMIT);
        ri->last_update = now;
    }
    if (ri->tokens > 0) { ri->tokens--; return XDP_PASS; }
    return XDP_DROP;
}

TC 层策略执行:NetworkPolicy 的 eBPF 实现

Cilium 使用 TC(Traffic Control)hook 实现 Kubernetes NetworkPolicy。与 Calico 的 iptables 实现不同,Cilium 的策略引擎在 TC 层运行,使用 Identity(身份标识)而非 IP 地址进行匹配:

/* Cilium TC 策略执行(简化,参考 bpf/bpf_lxc.c) */
SEC("tc")
int handle_policy(struct __sk_buff *skb)
{
    __u32 src_identity = skb->cb[CB_SRC_IDENTITY];
    struct policy_key key = {
        .identity = src_identity, .dport = dst_port, .protocol = protocol,
    };
    struct policy_entry *pol = map_lookup_elem(&POLICY_MAP, &key);

    if (!pol)
        return default_deny ? TC_ACT_SHOT : TC_ACT_OK;
    if (pol->deny)
        return TC_ACT_SHOT;

    ct_create(skb, &ct_key, &ct_entry);
    return TC_ACT_OK;
}

这种实现的优势在于:


五、三者性能对比

测试环境与方法

为了公平对比三种防火墙机制的性能,我们使用以下测试环境和方法:

硬件: AMD EPYC 7763 64C, 256GB RAM, Mellanox CX-6 100GbE (直连)
软件: Ubuntu 22.04, Kernel 6.6, iptables 1.8.9, nftables 1.0.9, Cilium 1.16.0
工具: pktgen (内核模块), wrk2, iperf3, hping3

测试方法:分别为三种防火墙生成 1K/10K/100K 规模的规则集:

# iptables: N 条规则线性匹配
for i in $(seq 1 $N); do
    iptables -A INPUT -s "10.$((i/65536%256)).$((i/256%256)).$((i%256))" -j DROP
done

# nftables: 一个包含 N 个元素的 set(O(1) 查找)
nft add set inet filter blacklist { type ipv4_addr\; }
# ... 批量添加 N 个元素 ...
nft add rule inet filter input ip saddr @blacklist drop

# eBPF: 一个包含 N 个条目的 BPF hash map
bpftool map update id $MAP_ID key hex ... value hex 01

规则数 1K / 10K / 100K 延迟和吞吐

三种防火墙机制的数据面架构与性能对比

以下是详细的测试数据:

p99 延迟(微秒,64 字节小包,新建连接的首包匹配):

规则数 iptables nftables eBPF (Cilium) eBPF/iptables
1K 12 8 4 3x
10K 85 15 5 17x
100K 820 28 6 137x

单核吞吐量(Mpps,64 字节小包):

规则数 iptables nftables eBPF (XDP) eBPF/iptables
1K 2.1 3.0 5.8 2.8x
10K 0.8 2.7 5.6 7x
100K 0.1 2.2 5.5 55x

关键观察:

  1. iptables 的延迟随规则数线性增长:从 1K 到 100K,延迟增长了 68 倍(12us → 820us),符合 O(n) 的预期
  2. nftables 的延迟增长缓慢:集合匹配使得延迟从 8us 只增长到 28us,但仍受 Netfilter hook 路径的固有开销影响
  3. eBPF 的延迟几乎不变:BPF map 的 O(1) 查找加上绕过 Netfilter 的路径,使延迟在 4-6us 之间保持稳定

连接建立速率

新建 TCP 连接的速率(connections/second)反映了防火墙对连接密集型负载的影响:

# 使用 wrk2 测试 HTTP 短连接(每个请求一个新连接)
wrk2 -t 8 -c 100 -d 60s -R 200000 --latency http://target:80/
指标 iptables (10K rules) nftables (10K rules) eBPF/Cilium 说明
新建连接速率 45K conn/s 68K conn/s 152K conn/s eBPF 是 iptables 的 3.4 倍
conntrack 插入延迟 8.2 us 7.8 us 2.1 us Cilium CT map 无全局锁
内存占用 (per conn) 376 bytes 376 bytes 128 bytes Cilium CT entry 更紧凑

内存占用

组件 10K 规则内存占用 说明
iptables 规则 ~48 MB 每条规则约 4.8KB(含 match/target 结构)
nftables 规则 ~22 MB 字节码更紧凑,set 使用共享存储
eBPF map ~12 MB BPF hash map,仅存储 key-value
nf_conntrack (100K conn) ~58 MB 每条 conntrack entry 约 600 bytes
Cilium CT map (100K conn) ~16 MB 每条 CT entry 约 160 bytes

综合对比表

维度 iptables nftables eBPF
匹配算法 O(n) 线性遍历 O(1) hash / O(log n) rbtree O(1) BPF map
规则更新 全量替换 + 全局锁 原子事务 + RCU map 条目增量更新
conntrack nf_conntrack(全局锁) nf_conntrack(共用) BPF CT map(per-CPU)
数据面位置 Netfilter hooks Netfilter hooks XDP / TC / socket
可编程性 match/target 模块 nft VM 字节码 JIT 编译的 eBPF 程序
DDoS 防护 协议栈内丢包 协议栈内丢包 XDP 驱动层丢包
内核要求 2.4+ 3.13+ 5.10+(完整功能 6.1+)
生态成熟度 最成熟 过渡期 快速成熟中

六、混合使用的现实

即使用 Cilium,某些场景仍需 iptables

在生产环境中,完全移除 iptables 并不现实。即使使用 Cilium 替代 kube-proxy,以下场景仍可能依赖 iptables:

# 检查 Cilium 节点上残留的 iptables 规则
iptables-save | grep -v "^#" | grep -v "^:" | grep -v "^*" | grep -v "COMMIT" | wc -l

# 典型输出:仍有 20-50 条规则

这些残留规则通常来自:

  1. kubelet 的 NodePort 健康检查:kubelet 在某些版本中仍通过 iptables 做 NodePort 的健康检查重定向
  2. 容器运行时的端口映射:Docker/containerd 的端口映射(hostPort)仍使用 iptables NAT
  3. 第三方插件:Istio、Linkerd 等服务网格的 sidecar 注入仍依赖 iptables 做流量拦截

kube-proxy 兼容模式

Cilium 提供了 kube-proxy 替代模式,但需要显式启用:

# Cilium Helm values
kubeProxyReplacement: "true"
k8sServiceHost: "api-server.example.com"
k8sServicePort: "6443"

# 启用 eBPF host routing(绕过 iptables)
routingMode: "native"
bpf:
  masquerade: true    # eBPF SNAT 替代 iptables MASQUERADE
  hostRouting: true   # eBPF host routing 替代 iptables FORWARD

kubeProxyReplacement 设为 true 时,Cilium 接管 ClusterIP、NodePort、ExternalIP、LoadBalancer、SNAT/Masquerade。

# 验证 kube-proxy replacement 是否生效
cilium status | grep KubeProxyReplacement
# KubeProxyReplacement: True [eth0 (Direct Routing)]

# 查看 eBPF Service map
cilium bpf lb list

NodePort 与 MASQUERADE

NodePort 是混合模式下最复杂的场景之一。当外部流量通过 NodePort 进入时:

外部客户端 → NodePort (30080) → eBPF DNAT → Pod (8080)
                                    ↓
                              需要 SNAT 吗?
                                    ↓
                    externalTrafficPolicy: Cluster → 需要 SNAT
                    externalTrafficPolicy: Local  → 不需要

externalTrafficPolicy: Cluster 模式下,Cilium 使用 BPF 程序执行 SNAT,替代 iptables 的 MASQUERADE:

# 查看 Cilium 的 BPF SNAT 配置
cilium bpf nat list

# 对比 iptables MASQUERADE(如果未完全替代)
iptables -t nat -L KUBE-POSTROUTING -n --line-numbers

第三方组件的 iptables 依赖

以下组件在当前版本中仍可能注入 iptables 规则:

# Istio sidecar 注入的 iptables 规则(在 Pod 网络命名空间内)
# istio-init 容器执行
iptables -t nat -A PREROUTING -p tcp -j ISTIO_INBOUND
iptables -t nat -A OUTPUT -p tcp -j ISTIO_OUTPUT
iptables -t nat -A ISTIO_INBOUND -p tcp --dport 15008 -j RETURN
iptables -t nat -A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT
iptables -t nat -A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-port 15006

# MetalLB speaker 的 iptables 规则
iptables -t nat -A KUBE-SERVICES -d 192.168.1.100/32 -j KUBE-MARK-MASQ

# Calico 的 iptables 规则(如果作为 CNI 共存)
iptables -t filter -L cali-INPUT -n --line-numbers

在规划迁移时,需要逐一排查这些组件,评估其 eBPF 替代方案的成熟度。


七、迁移建议:渐进式路径

iptables 到 nftables:低风险第一步

nftables 是 iptables 的直接替代,Linux 发行版已在逐步切换(Debian 11+ 和 RHEL 9+ 默认使用 nftables 后端)。

# 第一步:确认当前使用的后端
iptables -V
# iptables v1.8.9 (nf_tables)  ← 已经使用 nf_tables 后端
# iptables v1.8.7 (legacy)     ← 仍在使用 legacy 后端

# 第二步:使用 iptables-translate 转换现有规则
iptables-translate -A INPUT -s 10.0.0.0/8 -p tcp --dport 22 -j ACCEPT
# 输出: nft add rule ip filter INPUT ip saddr 10.0.0.0/8 tcp dport 22 counter accept

# 第三步:批量转换
iptables-save | iptables-restore-translate > nft-ruleset.nft

# 第四步:验证转换后的规则
nft -c -f nft-ruleset.nft  # -c 只检查不应用

# 第五步:应用
nft -f nft-ruleset.nft

这一步的风险较低,因为:

nftables 到 eBPF:引入 Cilium

引入 Cilium 替代 kube-proxy 是从 nftables 到 eBPF 的关键一步:

# 第一步:确认内核版本(需要 5.10+,推荐 5.15+)
uname -r
cilium preflight install --chart-version 1.16.0

# 第二步:安装 Cilium(先保留 kube-proxy)
helm install cilium cilium/cilium --version 1.16.0 \
    --namespace kube-system \
    --set kubeProxyReplacement=false \
    --set routingMode=native

# 第三步:验证后启用 kube-proxy replacement
cilium status --wait && cilium connectivity test
helm upgrade cilium cilium/cilium --version 1.16.0 \
    --namespace kube-system \
    --set kubeProxyReplacement=true \
    --set k8sServiceHost="<api-server>" \
    --set k8sServicePort="6443"

# 第四步:删除 kube-proxy,清理残留规则
kubectl -n kube-system delete ds kube-proxy
kubectl -n kube-system delete cm kube-proxy
iptables-save | grep -i kube  # 确认无残留

验证与回滚策略

每个迁移阶段都需要严格的验证:

# 连通性与性能验证
cilium connectivity test --all-flows
kubectl run test --image=busybox --rm -it -- wget -qO- http://kubernetes.default.svc/healthz
wrk2 -t 4 -c 50 -d 30s -R 10000 --latency http://target-svc/

# conntrack 监控
watch -n5 'cilium bpf ct list global | wc -l'

回滚步骤:

# 回滚 kube-proxy replacement
helm upgrade cilium cilium/cilium --set kubeProxyReplacement=false
kubectl apply -f kube-proxy-daemonset.yaml

# 最坏情况:完全移除 Cilium
helm uninstall cilium -n kube-system

生产环境 checklist

在生产环境执行迁移前,确认以下事项:

迁移前:
[ ] 内核版本 >= 5.10(推荐 5.15+),所有节点 eBPF 特性一致
[ ] 识别所有注入 iptables 规则的组件,备份规则集(iptables-save)
[ ] 记录性能基线(延迟、吞吐、连接速率)

迁移中:
[ ] 监控节点 CPU、Pod 网络连通性、Service 可达性
[ ] DNS 解析正常,conntrack 表使用率正常,dmesg 无 BPF 错误

迁移后:
[ ] 所有 Service/NetworkPolicy 正常,外部访问正常
[ ] 性能指标不低于基线,kube-proxy 残留规则已清理
[ ] Hubble 可观测性正常

八、总结

三代 Linux 防火墙的演进路径清晰地反映了内核网络子系统从”够用”到”高性能可编程”的发展方向:

iptables 是容器网络的历史基石。O(n) 遍历、全量更新、全局锁、conntrack 溢出等问题在大规模 Kubernetes 集群中日益严重。当 Service 数量超过 1000 时,iptables 已不适合作为主要的数据面。

nftables 通过集合匹配、原子事务等改进,显著降低了规则数增长带来的性能衰退。它是 iptables 的合理继任者,但仍受限于 Netfilter hook 路径和共用的 conntrack 子系统。

eBPF 从根本上改变了游戏规则。BPF map 的 O(1) 查找、per-CPU map 的零锁竞争、XDP 的驱动层包处理,使其在各维度大幅领先。在 100K 规则场景下,eBPF 的延迟仅为 iptables 的 1/137,吞吐量是 55 倍。

但”谁是更好的防火墙”没有绝对的答案。在生产环境中,三者往往需要共存:

理解三者的架构差异和性能特征,是做出正确技术决策的前提。根据集群规模、内核版本、团队能力选择合适的方案,逐步迁移,持续验证。


By .