百度网络监控工具开源第四弹:evr — 构造 VXLAN 探测

这是百度网络监控工具 nettools 开源系列的第四篇。前三篇分别介绍了 bitflip/baize(UDP 丢包与改包检测工具和Agent)、lidar(TCP SYN 端口可达性探测),它们解决的都是「服务器之间」「点到点」的探测问题——前提是:探测机和被探测对象,至少有一端在我们手里。

但有一类设备,我们既无法在它上面装 agent,也没法在它对面的机房里放一台探测机。这就是今天的主角 evr 要解决的困境。

这也是我更深入的了解网络包的构造,把网络探测玩出花了来,对我的网络编程的功力大增的一个很好的场景。

项目地址:https://github.com/baidu/nettools
文档:https://nettools.rpcx.io

一、evr 探测的困境:探测机进不去客户机房

先说一个真实的场景。

百度有大量的云客户,我们提供的 EVR(Edge Virtual Router,边缘虚拟路由器) 设备作为客户侧网络接入百度云网络的边界节点。EVR 往上连百度的骨干/城域网络,往下连客户自己的虚拟网络(VXLAN overlay)。

EVR - 边缘虚拟路由器,通常用于在虚拟化环境中实现路由功能。EVR 位于网络的边缘,用于连接内部网络和外部网络(如客户机房)。

现在问题来了:我们需要监控「百度网络 → EVR」这一段链路的健康度——有没有丢包、延迟多大、有没有改包。按照前几个工具的套路,我们的方案应该是:

  • 在 EVR 设备上装个 agent?—— 不行。EVR 是网络设备/客户侧设备,我们没有权限往里塞监控程序。
  • 在 EVR 对面(客户机房内)放一台探测机,做点到点探测?—— 更不行。那是客户的机房,正常情况我们不可能在客户的物理环境里申请一台探测机常驻。

lidar 那一套「发 SYN,靠对端内核 TCP 协议栈自动回 SYN-ACK/RST」的思路,在这里也不灵——EVR 不是一台服务器,它不会帮你跑 TCP 协议栈三次握手,或者说不允许我们高频的探测。

1
2
3
4
5
6
   百度侧                          边缘设备
┌──────────┐ ┌──────────────────────┐
│ 探测机 │ ───── ??? ─────► │ EVR 设备 │
│ (我们的) │ │ (装不了 agent) │
└──────────┘ │ 对面也放不了探测机 │
└──────────────────────┘

困境的本质是:被探测对象不可控,且它对面也无法部署探测机。 我们需要一个 「单边」 就能完成的探测方案——只在百度侧放一台机器,让 EVR 设备自己「帮我们把包送回来」。

答案藏在 EVR 设备的工作原理里:它是一个 VXLAN VTEP(VXLAN Tunnel End Point)。而 VTEP 有一个非常好用的特性——它会忠实地按照内层 IP 头转发解封后的内层帧。这就给了我们「构造一个会被反射回来的 VXLAN 包」的可能。

要理解这个技巧,得先看懂 VXLAN 的包结构。

二、VXLAN:把二层帧塞进 UDP 里

VXLAN(Virtual Extensible LAN,RFC 7348)是数据中心 overlay 网络的事实标准。它要解决的核心问题是:传统 VLAN 只有 12 位 VLAN ID,最多 4096 个二层网络,在大规模多租户云环境里完全不够用。

VXLAN 的做法简单粗暴又有效:把一个完整的二层以太网帧,整个塞进一个 UDP 数据报里,通过三层网络传输。这样原本受限于物理二层域的网络,可以跨越任意三层网络延展,VNI(VXLAN Network Identifier)有 24 位,支持约 1600 万个 overlay 网络。

它的包结构从外到内是这样的:

一层层拆开看:

