evr 使用指南

nettools · 基于 VXLAN 的 EVR 设备探测工具

Usage Guide — 通过"自环内层帧 + payload 内嵌 EVR 源 IP"的巧妙设计,让 EVR 反射回包能直接对应到原始目标。

← 返回首页

目录

1. 概述

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 元组映射。

2. 巧妙的设计:自环内层帧 + payload 内嵌 EVR 源 IP

EVR 设备的工作模型是 VXLAN VTEP:从外层 UDP/4789 收 VXLAN,把内层以太网帧解封后按内层 IP 头继续转发。如果按"自然"的设计去探测:

这种设计在多目标场景下要维护一张端口/IP 反查表,且 ECMP 哈希会把回包打散到本机不同 IP 上,处理起来很复杂。evr 用了一个反直觉但极其精简的方案:

内层 src/dst IP 都设为本机

  • EVR 反射的内层帧 dst = 本机
  • 本机 raw socket 直接收到回包
  • 不需要单独申请回包 IP/端口

真实 EVR 源 IP 嵌入 payload

  • payload 头部 4 字节 = EVR srcAddr
  • 回包按 IP 直接 peerByEVRSrc[ip]
  • 无需 5 元组、无需端口表

外层源 IP 可 spoof

  • mock_src 让外层 src IP 与本机不同
  • ipv4.NewRawConn 启用 IP_HDRINCL
  • 多个目标可以共享同一个对外身份

报文匹配流程

发送侧 回程侧 ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ 外层 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 │ │ └────────────────────────┘ │ └──────────────────────────────┘ └──────────────────────────────┘
本质:把"目标标识"从 IP 头搬到了 payload 里,让 EVR 的 VXLAN 反射动作天然变成一次"单源对多目标"的回包匹配。代价仅仅是 4 字节 payload 占用 + 解析时多读 4 字节。

3. 报文结构

evr 完整探测包从外到内分为四层:

┌──────── 外层 IPv4 (20 B) ─────────┐ src=mockSrc, dst=vtepAddr, tos=cfg.tos, ttl=cfg.ttl ├──────── 外层 UDP (8 B) ────────┤ srcPort∈[port_min,port_max], dstPort=4789 ├──────── VXLAN (8 B) ────────┤ VNI=cfg.vni(默认 15990000) ├──────── 内层 Eth (14 B) ────────┤ srcMAC, dstMAC(默认 00:00:00:00:ff:ff / 00:00:5e:00:01:ff) ├──────── 内层 IPv4 (20 B) ────────┤ src=本机, dst=本机, tos=cfg.tos, ttl=cfg.ttl ├──────── 内层 UDP (8 B) ────────┤ srcPort=外层 srcPort, dstPort=8972(默认) └──────── EVRCHECK 协议体 ─────────┘ msg_len 字节

EVRCHECK 协议头(28 B 固定 + Salt 填充)

偏移长度字段说明
08magic固定 ASCII "EVRCHECK",校验合法报文
88seq大端 uint64,业务序列号(每次发送原子自增)
168ts大端 int64,发送时间戳(纳秒)
244srcIPEVR 源 IP(关键)——回包时据此映射回 target
28NsaltSalt 模式(按 seq%4 选 0xFF / 0x00 / 0x5A / 互补 AAAA-5555),用于探测 bitflip

Salt 由 util.Salts 生成,与 baize/kuiniu 一致;回包时若 payload 长度等于发送长度,则与对应 Salt 比对,命中差异即记一条 [client bitflip] 日志。

4. 使用场景

evr 主要面向数据中心网络中部署了 EVR/VTEP 的虚拟网络环境:

EVR 设备健康度监控

  • 批量探测多台 EVR 设备 / 多个 VTEP IP
  • 持续发现 VTEP 转发路径丢包
  • 反射延迟监控替代主动 ping

VXLAN 隧道路径监控

  • 验证 underlay 到 EVR VTEP 的 ECMP 是否健康
  • 通过 srcPort 轮转覆盖 ECMP 哈希全空间
  • 发现 underlay 丢包但 overlay 看不到的链路

多 EVR 并发对比探测

  • 一份配置探测十几个 EVR
  • 同 mock_src 让对外身份一致
  • per-target 丢包/延迟/bitflip 统计

变更前后对比

  • EVR 升级 / underlay 调整前后基线对比
  • 找出变更引入的丢包或延迟回归
  • 结合 stat 分桶定位短期抖动

