记一次 Cilium VXLAN 全零 MAC 丢包问题的排查与修复

本文所有 IP、MAC、节点名、网卡名均为脱敏后的示意值,仅用于说明网络结构与排查思路,不代表真实生产环境。

背景

某次接到反馈:从公有云上访问某边缘 K8s 集群的推理服务不通

网络架构上有两条专线(WireGuard 隧道)链路,分别接到集群内两个不同节点:

  1. 公有云 VM-1 → WireGuard 隧道-1 → 集群的 edge-gw 节点(一个不在 K8s 集群内的网关节点) ✅
  2. 公有云 VM-2 → WireGuard 隧道-2 → 集群的 master 节点(K8s 控制面节点) ❌

诡异的是:两条链路打到的后端其实是同一个推理服务 Pod,一条通、一条不通。这种"对称中又不对称"的现象,往往最耗时间——因为它会反复推翻你的假设。

整体拓扑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
                    ┌──────────── 公有云 ────────────┐
│ │
│ VM-1 VM-2 │
│ (client) (client) │
└─────┬──────────────────┬────────┘
│ │
WG 隧道-1 (udp) WG 隧道-2 (udp)
│ │
┌─────────────────────────┼──────────────────┼──────────────────────────────┐
│ │ │ │
│ ┌──────────────────────▼──────┐ ┌─────────▼──────────────┐ │
│ │ edge-gw 节点 (harbor) │ │ master 节点 (K8s 控制面)│ │
│ │ 不在 K8s 集群 / 无 cilium │ │ 有 cilium │ │
│ │ │ │ │ │
│ │ wg-tunnel-1 │ │ wg-tunnel-2 ◄─ 问题网卡 │
│ │ (L3/ARPHRD_NONE, │ │ (L3/ARPHRD_NONE, │ │
│ │ type=65534, 无 MAC) │ │ type=65534, 无 MAC) │ │
│ │ │ │ ┌──────────────────┐ │ │
│ │ 无 cilium BPF attach │ │ │ cilium BPF attach │ │ │
│ │ → 走 Linux 静态路由 │ │ │ (cil_from_netdev │ │ │
│ │ │ │ │ -wg-tunnel-2) │ │ │
│ │ cilium_host: — │ │ │ DNAT+SNAT 抢在路由 │ │ │
│ │ bond0: 192.168.10.2 │ │ │ 查找之前执行 │ │ │
│ └──────────────┬─────────────┘ │ └────────┬─────────┘ │ │
│ │ │ │ │ │
│ 命中静态路由 │ 直接转发 │ DNAT: VIP2→Pod │ │
│ (未进 overlay) │ SNAT: →172.18.2.240 │ │
│ │ │ │ │ │
│ │ │ ┌────────▼─────────┐ │ │
│ │ │ │ cilium_vxlan VTEP│ │ │
│ │ │ │ 二次封装进 VXLAN │ │ │
│ │ │ │ 内层以太网 MAC │ │ │
│ │ │ │ = 全零 00:00:... │ ◄─ 根因 │
│ │ │ └────────┬─────────┘ │ │
│ │ │ │ │ │
│ │ │ bond0: 192.168.10.192 │ │
│ │ └───────────┼───────────────┘ │
│ │ │ │
│ │ VXLAN(udp/8472) │ VXLAN(udp/8472) │
│ │ overlay │ overlay │
│ │ │ │
│ ┌─────────────▼─────────────┐ ┌──────────▼──────────────┐ │
│ │ gpu-171 节点 │ │ gpu-119 节点 │ │
│ │ bond0: 192.168.10.171 │ │ bond0: 192.168.10.119 │ │
│ │ 有 cilium │ │ 有 cilium │ │
│ │ │ │ │ │
│ │ 收到的 VXLAN 内层 │ │ 收到的 VXLAN 内层 │ │
│ │ MAC 有效 (aa:bb:cc:..:03) │ │ MAC 全零 (00:00:..:00) │ │
│ │ → vxlan_rcv 解封装成功 │ │ → vxlan_rcv 解封装 │ │
│ │ → 交付到 Pod │ │ kfree_skb 丢弃 ✗ │ ◄─ 丢包点 │
│ └──────────────┬────────────┘ └─────────────────────────┘ │
│ │ │
└──────────────────┼────────────────────────────────────────────────────────┘