大小 关键字段 作用
外层 Ethernet 14 B 物理 MAC underlay 二层转发
外层 IP 20 B src / dst IP underlay 三层路由(VTEP 之间)
外层 UDP 8 B dport = 4789 VXLAN 标准端口,VTEP 据此识别
VXLAN header 8 B flags + VNI(24 位) 标识 overlay 网络
内层 Ethernet 14 B 租户 MAC 被封装的原始二层帧开始
内层 IP 20 B 内层 src / dst overlay 里的真实通信地址
内层 UDP/TCP 8 B 内层端口 租户的真实流量
Payload N B 业务数据 原始负载
其中 8 字节的 VXLAN header 结构是:
1
2
3
4
5
6
7
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|R|R|R|R|I|R|R|R| Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| VXLAN Network Identifier (VNI) | Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

I 位(第 5 位)置 1 表示 VNI 有效,剩下的 24 位就是 VNI。

VTEP 的工作模型:它从外层 UDP/4789 收到一个 VXLAN 包,剥掉外层 Eth/IP/UDP 和 VXLAN header,拿到内层的原始以太网帧,然后按内层 IP 头继续转发

注意最后这句话——「按内层 IP 头继续转发」。这就是 evr 整个设计的命门所在:如果我把内层 dst IP 填成我自己,VTEP 解封后不就把包转回给我了吗?

三、evr 工具:让 EVR 自己把包反射回来

实现原理:自环内层帧 + payload 内嵌 EVR 源 IP

evr 的核心是一个反直觉但极其精简的设计——「自环内层帧」。

先看一个绕不开的硬约束:VTEP 是按内层 IP 头转发解封后的帧的。所以要让反射的包能回到探测机,内层 dst IP 就必须填探测机本机——填别的地址,包就被转到别处去了,根本回不来。这一点没得选。

真正有选择空间的是内层 src IP。最「自然」的想法是:把每个目标 EVR 的源 IP 填进内层 src IP,回包时靠这个 src IP 区分「是哪个目标反射回来的」。多目标时,本机收到一堆 dst=本机、src=各个 EVR 的回包,按 src IP 分流即可。

但 evr 偏偏没这么做——它把内层 src IP 也填成了本机(src = dst = 本机,自环)。这样一来,那个本可用来标识目标的内层 src IP 就被「浪费」掉了,回包里全是 src=本机, dst=本机,彼此长得一模一样,没法区分目标了。

那为什么要主动放弃这个天然的标识位?这里其实有一个技巧
当出现故障的时候,我们要使用traceroute的功能,通过设置TTL,回获取内层的回包经过的路径。通过设置src IP为探测机的IP,我们就能够让ICMP的回包发送给探测机,这样就可以把回程的路径都探测出来了。

evr 的三招合起来是这样:

第一招:内层 src IP 和 dst IP 都填探测机的本机地址。

1
2
3
4
// agent.go:内层 src == 内层 dst == 本机 IP
inner, err := codec.EncodeVxlanInner(a.conf.VNI, a.conf.SrcMAC, a.conf.DstMAC,
p.innerDstIP, p.innerDstIP, // inner src == inner dst == local IP
newPort, a.conf.InnerDstPort, uint8(a.conf.TOS), a.conf.TTL, payload)

EVR 解封 VXLAN 后看到内层 dst = 本机,自然就把内层帧转回本机。本机的 raw socket 直接收到回包,不需要在对端起任何 server,也不需要单独申请回包用的 IP/端口

第二招:把「真正的目标标识」嵌进 payload。

既然内层 src/dst 都被占用成本机了,那「这个包是探测哪个 EVR 的」靠什么区分?答案是把目标 EVR 的IP 写进 payload 的第 24~28 字节:

1
2
3
4
5
6
// codec/packet.go:EVRCHECK 协议头
// 偏移 0 : magic "EVRCHECK" (8B),校验合法报文
// 偏移 8 : seq (8B) 业务序列号,原子自增
// 偏移 16 : ts (8B) 发送时间戳(纳秒)
// 偏移 24 : evrIP (4B) EVR 源 IP —— 回包据此映射回 target
// 偏移 28+ : salt Salt 填充,用于 bitflip 检测

回包时解析 payload 里的这 4 字节,一步定位回目标:

1
2
3
// agent.go:handlePacket 收到回包
seq, ts, evrIP := codec.DecodeWithSrcIP(payload)
p := a.peerByEVRSrc[evrIP.String()] // 直接 O(1) 定位 target

