nettools · 基于 VXLAN 的 EVR 设备探测工具
Usage Guide — 通过"自环内层帧 + payload 内嵌 EVR 源 IP"的巧妙设计,让 EVR 反射回包能直接对应到原始目标。
← 返回首页Probe VXLAN SrcIP Spoof evr 是面向 EVR(Edge Virtual Router)设备的链路探测工具。它向一个或多个 EVR VTEP 发送 UDP/VXLAN 探测包,每个探测包内层都封装了一个完整的以太网/IPv4/UDP 帧;EVR 在收到 VXLAN 后会把内层帧"反射"回探测端。evr 通过解析反射回来的内层帧来评估 EVR 设备的丢包、延迟、bitflip。
evr 的核心是一种"自环内层帧"设计:内层 src IP 与 dst IP 都填本机地址(这样 EVR 反射回来的报文会落到本机的 raw socket),同时把"原本要写在内层 src IP 上的 EVR 源 IP"嵌入在 payload,回包时按 payload 中的 IP 直接定位回原始目标,避免维护 5 元组映射。
EVR 设备的工作模型是 VXLAN VTEP:从外层 UDP/4789 收 VXLAN,把内层以太网帧解封后按内层 IP 头继续转发。如果按"自然"的设计去探测:
这种设计在多目标场景下要维护一张端口/IP 反查表,且 ECMP 哈希会把回包打散到本机不同 IP 上,处理起来很复杂。evr 用了一个反直觉但极其精简的方案:
peerByEVRSrc[ip]mock_src 让外层 src IP 与本机不同ipv4.NewRawConn 启用 IP_HDRINCLevr 完整探测包从外到内分为四层:
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 8 | magic | 固定 ASCII "EVRCHECK",校验合法报文 |
| 8 | 8 | seq | 大端 uint64,业务序列号(每次发送原子自增) |
| 16 | 8 | ts | 大端 int64,发送时间戳(纳秒) |
| 24 | 4 | srcIP | EVR 源 IP(关键)——回包时据此映射回 target |
| 28 | N | salt | Salt 模式(按 seq%4 选 0xFF / 0x00 / 0x5A / 互补 AAAA-5555),用于探测 bitflip |
Salt 由 util.Salts 生成,与 baize/kuiniu 一致;回包时若 payload 长度等于发送长度,则与对应 Salt 比对,命中差异即记一条 [client bitflip] 日志。
evr 主要面向数据中心网络中部署了 EVR/VTEP 的虚拟网络环境:
三者都用于"长期常驻 + JSON 配置驱动 + 共享 stat/log 体系"的网络探测,但探测对象与封装层次差别很大:
| 特性 | baize | kuiniu | evr |
|---|---|---|---|
| 面向网络 | 通用 IP 网络 | GPU NIC 互联(RoCE) | VXLAN/EVR 虚拟网络 |
| 报文形态 | UDP | RoCEv2/UDP | VXLAN 封装的内层 ETH/IPv4/UDP |
| 地址模型 | client_addr + server_addrs | local_gpu_addrs / remote_gpu_addrs | targets: vtep#evrSrc[#mockSrc] |
| 需要服务端进程吗 | 需要 baize server | 需要 kuiniu(role=server / both) | 不需要——EVR 设备本身就是反射器 |
| 响应匹配 | 5 元组 / 端口表 | GPU 对 + 端口 | payload 内嵌 EVR srcIP |
| 外层源 IP | 本机 | 本机 GPU NIC | 可 spoof 为 mock_src |
| 典型部署 | baize.json 长期监控 | 两台机器对称部署 | 单机即可,常驻探测多台 EVR |
# 克隆仓库
git clone https://github.com/baidu/nettools.git
cd nettools
# 编译全部工具
make compile
# 或单独编译 evr
make build-evr
go build -o evr ./cmd/evr/
# 本地测试构建 (snapshot)
make snapshot
# 正式发布 (需要 git tag + GITHUB_TOKEN)
git tag v0.5.0
make deploy
socket: operation not permitted。
| 操作系统 | 架构 | 支持 |
|---|---|---|
| Linux | AMD64 | ✓ 生产可用 |
| Linux | ARM64 | ✓ 生产可用 |
| macOS | AMD64 / ARM64 | 仅开发/编译 |
仓库 cmd/evr/evr.example.json 提供了一份脱敏的示例配置。每一项 target 用 vtep#evrSrc[#mockSrc] 三段式表达:
{
"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
}
# 使用配置文件启动(推荐)
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
sudo(或 CAP_NET_RAW)来创建 raw IP 套接字、设置 IP_HDRINCL 与 IP TOS。线上推荐 systemd 以 root 启动并指定 log_dir。
所有 CLI 参数在显式传入时会覆盖 JSON 配置中的同名字段(pflag.Visit 模式),便于自动化系统按需调整:
sudo ./evr -c /etc/evr/evr.json \
--rate-in-span 5000 \
--verbose
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
id | string | "" | 探测端标识,仅用于日志区分 |
client_addr | string | auto | 本机外层源 IP(IPv4),未填时自动探测主机名对应地址 |
targets | string | 必填 | 目标列表,逗号分隔;每项 vtep#evrSrc[#mockSrc] |
dst_port | uint16 | 4789 | 外层 UDP 目的端口(VXLAN 标准端口) |
inner_dst_port | uint16 | 8972 | 内层 UDP 目的端口(兼用作 BPF 过滤条件) |
src_mac | string | 00:00:00:00:ff:ff | 内层以太帧源 MAC |
dst_mac | string | 00:00:5e:00:01:ff | 内层以太帧目的 MAC |
vni | uint32 | 15990000 | VXLAN VNI(24 位) |
tos | int | 0 | 外层 + 内层 IPv4 TOS/DSCP(同时也是 BPF 过滤条件) |
ttl | int | 64 | 外层 + 内层 IPv4 TTL |
client_port_range | string | "9981,9981" | 外层源端口范围 min,max |
rate_in_span | int64 | 1 | 每个 span 内 所有 target 总发包数 |
span | string | "100ms" | 统计时间窗口 |
delay | string | "100ms" | 统计延迟,等在途报文回来 |
msg_len | int | 必填 | 内层 UDP payload 长度(含 28 B 协议头) |
pprof_addr | string | "" | pprof HTTP 监听地址(如 :6060) |
log_dir | string | "" | 日志目录,为空则只打 stderr |
log_max_age_days | int | 3 | 日志保留天数 |
verbose | bool | false | 打印每个端口的丢包详情 |
rate_in_span 是所有 target 总速率。例如 12 个 target、rate_in_span=2000、span=1s,则每个 target 平均 ~166 pps。
| 段 | 必填 | 含义 |
|---|---|---|
| vtepIP | 是 | EVR VTEP 的 IPv4 地址(外层 IP 目的地址) |
| evrSrcIP | 是 | "目标"标识,会被嵌入 payload,用于回包匹配 |
| mockSrcIP | 否 | 外层源 IP;为空则用 client_addr |
所有 CLI 参数都对应 JSON 配置中同名字段,显式传参时会覆盖 JSON:
| 参数 | 简写 | 含义 |
|---|---|---|
| --config | -c | JSON 配置文件路径 |
| --id | 探测端标识 | |
| --client-addr | 本机外层源 IP | |
| --targets | -t | vtep#evrSrc[#mockSrc] 列表 |
| --dst-port | 外层 UDP 目的端口(默认 4789) | |
| --inner-dst-port | 内层 UDP 目的端口(默认 8972) | |
| --src-mac / --dst-mac | 内层以太帧 MAC | |
| --vni | VXLAN VNI | |
| --tos / --ttl | 外层 + 内层 IP 头 TOS / TTL | |
| --client-port-range | 外层源端口范围 min,max | |
| --rate-in-span | 每 span 总发包数 | |
| --span | -s | 统计时间窗口 |
| --delay | 统计延迟 | |
| --msg-len | 内层 UDP payload 长度 | |
| --pprof | pprof 监听地址 | |
| --log-dir / --log-max-age | 日志目录与保留天数 | |
| --verbose | -v | 打印每个端口的丢包详情 |
| --version | -V | 打印版本 |
sudo ./evr \
--client-addr 203.0.113.10 \
--targets 198.51.100.96#192.0.2.1 \
--msg-len 1024
sudo ./evr \
--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" \
--client-port-range 63000,63999 \
--rate-in-span 2000 \
--span 1s \
--msg-len 1024
所有 target 都通过 203.0.113.99 这个 mock 源 IP 出去,让网络上看到的 evr 探测身份统一。
sudo ./evr -c /etc/evr/evr.json \
--tos 64
外层和内层 IP 头都打上 TOS=64;BPF 也按 TOS=64 过滤回包,避免接收到不相关的 UDP 流量。
sudo ./evr -c /etc/evr/evr.json \
--pprof ":6060"
# 然后用 go tool pprof 拉 cpu profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
sudo ./evr -c /etc/evr/evr.json --verbose
开启后丢包条目会列出每个 srcPort 的发送/接收次数,便于发现"特定端口(即特定 ECMP 哈希值)持续丢包"的故障。
日志由 util.RotateWriter 按天轮转,与 baize / kuiniu 共用同一份实现:
{
"log_dir": "/var/log/evr",
"log_max_age_days": 3
}
启动后日志目录形如:
/var/log/evr/
├── evr.log -> evr.log.20260611 # 当日符号链接
├── evr.log.20260611
└── evr.log.20260610
log_dir 时只打到 stderr,适合容器化部署中由日志收集器统一采集。
journalctl -u evr 也能 tail -f /var/log/evr/evr.log。
这样 EVR 反射回来的内层帧 dst 就是本机,本机 raw socket 直接收得到,无需在对端起 server,也不需要单独申请回包用的 IP。这是 evr 的核心"巧妙设计",详见第 2 节。
把目标 EVR 的源 IP 嵌进 payload 第 24~28 字节,回包解析后用 peerByEVRSrc[ip] 一步定位到 target,不再需要 5 元组反查表。
evr 在写端用 ipv4.NewRawConn(conn) 包了一层,等价于打开 IP_HDRINCL。开启后内核不再自己生成 IP 头,而是原样发出我们手工拼好的外层 IPv4 头——其中 src 就是 mock_src。如果不包这一层,内核会前置一份它自己的 IP 头,导致双层 IP 封装且 mock_src 失效。
是所有 target 总速率。12 个 target、rate_in_span=2000/s,每个 target 平均 ~166 pps。要提高单 target 速率,可以减少 target 数量或调大 rate_in_span。
读 socket 的 BPF 程序匹配两个条件:(1) IPv4 协议号 = UDP;(2) IPv4 TOS = cfg.tos;(3) UDP 目的端口 = inner_dst_port。三个条件同时命中才放行,确保不被同机器其他 UDP 流量打扰。写 socket 上则装一个"全丢"的 BPF,以阻止内核给这个发送 socket 排队任何回包。
0xFF 全 1 暴露任何变 0 的位翻转;0x00 全 0 暴露任何变 1 的位翻转;0x5A 交替位 (01011010) 适合 NIC 串行链路上的奇偶错误;互补 0xAAAA / 0x5555 专门覆盖 1's complement 校验和漏检的"互补翻转"——这是普通 UDP/TCP checksum 唯一无法察觉的一类 bitflip。
需要创建 ip4:udp raw socket、启用 IP_HDRINCL、设置 IP TOS/DSCP,且要装 BPF。Linux 上需要 CAP_NET_RAW;最简单的就是 sudo 或 systemd 以 root 启动。
需要。evr 不支持热加载。建议 systemd 管理:sudo systemctl restart evr。
发送 SIGINT (Ctrl+C) 或 SIGTERM。程序会取消所有 goroutine 并睡眠 1 秒等在途报文回来后退出。
仅作为编译/开发环境。运行时 macOS 不允许 ip4:udp raw socket 与 IP_HDRINCL 的组合,会报 socket: operation not permitted。生产请用 Linux。