┌────────▼──────────────────────────────┐
│ K8s 集群 (cilium overlay, VXLAN 模式) │
│ │
│ MetalLB LoadBalancer Service │
│ ┌──────────────────────────────────┐ │
│ │ 推理服务 (两条 VIP 同一后端 Pod) │ │
│ │ VIP1 10.20.1.5 → Pod 172.18.5.34 │ │ ← Link1 用 ✅
│ │ VIP2 10.20.2.1 → Pod 172.18.5.34 │ │ ← Link2 用 ❌
│ └──────────────────────────────────┘ │
│ ┌──────────────────────────────────┐ │
│ │ nginx 测试服务 (复刻拓扑) │ │
│ │ VIP1 10.20.1.7 → Pod 172.18.5.17 │ │ ← Link1 用 ✅
│ │ VIP2 10.20.2.2 → Pod 172.18.5.17 │ │ ← Link2 用 ❌
│ │ (Pod 固定在 gpu-119) │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘

两条链路对照

Link 1 ✅ Link 2 ❌
公有云客户端 VM-1 VM-2
WireGuard 隧道 wg-tunnel-1 wg-tunnel-2
落地节点 edge-gw (harbor,非 K8s) master (K8s 控制面)
该节点是否有 cilium
wg 网卡上是否有 cilium BPF 有(提前 DNAT+SNAT)
流量走向 命中静态路由,直接转发往 gpu-171 被 BPF 拐进 cilium_vxlan overlay 去 gpu-119
VXLAN 内层以太网 MAC 有效 全零
对端处理 vxlan_rcv 解封装成功,交付到 Pod vxlan_rcv 解封装发现 MAC 非法,kfree_skb 丢弃
使用的 VIP VIP1 (10.20.1.x,pool-1) VIP2 (10.20.2.x,pool-2)
后端 Pod 同一个 (172.18.5.34 / 测试用 172.18.5.17) 同一个

关键不对称点:edge-gw(harbor)不在 K8s 集群里、没有 Cilium,所以 wg-tunnel-1 上没有 BPF attach,流量老老实实走 Linux 静态路由直达 gpu 节点;而 master 是 K8s 节点、有 Cilium,wg-tunnel-2 上被 attach 了 cil_from_netdev-wg-tunnel-2,它在路由查找之前就把流量 DNAT+SNAT 并塞进了 VXLAN overlay,既让静态路由失效,又因为 L3 设备无 MAC 产生了内层全零 MAC 的非法 VXLAN 帧。

现象与初步排查

两个 Service 都是 LoadBalancer 类型,由 MetalLB 实现,分别分配了两个 VIP,最终 DNAT 到同一个后端 Pod IP。

1
2
3
$ kubectl get svc -n <ns> | grep if-xxx-service
if-xxx-service LoadBalancer 172.20.3.29 10.20.1.5 80/TCP,9998/TCP 53d
if-xxx-service-2 LoadBalancer 172.20.2.200 10.20.2.1 80/TCP,9998/TCP 2d16h

cilium-dbg service list 看两条链路的后端:

1
2
208   10.20.1.5:80/TCP     LoadBalancer   1 => 172.18.5.34:80/TCP     (active)
2013 10.20.2.1:80/TCP LoadBalancer 1 => 172.18.5.34:80/TCP (active)

两个 VIP 都指向同一个 Pod IP 172.18.5.34

在集群内 master 节点上直接通过 Cluster IP 访问,均没有问题。这一步非常关键:它说明 Cilium 的 DNAT 和集群的 overlay 网络本身是正常的——问题只出在"从公有云经 WireGuard 进来"这条外部链路上。

排查丢包点:逐层推进

1. master 节点 Cilium 丢包监控