本质上,evr 把「目标标识」从 IP 头搬到了 payload 里,让 EVR 的 VXLAN 反射动作天然变成一次「单源对多目标」的回包匹配。代价仅仅是 4 字节 payload + 解析时多读 4 字节。

第三招:外层源 IP 可以 spoof。

evr 用 ipv4.NewRawConn 包了一层(等价于开启 IP_HDRINCL),让内核不再自己生成外层 IP 头,而是原样发出我们手工拼好的外层 IPv4 头。这样外层 src IP 可以填成 mock_src——一个虚假的IP地址。当然这个功能是可选的。

这有什么好处呢?
为了在Evr上识别出来探测包,我们其实是在Evr中做了一些特殊的配置的,使用和客户不同的VNI,识别出『探测机』的源IP,有不同的路由策略。但这也带来的一个问题: 如果探测机有故障,需要切换探测机的时候,需要在Evr上改配置。
为线上的网络设备修改配置流程上很复杂,需要评审改配方案、需要等待变更窗口,需要在凌晨的时候操作等。所以不能及时的更改探测机。但是如果我们使用mockIP,那么就可以分分钟的把探测切到新的探测机上,不需要修改Evr的配置。这也是我们的一个技巧。

完整的一发一收流程:

1
2
3
4
5
6
7
8
9
10
   发送侧                                  回程侧
┌────────────────────────────┐ ┌────────────────────────────┐
│ 外层 IP : mock_src → vtepIP │ │ 内层 dst = 本机 │
│ 外层 UDP: srcPort → 4789 │ 反射 │ ↓ 本机 raw socket 收到 │
│ ┌ VXLAN(VNI) ───────────┐ │ ────► │ 解析 payload[24:28] │
│ │ 内层 ETH/IPv4/UDP │ │ │ 得到 EVR 源 IP │
│ │ src=本机, dst=本机 │ │ │ peerByEVRSrc[ip] → target │
│ │ payload: ...+EVR srcIP│ │ │ 累加 stat(丢包/延迟/翻转) │
│ └───────────────────────┘ │ └────────────────────────────┘
└────────────────────────────┘

整个过程只在百度侧部署一台 evr,EVR 设备本身充当了反射器——困境破解。

使用方法

evr 由 JSON 配置驱动,每个 target 用 vtep#evrSrc[#mockSrc] 三段式表达:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"id": "evr-probe-1",
"client_addr": "203.0.113.10",
"targets": "198.51.100.96#192.0.2.1#203.0.113.99,198.51.100.97#192.0.2.2#203.0.113.99",
"vni": 15990000,
"tos": 64,
"ttl": 64,
"client_port_range": "63000,63999",
"rate_in_span": 2000,
"span": "1s",
"delay": "5s",
"msg_len": 1024,
"log_dir": "./log",
"log_max_age_days": 3
}

三段式的含义:

必填 含义
vtepIP EVR VTEP 的 IP(外层目的地址)
evrSrcIP 「目标」标识,嵌入 payload 用于回包匹配
mockSrcIP 外层源 IP;为空则用 client_addr
启动:
1
2
3
4
5
6
7
8
9
10
11
# 配置文件启动(推荐,线上用 systemd 以 root 拉起)
sudo ./evr -c /etc/evr/evr.json

# 临时排查,全用命令行参数
sudo ./evr \
--client-addr 203.0.113.10 \
--targets 198.51.100.96#192.0.2.1#203.0.113.99 \
--rate-in-span 2000 --span 1s --msg-len 1024

# CLI 参数会覆盖配置文件里的同名字段(pflag.Visit 模式)
sudo ./evr -c /etc/evr/evr.json --rate-in-span 5000 --verbose

evr 需要 sudo(或 CAP_NET_RAW)来创建 raw socket、启用 IP_HDRINCL、设置 IP TOS 并挂载 BPF。它仅在 Linux 上有意义,macOS 只能用于编译开发。

四、evr 中的技巧与高频 FAQ

evr 看似简单,但藏了几个值得拿出来单独说的工程技巧。

技巧 1:BPF 内核层过滤,别让无关 UDP 流量打扰