NIC/中间设备 bitflip 检测

  • 4 种 Salt 模式覆盖互补翻转
  • 抓出 UDP 校验和漏检的改包
  • 对长程路径数据完整性提供网络层证据

VTEP 容量压测旁路监控

  • 压测期间常态化跑 evr
  • 实时观察 VTEP 在压力下是否开始丢包
  • 结合 pprof 监控发送端自身瓶颈

5. 与 baize / kuiniu 的区别

三者都用于"长期常驻 + JSON 配置驱动 + 共享 stat/log 体系"的网络探测,但探测对象与封装层次差别很大:

特性baizekuiniuevr
面向网络通用 IP 网络GPU NIC 互联(RoCE)VXLAN/EVR 虚拟网络
报文形态UDPRoCEv2/UDPVXLAN 封装的内层 ETH/IPv4/UDP
地址模型client_addr + server_addrslocal_gpu_addrs / remote_gpu_addrstargets: vtep#evrSrc[#mockSrc]
需要服务端进程吗需要 baize server需要 kuiniu(role=server / both)不需要——EVR 设备本身就是反射器
响应匹配5 元组 / 端口表GPU 对 + 端口payload 内嵌 EVR srcIP
外层源 IP本机本机 GPU NIC可 spoof 为 mock_src
典型部署baize.json 长期监控两台机器对称部署单机即可,常驻探测多台 EVR
怎么选:普通业务网络长期监控用 baize;AI 训练 GPU 互联用 kuiniu;机房 VXLAN/EVR 路径与 EVR 设备本身的探测用 evr(不需要在对端起 server)。

6. 安装

从源码编译

# 克隆仓库
git clone https://github.com/baidu/nettools.git
cd nettools

# 编译全部工具
make compile

# 或单独编译 evr
make build-evr
go build -o evr ./cmd/evr/

使用 GoReleaser 构建发行版

# 本地测试构建 (snapshot)
make snapshot

# 正式发布 (需要 git tag + GITHUB_TOKEN)
git tag v0.5.0
make deploy
Tip:evr 仅在 Linux 上有意义(依赖 raw socket + IP_HDRINCL)。macOS 仅作为开发/编译环境,运行时会报 socket: operation not permitted

支持的平台

操作系统架构支持
LinuxAMD64✓ 生产可用
LinuxARM64✓ 生产可用
macOSAMD64 / ARM64仅开发/编译

7. 快速开始

1. 准备配置

仓库 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
}

2. 启动 evr

# 使用配置文件启动(推荐)
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
Note:evr 需要 sudo(或 CAP_NET_RAW)来创建 raw IP 套接字、设置 IP_HDRINCL 与 IP TOS。线上推荐 systemd 以 root 启动并指定 log_dir

3. CLI 覆盖配置文件

所有 CLI 参数在显式传入时会覆盖 JSON 配置中的同名字段(pflag.Visit 模式),便于自动化系统按需调整:

sudo ./evr -c /etc/evr/evr.json \
  --rate-in-span 5000 \
  --verbose

8. 配置文件详解

顶层字段