1
2
3
4
$ kubectl exec -it -n kube-system cilium-xxx -- cilium monitor --type drop
xx drop (Policy denied) flow 0x... to endpoint 0, ifindex 69, file bpf_lxc.c:1651, , identity 50131->world: 172.18.2.127:33440 -> 198.51.100.10:80 tcp SYN
xx drop (Policy denied) flow 0x... to endpoint 0, ifindex 69, file bpf_lxc.c:1651, , identity 50131->world: 172.18.2.127:36978 -> 203.0.113.20:80 tcp SYN
...

丢包日志里满屏的 Policy denied,但仔细看——这些全是 Pod 主动出集群到 world 的流量(目的 IP 是公网地址),与我们要查的 VIP 10.20.2.1 和 Pod 172.18.5.34 毫无关系。

⚠️ 第一个坑:cilium monitor --type drop 会把节点上所有丢包都打出来,包括与本问题无关的噪声。必须用目的 IP/Port 过滤,否则会被淹没。

结论:包不是在 master 节点的 Cilium BPF 这里丢的。

2. master 节点抓包

在 master 上抓包,看到 Cilium 对从 WireGuard 进来的流量做了三件事:

1
10:25:56.081452 cilium_vxlan Out ifindex 9 ... 172.18.2.240.42750 > 172.18.5.17.80: Flags [S], ...
  1. 将 VIP 10.20.2.1 DNAT 成后端 Pod IP 172.18.5.17
  2. 将源 IP SNAT172.18.2.240(master 节点 cilium_host 设备的 IP);
  3. 把数据包送入 cilium_vxlan VTEP 设备,二次封装进 VXLAN,走隧道去往运行 Pod 的 gpu 节点

这里有个麻烦:SNAT 抹掉了真实来源。由于另一条链路是好的、推理服务正在 serving,从 Pod 侧根本看不出来到底有没有来自 VM-2 的流量到达。为了把"流量有没有真正打到 Pod"这件事变成可观测的,我部署了一个固定到 gpu 节点 的 nginx Pod,并复刻推理服务的拓扑——两个 Service、两个 VIP、同一个后端 Pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
nodeName: gpu-node # 固定到运行后端 Pod 的节点
containers:
- name: nginx
image: registry.example.com/nginx:alpine
ports:
- name: http
containerPort: 80
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 1Gi }
---
apiVersion: v1
kind: Service
metadata:
name: nginx-2
annotations:
metallb.universe.tf/ip-allocated-from-pool: mizar-ip-pool2
spec:
allocateLoadBalancerNodePorts: false
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
loadBalancerClass: metallb
type: LoadBalancer
selector: { app: nginx }
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: nginx-1
annotations:
metallb.universe.tf/ip-allocated-from-pool: mizar-ip-pool
spec:
allocateLoadBalancerNodePorts: false
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
loadBalancerClass: metallb
type: LoadBalancer
selector: { app: nginx }
ports:
- name: http
port: 80
targetPort: http
protocol: TCP

得到和推理服务一致的拓扑:

  • VIP 10.20.1.7 → Pod IP 172.18.5.17
  • VIP 10.20.2.2 → Pod IP 172.18.5.17

3. nginx Pod 内抓包

只在 VM-2(问题链路)发包,在 nginx Pod 网络命名空间内抓包:

1
2
$ tcpdump -nne -i eth0 tcp and port 80
# 没有任何数据包

TCP 包根本没到 Pod。

4. gpu 节点 Cilium 丢包

1
2
$ kubectl exec -it -n kube-system cilium-yyy -- cilium-dbg monitor -vv --type drop --type trace --type policy-verdict | grep 172.18.5.17
# 没有任何输出

包也不在 gpu 节点的 Cilium BPF 这里丢。

5. master bond0 出包

在 master 的 bond0 上抓 VXLAN 出包,过滤内层封包目的 IP 为 172.18.5.17

