Skip to content

Commit 21376d4

Browse files
authored
Merge pull request #1 from kvmaker/todo/p02-async-send-queue
feat(performance): P02 异步转发队列 + e2e 设计 spec
2 parents 2921f0d + fc0d9c6 commit 21376d4

5 files changed

Lines changed: 669 additions & 13 deletions

File tree

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
# Mesh e2e 测试(基于 Docker)— 设计 Spec
2+
3+
> 状态:等待用户 review
4+
> 日期:2026-07-08
5+
> 范围:仅设计,不含实施
6+
7+
## 1. 背景
8+
9+
当前 `tests/integration_test.go` 是单进程 Go 集成测试(HTTP API、SQLite、设备表),不验证:
10+
11+
- 真实 TUN 设备的创建与路由注入
12+
- 真实跨主机(容器间)包转发
13+
- 性能指标(RTT、吞吐、丢包)
14+
- 链路恶化(延迟/抖动/丢包)下的行为
15+
- server / client 故障下的重连与优雅退出
16+
17+
为补足这些维度,本 spec 设计一套基于 Docker 的 e2e 测试,输出可观测的指标与明确的 pass/fail。
18+
19+
## 2. 目标
20+
21+
- 验证 mesh VPN 的端到端正确性:TUN 路由、跨 client 转发、server 中继、设备上下线、ACME/自签证书下连通。
22+
- 量化核心性能:RTT p50/p95/p99、TCP 吞吐、UDP 丢包率。
23+
- 验证故障路径:server 重启、client 失联、SIGTERM 优雅退出。
24+
- 提供 CI 可消费的 PASS/FAIL 报告。
25+
- 与现有 `tests/integration_test.go` 解耦:后者负责快速集成验证,e2e 负责深度验证。
26+
27+
## 3. 非目标
28+
29+
- 不覆盖安全/加密协议的负面测试(无效 token 拒绝等)—— 留给单元测试。
30+
- 不做 macOS host 上的 TUN e2e —— macOS Docker 不支持 utun,跨平台 TUN 验证统一在 Linux 容器内进行。
31+
- 不做大规模(>50 client)负载测试 —— 本 spec 关注 2 client + 1 server 的核心场景。
32+
- 不在 spec 内实现,只描述场景与接口。
33+
34+
## 4. 运行模型
35+
36+
### 4.1 容器拓扑
37+
38+
```yaml
39+
# tests/e2e/docker-compose.yml
40+
version: "3.9"
41+
services:
42+
server:
43+
build: { context: ../.., dockerfile: tests/e2e/Dockerfile.server }
44+
image: mesh-e2e/server:dev
45+
privileged: true
46+
cap_add: [NET_ADMIN, NET_RAW]
47+
networks: [meshnet]
48+
49+
client-a:
50+
build: { context: ../.., dockerfile: tests/e2e/Dockerfile.client }
51+
image: mesh-e2e/client:dev
52+
privileged: true
53+
cap_add: [NET_ADMIN, NET_RAW]
54+
devices: ["/dev/net/tun:/dev/net/tun"]
55+
networks: [meshnet]
56+
depends_on: [server]
57+
58+
client-b:
59+
同 client-a
60+
depends_on: [server]
61+
62+
networks:
63+
meshnet:
64+
driver: bridge
65+
```
66+
67+
要点:
68+
69+
- 全部 `privileged: true` + `NET_ADMIN/RAW`,TUN + tc 才能用。
70+
- 通过 Docker bridge 网络通信,模拟"跨主机"。
71+
- server / client / iperf3/nuttcp 都跑在容器内,host 不需装额外工具。
72+
- host 是 macOS / Linux / CI ubuntu-runner 都能跑同一份 compose。
73+
74+
### 4.2 镜像基线
75+
76+
- 基础镜像:`ubuntu:24.04`。
77+
- server 镜像:`meshd` 二进制 + bash + curl + jq + ca-certificates。
78+
- client 镜像:`mesh` 二进制 + bash + iperf3 + nuttcp + iputils-ping + fping + iproute2 + jq + ca-certificates。
79+
80+
### 4.3 TUN 处理
81+
82+
- Linux 容器用 `/dev/net/tun` 创建 `mesh0`,`internal/tun/tun_linux.go` 已支持,无需 build tag。
83+
- macOS host 不直接 e2e,但代码路径保留(`tun_darwin.go`)。
84+
85+
## 5. 目录结构
86+
87+
```text
88+
tests/e2e/
89+
├── docker-compose.yml
90+
├── Dockerfile.client
91+
├── Dockerfile.server
92+
├── run.sh # 一键启动 / 收尾
93+
├── lib/
94+
│ ├── helpers.sh # wait_ready / register_device / show_logs
95+
│ └── metrics.sh # 收集 RTT/吞吐/丢包
96+
├── scenarios/
97+
│ ├── 01-connectivity.sh # P0
98+
│ ├── 02-performance.sh # P0
99+
│ └── 03-failure.sh # P1
100+
├── fixtures/
101+
│ ├── meshd.yaml # server 配置
102+
│ └── netem.sh # tc netem 封装
103+
└── results/
104+
└── <timestamp>/ # JSON + log 输出
105+
```
106+
107+
驱动:bash + 简单 helper。
108+
理由:直接调 iperf3 / tc / ss / curl,跨平台问题少,CI 友好。
109+
110+
## 6. 关键设计选择
111+
112+
### 6.1 TLS / 证书
113+
114+
- 容器内 server 拿不到 Let's Encrypt 证书。
115+
- 引入 `MESH_TEST_TLS=off` 开关(推荐在 `internal/config` 落地):
116+
- server 跳过 `acme/autocert`,改用自签证书。
117+
- client `mesh join` 端允许自签(`InsecureSkipVerify`,已在 `internal/client/peers.go` 使用过)。
118+
- 退出测试时无需清理证书目录,容器销毁即可。
119+
120+
### 6.2 网络模拟(tc netem)
121+
122+
`fixtures/netem.sh` 封装:
123+
124+
```bash
125+
netem clean <iface>
126+
netem baseline <iface> # 0 干扰
127+
netem wan <iface> # 80ms ± 10ms, 1% loss
128+
netem bad <iface> # 200ms ± 50ms, 5% loss
129+
netem satellite <iface> # 600ms ± 100ms, 2% loss
130+
```
131+
132+
### 6.3 性能工具
133+
134+
- `iperf3`:TCP 吞吐(`1 stream`、`4 stream`)、UDP 模式。
135+
- `nuttcp`:备用,覆盖长流 + 小包。
136+
- `ping -c 200 -i 0.01`:RTT 分布。
137+
- `fping -p 20 -c 50`:并行 ping 抖动。
138+
- `ss -ti` / `netstat -s`:重传统计。
139+
140+
### 6.4 mesh 启动流程
141+
142+
server 容器:
143+
144+
```text
145+
1. /usr/local/bin/meshd init
146+
2. /usr/local/bin/meshd run
147+
3. 等 :443 可达 / `/api/devices` 200
148+
```
149+
150+
client 容器:
151+
152+
```text
153+
1. wait_for_server (curl https://server:443/api/devices)
154+
2. mesh join <server-domain> --token <tok>
155+
3. mesh up
156+
4. ip route show | grep 10.100.0.0/24
157+
5. ping 10.100.0.1
158+
```
159+
160+
### 6.5 失败注入
161+
162+
- server 容器:`docker compose kill server` / `docker compose restart server`
163+
- 链路:`tc qdisc change ... loss 50%`
164+
- client:`docker compose kill client-a`
165+
166+
## 7. 场景拆分
167+
168+
### 7.1 场景 01:连通性与路由(P0)
169+
170+
```text
171+
01.1 启动 server,等 /api/devices 可达
172+
01.2 client-a join + up;client-b join + up
173+
01.3 ip route 校验:10.100.0.0/24 dev mesh0
174+
01.4 ping 10.100.0.1(server)必须通
175+
01.5 ping client-b(10.100.0.3)必须通
176+
01.6 ping 不存在的 10.100.0.99 必须 100% 丢包
177+
01.7 kill client-b;client-a ping 10.100.0.3 应超时
178+
01.8 server route table 移除
179+
01.9 restart client-b;重新 join;client-a ping 恢复
180+
01.10 fping -p 20 -c 50 不丢
181+
```
182+
183+
判定:每个 case 必须 100% 符合预期,任意 fail → 整体 fail。
184+
185+
### 7.2 场景 02:性能与抖动(P0)
186+
187+
```text
188+
02.1 baseline iperf3 -c 10.100.0.3 -t 30 -P 1
189+
02.2 iperf3 -c 10.100.0.3 -t 30 -P 4
190+
02.3 iperf3 -u -b 100M -t 30(UDP 100Mbps)
191+
02.4 加 wan netem 后重测 02.1 / 02.3
192+
02.5 加 bad netem 后重测 02.1
193+
02.6 ping -c 200 -i 0.01 收集 RTT
194+
02.7 5 分钟长流,验证 Tx/Rx 计数不漂移
195+
```
196+
197+
输出 `02-performance.json`
198+
199+
```json
200+
{
201+
"tcp_1stream_mbps": 92.3,
202+
"tcp_4stream_mbps": 110.5,
203+
"udp_100m_loss_pct": 0.7,
204+
"wan_tcp_mbps": 78.4,
205+
"wan_udp_loss_pct": 1.4,
206+
"rtt_p50_ms": 81.2,
207+
"rtt_p95_ms": 95.0,
208+
"rtt_p99_ms": 110.4
209+
}
210+
```
211+
212+
判定:**软门槛**默认只报告;`--strict` 触发硬门槛:
213+
214+
```text
215+
rtt_p95_ms < 200
216+
tcp_1stream_mbps > 30
217+
wan_udp_loss_pct < 5
218+
```
219+
220+
### 7.3 场景 03:故障 / 重连 / 优雅退出(P1)
221+
222+
```text
223+
03.1 docker kill server;观察 client 错误日志;2-5s 内重连尝试
224+
03.2 server restart;client 自动恢复
225+
03.3 重建连接后 ping / iperf 复测
226+
03.4 长流中短暂 server 中断,client 重建,iperf 重新建立
227+
03.5 满队列抗压:iperf3 + tc 丢包 30%,观察 drop 计数与吞吐变化
228+
03.6 SIGTERM client:必须 <=2s 退出,不留 zombie goroutine
229+
03.7 client 连续 join 10 次,server 必须正确处理
230+
```
231+
232+
## 8. 结果收集与判定
233+
234+
```text
235+
results/
236+
└── 2026-07-08T10-30-00/
237+
├── 01-connectivity.log
238+
├── 01-connectivity.json
239+
├── 02-performance.log
240+
├── 02-performance.json
241+
├── 03-failure.log
242+
├── 03-failure.json
243+
└── summary.txt
244+
```
245+
246+
`summary.txt`
247+
248+
```text
249+
[P0] 01-connectivity: PASS (9/9)
250+
[P0] 02-performance: PASS (soft); tcp=92Mbps rtt_p95=95ms
251+
[P1] 03-failure: PASS (6/6)
252+
Overall: PASS
253+
```
254+
255+
## 9. CI 集成
256+
257+
GitHub Actions(在 `.github/workflows/` 下新增 `e2e.yml`):
258+
259+
```yaml
260+
e2e:
261+
runs-on: ubuntu-latest
262+
steps:
263+
- uses: actions/checkout@v4
264+
- run: docker compose -f tests/e2e/docker-compose.yml build
265+
- run: ./tests/e2e/run.sh --all
266+
- run: ./tests/e2e/run.sh --report | tee junit.xml
267+
- uses: actions/upload-artifact@v4
268+
with: { name: e2e-report, path: tests/e2e/results/ }
269+
```
270+
271+
- push 触发软门槛(默认)。
272+
- merge to master 触发硬门槛(`--strict`)。
273+
- release tag 触发全量 + 严格。
274+
275+
## 10. 与现有 `tests/integration_test.go` 的边界
276+
277+
```text
278+
tests/integration_test.go 单元 + HTTP API 集成(无 TUN / 无网络)
279+
tests/e2e/ 完整 e2e(容器 + TUN + tc + 性能)
280+
```
281+
282+
不重叠。前者跑得快(CI 默认每次 push),后者跑得慢(合并前 / release 前)。
283+
284+
## 11. 实施 TODO
285+
286+
实施时建议拆为以下 TODO(落到 `docs/todo/testing/`):
287+
288+
- T00:MESH_TEST_TLS 开关 + server 自签支持。
289+
- T01:Dockerfile.server / Dockerfile.client 与 docker-compose.yml。
290+
- T02:lib/helpers.sh + lib/metrics.sh。
291+
- T03:fixtures/netem.sh。
292+
- T04:scenario 01-connectivity。
293+
- T05:scenario 02-performance。
294+
- T06:scenario 03-failure。
295+
- T07:run.sh + 结果聚合 + summary.txt。
296+
- T08:CI workflow。
297+
298+
## 12. 风险与缓解
299+
300+
| 风险 | 缓解 |
301+
|------|------|
302+
| `tc` 在 macOS host 不可用 | e2e 全在 Linux 容器内 |
303+
| Docker privileged 模式安全风险 | 仅 CI/本地开发,文档明示 |
304+
| 性能数据受 host 负载影响 | 软门槛 + 历史趋势对比 |
305+
| server 拿不到 ACME 证书 | 引入 `MESH_TEST_TLS=off` 开关 |
306+
| `mesh join` 在 TUN 起来前需要 DNS | 容器内 `/etc/hosts` 注入 server 别名 |
307+
| 长时间 iperf 占用 CI 资源 | 默认 30s 短流,CI 用 `--quick` 模式 |
308+
309+
## 13. 参考资料
310+
311+
- 当前 `tests/integration_test.go`
312+
- `internal/tunnel/router.go`、`internal/tunnel/server.go`(P02 已落地)
313+
- `docs/todo/performance/performance.md`(性能目标基线)
314+
- Tailscale / ZeroTier 的 e2e 思路(控制面与数据面分离 + 容器化拓扑)

internal/tunnel/packet.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ import (
55
"net/netip"
66
)
77

8+
// DefaultSendQueueSize is the per-connection send queue size used by the
9+
// async forwarding model introduced in TODO P02.
10+
const DefaultSendQueueSize = 1024
11+
12+
// Packet carries an IP packet through the per-connection send queue.
13+
// It owns its own byte slice; callers must not mutate Data after handing it
14+
// to Enqueue.
15+
type Packet struct {
16+
Data []byte
17+
}
18+
819
// ExtractDstIP extracts the destination IP address from an IP packet header.
920
// Supports both IPv4 and IPv6 packets.
1021
func ExtractDstIP(pkt []byte) (netip.Addr, error) {

0 commit comments

Comments
 (0)