字段类型默认值说明
idstring""探测端标识,仅用于日志区分
client_addrstringauto本机外层源 IP(IPv4),未填时自动探测主机名对应地址
targetsstring必填目标列表,逗号分隔;每项 vtep#evrSrc[#mockSrc]
dst_portuint164789外层 UDP 目的端口(VXLAN 标准端口)
inner_dst_portuint168972内层 UDP 目的端口(兼用作 BPF 过滤条件)
src_macstring00:00:00:00:ff:ff内层以太帧源 MAC
dst_macstring00:00:5e:00:01:ff内层以太帧目的 MAC
vniuint3215990000VXLAN VNI(24 位)
tosint0外层 + 内层 IPv4 TOS/DSCP(同时也是 BPF 过滤条件)
ttlint64外层 + 内层 IPv4 TTL
client_port_rangestring"9981,9981"外层源端口范围 min,max
rate_in_spanint641每个 span 内 所有 target 总发包数
spanstring"100ms"统计时间窗口
delaystring"100ms"统计延迟,等在途报文回来
msg_lenint必填内层 UDP payload 长度(含 28 B 协议头)
pprof_addrstring""pprof HTTP 监听地址(如 :6060
log_dirstring""日志目录,为空则只打 stderr
log_max_age_daysint3日志保留天数
verboseboolfalse打印每个端口的丢包详情
Note:rate_in_span所有 target 总速率。例如 12 个 target、rate_in_span=2000span=1s,则每个 target 平均 ~166 pps。

targets 三段式

必填含义
vtepIPEVR VTEP 的 IPv4 地址(外层 IP 目的地址)
evrSrcIP"目标"标识,会被嵌入 payload,用于回包匹配
mockSrcIP外层源 IP;为空则用 client_addr

9. 命令行参数

所有 CLI 参数都对应 JSON 配置中同名字段,显式传参时会覆盖 JSON:

参数简写含义
--config-cJSON 配置文件路径
--id探测端标识
--client-addr本机外层源 IP
--targets-tvtep#evrSrc[#mockSrc] 列表
--dst-port外层 UDP 目的端口(默认 4789)
--inner-dst-port内层 UDP 目的端口(默认 8972)
--src-mac / --dst-mac内层以太帧 MAC
--vniVXLAN VNI
--tos / --ttl外层 + 内层 IP 头 TOS / TTL
--client-port-range外层源端口范围 min,max
--rate-in-span每 span 总发包数
--span-s统计时间窗口
--delay统计延迟
--msg-len内层 UDP payload 长度
--pprofpprof 监听地址
--log-dir / --log-max-age日志目录与保留天数
--verbose-v打印每个端口的丢包详情
--version-V打印版本

10. 使用示例

探测一台 EVR(最小用法)

sudo ./evr \
  --client-addr 203.0.113.10 \
  --targets 198.51.100.96#192.0.2.1 \
  --msg-len 1024

多 EVR 并发探测 + 共享 mock_src

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 探测身份统一。

带 TOS/DSCP 标记 + BPF 过滤

sudo ./evr -c /etc/evr/evr.json \
  --tos 64

外层和内层 IP 头都打上 TOS=64;BPF 也按 TOS=64 过滤回包,避免接收到不相关的 UDP 流量。

开启 pprof 排查发送瓶颈

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

verbose 排查丢包

sudo ./evr -c /etc/evr/evr.json --verbose

开启后丢包条目会列出每个 srcPort 的发送/接收次数,便于发现"特定端口(即特定 ECMP 哈希值)持续丢包"的故障。

11. 日志管理

日志由 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
Tip:未配置 log_dir 时只打到 stderr,适合容器化部署中由日志收集器统一采集。
Note:日志同时写终端和文件,便于运维既能 journalctl -u evr 也能 tail -f /var/log/evr/evr.log

12. FAQ

Q: 为什么内层 src 和 dst 都设为本机 IP?

这样 EVR 反射回来的内层帧 dst 就是本机,本机 raw socket 直接收得到,无需在对端起 server,也不需要单独申请回包用的 IP。这是 evr 的核心"巧妙设计",详见第 2 节

Q: 那"哪个 EVR 反射回来的"怎么区分?

把目标 EVR 的源 IP 嵌进 payload 第 24~28 字节,回包解析后用 peerByEVRSrc[ip] 一步定位到 target,不再需要 5 元组反查表。

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: BPF 过滤都过滤了哪些字段?

读 socket 的 BPF 程序匹配两个条件:(1) IPv4 协议号 = UDP;(2) IPv4 TOS = cfg.tos;(3) UDP 目的端口 = inner_dst_port。三个条件同时命中才放行,确保不被同机器其他 UDP 流量打扰。写 socket 上则装一个"全丢"的 BPF,以阻止内核给这个发送 socket 排队任何回包。

Q: 4 种 Salt 模式为什么这么选?

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

Q: 为什么 evr 要用 sudo?

需要创建 ip4:udp raw socket、启用 IP_HDRINCL、设置 IP TOS/DSCP,且要装 BPF。Linux 上需要 CAP_NET_RAW;最简单的就是 sudo 或 systemd 以 root 启动。

Q: 配置文件改了要重启吗?

需要。evr 不支持热加载。建议 systemd 管理:sudo systemctl restart evr

Q: 怎么优雅停止?

发送 SIGINT (Ctrl+C) 或 SIGTERM。程序会取消所有 goroutine 并睡眠 1 秒等在途报文回来后退出。

Q: macOS 能跑吗?

仅作为编译/开发环境。运行时 macOS 不允许 ip4:udp raw socket 与 IP_HDRINCL 的组合,会报 socket: operation not permitted。生产请用 Linux。