1
2
3
4
$ tcpdump -nne -i bond0 \
'src host 192.168.10.192 and dst host 192.168.10.119 and udp dst port 8472 and udp[46:4] = 0xac120511'
20:27:38.038003 aa:bb:cc:00:00:01 > aa:bb:cc:00:00:02, ethertype IPv4 (0x0800), length 124: 192.168.10.192.59598 > 192.168.10.119.8472: OTV, flags [I] (0x08), overlay 0, instance 2
00:00:00:00:00:00 > 00:00:00:00:00:00, ethertype IPv4 (0x0800), length 74: 172.18.2.240.59330 > 172.18.5.17.80: Flags [S], ...

包成功从 master 出去了(走 VXLAN 隧道)。

这里其实已经出现了一条决定性线索,但我(包括一起协作排查的 AI)当时都没第一时间看出来。先按下不表。

6. gpu 节点 bond0 入包

在 gpu 节点 bond0 上抓入包:

1
2
3
$ tcpdump -nne -s 0 -Q in -i bond0 'src host 192.168.10.192 and dst host 192.168.10.119 and udp dst port 8472 and udp[46:4] = 0xac120511'
20:33:59.402199 aa:bb:cc:00:00:01 > aa:bb:cc:00:00:02, ethertype IPv4 (0x0800), length 124: 192.168.10.192.49067 > 192.168.10.119.8472: OTV, flags [I] (0x08), overlay 0, instance 2
00:00:00:00:00:00 > 00:00:00:00:00:00, ethertype IPv4 (0x0800), length 74: 172.18.2.240.36008 > 172.18.5.17.80: Flags [S], ...

包也成功到达了 gpu 节点,但后面只有 SYN 重传,没有 SYN-ACK——握手建立不起来。

这里那条决定性线索第二次出现,AI 依然没能点破。

7. 用 pwru 定位内核丢包点

Cilium BPF 层没丢、bond0 有入包、Pod 却收不到——丢包只可能发生在 VXLAN 解封装到 Cilium 接管之间的内核路径。这种"内核态细粒度丢包"靠 cilium monitor 看不到了,得上 pwru(基于 kprobe 的 skb 跟踪工具):

1
2
3
4
5
6
7
8
9
10
11
12
root@gpu-node:~# pwru \
--filter-track-skb \
--output-tuple \
--output-tcp-flags \
--output-caller \
--timestamp=relative \
'tcp and dst host 172.18.5.17 and dst port 80'
...
0xff391ae9e95e7200 183 bond0:6 0x0800 0 74 192.168.10.192:42582->192.168.10.119:8472(udp) eth_type_trans vxlan_set_mac[vxlan]
0xff391ae9e95e7200 183 cilium_vxlan:20 0x0800 0 60 192.168.10.192:42582->192.168.10.119:8472(udp) kfree_skb_reason(SKB_DROP_REASON_NOT_SPECIFIED) vxlan_rcv[vxlan]
0xff391ae9e95e7200 183 cilium_vxlan:20 0x0800 0 60 ... skb_release_head_state kfree_skb_reason
0xff391ae9e95e7200 183 cilium_vxlan:20 0x0800 1500 60 ... skb_free_head skb_release_data

包的释放点清清楚楚:

1
kfree_skb_reason(SKB_DROP_REASON_NOT_SPECIFIED)  vxlan_rcv[vxlan]

包是在内核 vxlan_rcv 解 VXLAN 封装时被丢弃的。 NOT_SPECIFIED 是因为当前内核版本没有为 VXLAN 接收分支提供更细粒度的 drop reason——这也是为什么前面 cilium monitor 和常规抓包都看不到它,只能靠 pwru 这种内核态跟踪工具才能抓到。

根因:VXLAN 内层以太网 MAC 全零

定位到丢包函数后,回头对比"正常链路"和"异常链路"在 gpu 节点 bond0 上的 VXLAN 入包,差异一目了然。

正常链路的包(来自 edge-gw 那条):

1
2
3
23:46:11.060589 aa:bb:cc:00:00:01 > aa:bb:cc:00:00:02, ... OTV, flags [I], overlay 0, instance 6
aa:bb:cc:00:00:03 > aa:bb:cc:00:00:03, ethertype IPv4 (0x0800), length 74: 172.18.2.240.43570 > 172.18.5.17.80: Flags [S], ...
# 后续 SYN-ACK、ACK、HTTP GET 全都有