读 socket 用的是 ip4:udp raw socket,它默认会收到本机所有 UDP 报文。在万兆网卡、几十万 pps 的探测机上,内核把海量无关包拷到用户态,性能直接崩。

解法是在内核层用 cBPF 过滤,只放行三个条件同时命中的包:

1
2
3
(1) IPv4 协议号 = UDP (17)
(2) IPv4 TOS = cfg.tos
(3) UDP 目的端口 = inner_dst_port (默认 8972)

只有这三条都满足才投递到用户态,几乎零开销。而写 socket 上则反过来装一个「全丢」的 BPF——阻止内核给这个发送 socket 排队任何回包。

技巧 2:源端口轮转覆盖 ECMP 多路径

和 bitflip/lidar 一样,evr 也面临多路径覆盖问题。两个固定地址之间,五元组固定则 ECMP 哈希结果不变,永远只走一条链路。evr 通过 client_port_range 配置一段源端口,每发一轮就让 srcPort + 1,在统计意义上覆盖 ECMP 哈希全空间。

技巧 3:4 种 Salt 模式抓 bitflip

payload 第 28 字节往后是 Salt 填充,按 seq % 4 在四种模式间轮换,专门用来检测链路上的位翻转:

  • 0xFF 全 1 —— 暴露任何「1 变 0」的翻转
  • 0x00 全 0 —— 暴露任何「0 变 1」的翻转
  • 0x5A 交替位 (01011010) —— 适配 NIC 串行链路的奇偶错误
  • 互补 0xAAAA / 0x5555 —— 专治 1's complement 校验和漏检的「互补翻转」,这是普通 UDP/TCP checksum 唯一无法察觉的一类 bitflip

回包时如果 payload 长度等于发送长度,就和对应 Salt 比对,命中差异即记一条 [client bitflip] 日志。这套 Salt 实现与 baize/kuiniu 完全共用。

高频 FAQ

Q:mock_src 是怎么生效的?内核为什么不会把它改回去?
evr 在写端用 ipv4.NewRawConn(conn) 包了一层,等价于开启 IP_HDRINCL。开启后内核不再生成自己的 IP 头,而是原样发出我们手工拼的外层 IPv4 头,其中 src 就是 mock_src。如果不包这一层,内核会前置一份自己的 IP 头,导致双层 IP 封装且 mock_src 失效。

Q:rate_in_span 是单 target 速率还是总速率?
所有 target 的总速率。比如 12 个 target、rate_in_span=2000/s,每个 target 平均只有约 166 pps。要提高单 target 速率,就减少 target 数量或调大 rate_in_span

Q:为什么内层 src 和 dst 都填本机 IP?
这样 EVR 反射回来的内层帧 dst 就是本机,本机 raw socket 直接收得到,无需在对端起 server、无需单独申请回包 IP。这是 evr 破解「对面进不去」困境的核心招式。src IP也填写本机IP是为了定位的时候traceroute的需要。

Q:为什么 evr 必须用 sudo?
需要创建 ip4:udp raw socket、启用 IP_HDRINCL、设置 IP TOS/DSCP 并挂载 BPF。Linux 上需要 CAP_NET_RAW,最简单就是 sudo 或 systemd 以 root 启动。

Q:和 baize / kuiniu 怎么选?
普通业务网络长期监控用 baize;AI 训练的 GPU NIC 互联(RoCE)用 kuiniu;机房 VXLAN/EVR 路径与 EVR 设备本身的探测用 evr——关键区别是 evr 不需要在对端起 server,EVR 设备本身就是反射器。


evr 把一个看似无解的困境——「探测机进不去客户机房,被探测设备又装不了 agent」——通过对 VXLAN VTEP 反射特性的巧妙利用,变成了一个单边即可完成的探测。这背后是同一套技术栈在不同网络场景下的复用:raw socket 构造报文、BPF 内核过滤、源端口轮转覆盖 ECMP、Salt 检测 bitflip、时间桶统计。

这只是 nettools 的冰山一角。后续还有网关设备监控、定位工具,以及巨量监控数据的处理方案。

项目地址:https://github.com/baidu/nettools

欢迎 Star、试用、提 Issue 和 PR。