9.9 KiB
Tun device 写侧探针(x 侧)Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: x tun handler 在 client 模式下每 30s 往 device 写一个自回环 UDP 探针, 把发送/确认计数交给调用方(wisper 经 metadata 回调收数),读环路零改动。
Architecture: 新文件 probe.go(组包纯函数 + 探针循环,与 keepalive 同款
per-dial-iteration 生命周期);x/observer/stats 加两个计数器(加法字段,
core 不动);metadata 加 probe 开关(缺省关,gost 零行为变化)。
Tech Stack: Go, golang.org/x/net/ipv4(已在用)组包,UDP checksum 置零。
Spec: wisper/docs/superpowers/specs/2026-10-07-wisper-tun-probe-design.md
(§1/§2 是本 plan 的依据;本 plan 只覆盖 x 侧,wisper 侧见 Plan B
wisper/docs/superpowers/plans/2026-10-07-tun-probe-watchdog.md)
Global Constraints
core/一行不碰(Kind 常量在 x 内定义,见 Task 1 精确值)。- 缺省关闭:metadata 无
probe键时不建 socket、不起 goroutine、不写计数 (gost 路径零变化)。 - 本 plan 的每一步都不
commit/不push(仓库约定 no-auto-commit); 每个 Task 结束时工作区只留该 Task 的文件待审。 - 发版(Task 4 的 tag+push)由 maintainer 手动做,执行人不碰。
Review Focus
- 探针包绝不能离开本机:dst 必须是自绑 socket 的地址——Task 2 的测试 pin 死。
- IPv6-only 设备:探针禁用记 warn,不 crash——Task 2 的测试 pin 死。
- IP 头 checksum 必须正确(内核静默丢坏包)——Task 2 的测试用解析器验。
probeReport为 nil(gost/旧调用方):调用前判空——Task 3 的测试 pin 死。- 重拨时旧探针必须退出(per-iteration ctx),且双 writer 并发写 tun fd 是安全的
(datagram 原子,不加锁)——Task 3 接线时保持
keepalive同款模式。
Task 1: metadata 开关 + 探针计数器
Files:
- Modify:
x/handler/tun/metadata.go - Modify:
x/observer/stats/stats.go - Test:
x/handler/tun/metadata_test.go(若无则新建;先ls确认)、x/observer/stats/stats_test.go(追加)
Interfaces:
-
Consumes:
mdutil.GetBool现有模式(metadata.go:23,31)。 -
Produces:
metadata.probe bool、metadata.probeReport func(sentDelta, ackedDelta uint64);xstats.KindProbeSent stats.Kind = 101、xstats.KindProbeAcked stats.Kind = 102(101/102:core 用 1–5,x 内无其他自定义 Kind,测试用 999 避开);Stats.probeSent/probeAcked atomic.Uint64+Add/Get的 case (Task 2/3 用h.options之外的stats.Stats句柄传参,签名见各 Task)。 -
Step 1: 写 failing test(metadata)
func TestParseMetadataProbeDefaultOff(t *testing.T) {
h := &tunHandler{}
if err := h.parseMetadata(mdx.NewMetadata(map[string]any{})); err != nil {
t.Fatal(err)
}
if h.md.probe {
t.Fatal("probe defaults off")
}
}
(mdx 是 x/metadata 下 metadata 构造的包名,先 grep 确认,tun 包内已有用法。)
- Step 2: 运行,确认 FAIL(
h.md.probe未定义)
Run: go test ./handler/tun/ -run TestParseMetadataProbe -v
Expected: 编译失败。
- Step 3: 实现
metadata加两个字段 +parseMetadata解析
probe = mdutil.GetBool(md, "tun.probe", "probe");
probeReport 从 md.Get("tun.probeReport") 取不到则试 md.Get("probeReport"),
类型断言 func(uint64, uint64),失败留 nil(不断言 panic)。
- Step 4: 写 failing test(Stats 计数器)
func TestStatsProbeKinds(t *testing.T) {
s := NewStats(false)
s.Add(KindProbeSent, 1)
s.Add(KindProbeAcked, 1)
if s.Get(KindProbeSent) != 1 || s.Get(KindProbeAcked) != 1 {
t.Fatal("probe counters not tracked")
}
}
- Step 5: 运行,确认 FAIL
Run: go test ./observer/stats/ -run TestStatsProbeKinds -v
Expected: FAIL(未知 Kind 被 switch 吞掉,读回 0)。
-
Step 6: 实现 Kind 常量 + Stats 字段 + Add/Get case
-
Step 7: 补 metadata 的
probe:true解析测试 + report-func 透传测试, 跑全包
Run: go test -count=1 ./handler/tun/ ./observer/stats/
Expected: PASS。
Task 2: 探针组包 + 循环
Files:
- Create:
x/handler/tun/probe.go - Test:
x/handler/tun/probe_test.go
Interfaces:
- Consumes: Task 1 的
metadata.probe(调用方判断,本文件不管开关)。 - Produces:
probeInterval = 30 * time.Second、probeRecvTimeout = 5 * time.Second、probeMagic = "WSPROBE!"(8 字节);func buildProbePacket(src net.IP, dstPort uint16, seq uint64) []byte;func runDeviceProbe(ctx context.Context, dev io.Writer, ip net.IP, report func(sentDelta, ackedDelta uint64), log logger.Logger)。
包格式(IPv4 头 20B + UDP 头 8B + magic 8B + seq 8B = 44B):
src == dst == ip(第一个 v4 spoke 地址),UDP checksum 置零(IPv4 下 0 = 无校验,
内核接受),IP 头 checksum 必须正确计算(标准 internet checksum,
全零和取反——这是唯一需要手写的算法,其他照抄 ipv4.Header.Marshal 或手排字节均可,
测试会验)。
循环语义:每 tick 发一次(report(1,0) 先记 sent,write 报错也记——acked 停滞
本身就是信号);SetReadDeadline(now+5s) 等 socket 回包,magic+seq 命中记
report(0,1);超时/错包不记;ctx.Done() 退出。socket 为
net.ListenUDP("udp4", &net.UDPAddr{IP: ip, Port: 0}),ip.To4()==nil 时直接
返回(IPv6-only 禁用,调用方记 warn——调用方是 Task 3,见下)。
- Step 1: 写 failing test(组包纯函数)
func TestBuildProbePacket(t *testing.T) {
src := net.ParseIP("10.10.100.250")
pkt := buildProbePacket(src, 54321, 7)
if len(pkt) != 44 { t.Fatalf("len=%d", len(pkt)) }
if pkt[0] != 0x45 || pkt[9] != 17 { t.Fatal("not v4/UDP") }
if !net.IP(pkt[12:16]).Equal(src) || !net.IP(pkt[16:20]).Equal(src) {
t.Fatal("src/dst must both be the spoke address")
}
// IP 头 checksum 自验;UDP checksum 必须为零;magic+seq 位置值正确。
}
- Step 2: 运行,确认 FAIL(函数未定义)
Run: go test ./handler/tun/ -run TestBuildProbePacket -v
Expected: 编译失败。
-
Step 3: 实现
buildProbePacket -
Step 4: 运行,确认 PASS
-
Step 5: 写 failing test(循环:活设备推进 sent,acked 停滞可观测)
func TestRunDeviceProbeReportsSent(t *testing.T) {
device, kernelSide := net.Pipe() // 同 client_test.go 模式
defer device.Close()
defer kernelSide.Close()
var sent, acked uint64
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go runDeviceProbe(ctx, device, net.ParseIP("10.10.100.250"),
func(sd, ad uint64) { sent += sd; acked += ad }, xlogger.Nop())
// kernelSide 读到第一个包:断言 44B 且 magic 命中;此时 sent>=1 且 acked==0。
}
(xlogger.Nop() 用法同 client_test.go:67;runDeviceProbe 内 socket 绑
10.10.100.250:0 在容器里合法——无该地址也 bind 得上?ListenUDP 绑非本地
IP 会报错!测试改用 127.0.0.1 作 spoke IP,语义等价。)
-
Step 6: 运行,确认 FAIL
-
Step 7: 实现
runDeviceProbe -
Step 8: 补测试——写坏的设备不 panic 且 acked 永远为 0
func TestRunDeviceProbeDeadDevice(t *testing.T) {
device, kernelSide := net.Pipe()
kernelSide.Close() // 写端坏掉:write 报错(备用:device.Close() 后写)
// 跑两个周期:report 的 sent 在涨,acked==0,全程无 panic。
}
- Step 9: 跑全包
Run: go test -count=1 ./handler/tun/
Expected: PASS(约 15s,内有既有 14s 的用例)。
Task 3: 接入 handleClient
Files:
- Modify:
x/handler/tun/client.go(handleClient内go h.keepalive(...)旁, 约 62 行) - Test: 无新增集成测试(
handleClient需要Router.Dial,既有测试只到transportClient层,keepalive 同款无覆盖;覆盖由 Task 1 的解析测试 + Task 2 的循环测试承担,此处只做接线)
Interfaces:
-
Consumes: Task 1 的
h.md.probe/h.md.probeReport,Task 2 的runDeviceProbe,handleClient已有的iterCtx、conn(device)、ips。 -
Produces: 无新导出(行为:
probe开时每轮 dial 起一个探针,iterCtx结束即退)。 -
Step 1: 接线(幂等小改)
if h.md.probe {
go h.probeDevice(iterCtx, conn, ips)
}
其中 probeDevice 为 tunHandler 的小方法:取 ips 第一个 To4() 非空者,
nil report 判空(gost 路径),无 v4 时 log.Warn 后返回;其余直调
runDeviceProbe。report 透传 h.md.probeReport(nil-safe:调用前判空,
判空逻辑放在 probeDevice 一处,runDeviceProbe 假定非空——测试只测
runDeviceProbe 非空路径 + probeDevice 的空 report 不起循环)。
- Step 2: 补
probeDevice的两个小测试(nil report 不启动; 全 v6 ips 直接返回),跑全包
Run: go test -count=1 ./handler/tun/ ./observer/stats/
Expected: PASS。
- Step 3:
go vet+gofmt -l扫一遍改动文件
Task 4: 发版 x v0.23.0(maintainer 手动)
- Step 1: 确认工作区只有本 plan 的文件(
git status --short) - Step 2: 全量构建 + 相关测试
Run: go build ./... && go test -count=1 ./handler/tun/ ./observer/stats/
Expected: PASS。
- Step 3: 打 tag 并推(minor:新功能;patch 不够格)
git tag -a v0.23.0 -m "v0.23.0: tun device write-side probe (metadata probe, probe counters)"
git push origin v0.23.0
- Step 4: 通知 Plan B 可开始(bump 到 v0.23.0)。