异常链路的包(来自 master 那条):

1
2
3
23:49:00.327167 aa:bb:cc:00:00:01 > aa:bb:cc:00:00:02, ... OTV, flags [I], overlay 0, instance 2
00:00:00:00:00:00 > 00:00:00:00:00:00, ethertype IPv4 (0x0800), length 74: 172.18.2.240.46396 > 172.18.5.17.80: Flags [S], ...
# 只有 SYN 重传,没有后续

对比结论:

正常链路 异常链路
VXLAN 内层源 MAC aa:bb:cc:00:00:03(有效) 00:00:00:00:00:00(全零)
VXLAN 内层目的 MAC aa:bb:cc:00:00:03(有效) 00:00:00:00:00:00(全零)
握手结果 完成 失败

VXLAN 内层以太网帧的 MAC 全为 0,是丢包的直接原因。 内核在 vxlan_rcv 解封装后,拿到一个源/目的 MAC 都非法的以太网帧,无法继续处理,直接 kfree_skb 丢弃。

这就是前面两次"决定性线索"——在 master bond0 出包和 gpu bond0 入包时,tcpdump 都已经打印出了 00:00:00:00:00:00 > 00:00:00:00:00:00 这行内层全零 MAC。但我(和协助排查的 AI)一开始都把它当成 tcpdump 对 L3 设备流量的"正常占位"忽略了,没有意识到它就是丢包的根因。这是一个值得反复提醒自己的教训:抓包输出里的每一行"奇怪但稳定出现"的字段,都可能是钥匙,而不是噪声。

为什么内层 MAC 会全零?

因为问题链路流量进入 master 节点的入口网卡,是 WireGuard 的虚拟网卡 wg-tunnel-2

1
2
3
4
5
6
7
$ ip -d link show wg-tunnel-2
183: wg-tunnel-2: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/none promiscuity 0 minmtu 0 maxmtu 2147483552
wireguard addrgenmode none numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

$ cat /sys/class/net/wg-tunnel-2/type
65534

type 65534ARPHRD_NONE,这是一个 L3 设备没有标准的以太网 MAC 地址,自然也就没有正常的以太网头。

完整的数据通路对比:

问题链路(master):

1
2
3
4
公有云 client → wg-tunnel-2 (master, L3/ARPHRD_NONE, 无 MAC)
→ 命中 attach 在 wg-tunnel-2 上的 cilium BPF:DNAT(VIP→Pod) + SNAT(→cilium_host IP)
→ 送入 cilium_vxlan VTEP 二次封装,内层以太网填全零 MAC
→ 到达 gpu 节点,vxlan_rcv 解封装发现内层 MAC 非法 → 丢包

正常链路(edge-gw):

1
2
3
4
公有云 client → wg-tunnel-1 (edge-gw)
→ 命中静态路由,直接转发往 gpu 节点(不经过 cilium 在 L3 设备上的 NAT)
→ 从 bond0 入站,内层以太网 MAC 有效
→ 正常解封装、交付到 Pod

更进一步的根因是:attach 在 wg-tunnel-2 上的 Cilium BPF,在 Linux 路由查找之前就把流量 NAT 并塞进了 VXLAN。 这使得原本配置在 wg-tunnel-2 上"转发到 gpu 节点"的静态路由失效——流量根本走不到那条静态路由,就被 Cilium 拐进了 overlay 隧道,而且因为 L3 设备没有 MAC,封装出的内层以太网帧 MAC 全零,最终被对端内核丢弃。

一句话:L3 设备 + Cilium BPF 提前 NAT = 内层全零 MAC 的非法 VXLAN 帧 = 对端 vxlan_rcv 丢弃。

解决方案

1. 临时修复:detach 问题网卡上的 Cilium BPF

先看一下当前 attach 在 wg-tunnel-2 ingress 上的 Cilium 程序:

1
2
3
$ tc filter show dev wg-tunnel-2 ingress
filter protocol all pref 1 bpf chain 0
filter protocol all pref 1 bpf chain 0 handle 0x1 cil_from_netdev-wg-tunnel-2 direct-action not_in_hw id 17781 tag e064410d5c957a28 jited

把它删掉:

1
$ tc filter del dev wg-tunnel-2 ingress protocol all pref 1 handle 0x1 bpf

detach 完成后,wg-tunnel-2 链路立即通了——流量不再被 Cilium 提前 NAT,而是正常命中静态路由、转发到 gpu 节点,从 bond0 入站,内层 MAC 有效。

⚠️ 这个修复是非持久的:Cilium node plugin 一旦重启,会重新把 BPF 程序 attach 回 wg-tunnel-2,问题会复现。

2. 持久化修复:用 CiliumNodeConfig 排除问题设备

治本的办法是告诉 Cilium:“这个节点你只接管 bond0,不要碰 wg-tunnel-2”。通过为 master 节点创建专属的 CiliumNodeConfig 实现:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: cilium.io/v2
kind: CiliumNodeConfig
metadata:
name: master-devices
namespace: kube-system
spec:
nodeSelector:
matchLabels:
kubernetes.io/hostname: <master-node-hostname>
defaults:
devices: "bond0"
direct-routing-device: "bond0"

应用后只重启该节点的 Cilium,让配置生效。这样 Cilium 就不会再向 wg-tunnel-2 attach BPF,流量得以按静态路由正常转发,问题彻底解决且重启不还原。

复盘与几点经验

这次排查从现象到根因绕了不短的弯路,复盘下来有几条值得记住的经验:

  1. cilium monitor --type drop 的噪声很大。节点上无关的 Pod 出向丢包会刷屏,必须严格按目的 IP/Port 过滤,否则会误判丢包点。

  2. “内核态丢包” Cilium BPF 看不到。当 cilium monitorcilium-dbg 都查不到、bond0 又确实有入包、Pod 却收不到时,要果断上 pwru,从 kfree_skb_reason 反查丢包函数。本次正是靠 pwru 把丢包点精确锁到 vxlan_rcv[vxlan]

  3. 抓包输出里稳定出现的"奇怪字段"要当成钥匙,而不是噪声。内层全零 MAC 在 master bond0 出包、gpu bond0 入包时各出现了一次,都被我(和 AI)当成 L3 设备的正常占位忽略了。事后看,它就是根因本身。

  4. L3/ARPHRD_NONE 设备是 Cilium overlay 的"天然雷区"。WireGuard、tun、IPIP 等 ARPHRD_NONE 设备没有以太网 MAC,一旦 Cilium BPF 在其 ingress 上提前做 DNAT/SNAT 并封装进 VXLAN,就会产生内层全零 MAC 的非法帧,被对端内核在解封装时丢弃。这类设备原则上不应被 Cilium 接管,应通过 devices / CiliumNodeConfig 显式排除。

  5. 临时修复(tc filter del)和持久化修复(CiliumNodeConfig)要成对给出。运维现场先用 tc 快速止血恢复业务,再补持久化配置防止重启复现——这是处理"急"与"治本"的标准节奏。

  6. AI 协作的反向教训:排查全程我都在和 AI 一起看抓包,但它在两次关键节点(master 出包、gpu 入包的 tcpdump 输出)都没能识别出"内层全零 MAC = 根因"。这提醒我:把原始 tcpdump 输出丢给 AI 时,应当显式提示它逐字段对比正常包与异常包,而不是笼统问"这有什么问题"。AI 对"稳定出现的异常字段"的敏感度,目前仍弱于一个有经验、知道"哪些字段本不该为零"的工程师。


一句话总结:公有云经 WireGuard L3 设备进 master 节点的流量,被该网卡上 attach 的 Cilium BPF 提前 DNAT/SNAT 并封进 VXLAN,因 L3 设备无 MAC 导致内层以太网 MAC 全零,被对端内核在 vxlan_rcv 解封装时丢弃;通过 detach/排除该设备上的 Cilium BPF(临时用 tc filter del,持久用 CiliumNodeConfig 限定 devices: bond0)解决。