Files
Test User 4d8a295504
ci / test (push) Successful in 10s
ci / release-artifacts (push) Skipped
refactor: remove legacy server-driven SSE test path
The browser and CLI flows both use download/upload/complete; the
/api/tests/{id}/events endpoint and its Runner plumbing were dead code
that could bypass the single-active-test lock via expireActiveLocked
promoting the queue while an SSE stream was still running.

Remove the endpoint, Runner option, DefaultRunner, probe.Run engine and
the frontend subscribe/closeSource leftovers. Add a regression test
asserting the endpoint returns 404 and create responses omit eventsUrl.
2026-09-22 14:25:30 +08:00

19 KiB
Raw Permalink Blame History

AGENTS.md

本文件是 NetStable 项目的维护入口。任何自动化代理、Codex 会话或人工维护者在改动本仓库前都应先读这里。

基本原则

  • 用最简单、直接、改动最少的实现,优先复用现有文件、函数和类型。
  • 仅为当前实际需求或明显重复逻辑增加抽象,不为未来扩展提前设计。
  • 替换实现时移除旧路径,避免新旧双轨。
  • 不做顺手重构。除非它直接服务当前问题,否则不要改命名、目录结构、样式或部署流程。
  • 不回滚用户或其他维护者的改动。遇到不相关的脏工作区,忽略;遇到相关冲突,先读懂再处理。
  • 所有影响生产行为的改动都必须有验证证据:至少跑相关测试;改到公共路径时跑 go test ./... -count=1。
  • 完成代码改动后,提交并对齐 Gitea,除非用户明确要求不要提交或不要推送。

项目概况

NetStable 是一个独立 Go Web/CLI 测速项目,用于测试用户浏览器或 CLI 到部署服务器的网络质量。

核心能力:

  • 浏览器测速:用户打开网页后测试下载、上传、延迟和稳定性。
  • CLI 测速:同一个二进制可作为客户端运行,支持 HTTP 和 iperf3 模式。
  • 全局锁:同一时间只允许一个用户真正测速,其它用户进入 FIFO 队列。
  • 半截记录:已经产生样本的中断/停止测试也必须保存为历史记录;还没轮到且没有样本的排队取消不生成空记录。
  • 历史记录:完整样本曲线、摘要、脱敏 IP、地区、运营商写入 JSONL。
  • 域名访问:生产主入口是 https://speedtest.hzct.obscura.work/。

仓库信息

  • 本地路径:/Users/kanshan/Documents/speedtest
  • Gitea 仓库:https://git.aiot.ml/kanshan/speedtest
  • 远端名:origin
  • 远端主分支:main
  • 当前本地工作分支通常是:codex/go-network-stability-tester
  • 推送当前 HEAD 到 Gitea 主分支:
git push origin HEAD:main

提交前后检查:

git status --short --branch
git log --oneline --decorate -5
git remote -v
git ls-remote origin refs/heads/main

不要提交 dist/、本地 JSONL 记录、日志或临时文件;这些已在 .gitignore 中排除。

代码结构

  • cmd/netstable/main.go

    • CLI 入口。
    • 默认不带子命令时启动服务端。
    • serve 启动 Web 服务。
    • client 运行 HTTP/iperf3 客户端测速。
  • internal/web/server.go

    • HTTP API、网页静态资源、队列、全局锁、记录完成、下载/上传接口、iperf3 会话。
    • 这是生产行为最关键的文件,修改前必须读相关测试。
    • 测试 SSE 探测端点(/api/tests/{id}/events)已移除;浏览器和 CLI 测速统一走 download/upload/complete,页面只保留 /api/health/events 心跳。
  • internal/client/client.go

    • HTTP CLI 客户端。
    • 负责创建会话、排队等待、下载/上传阶段、complete/cancel。
    • 已产生样本后中断,必须走 partial complete,不能只 cancel。
  • internal/client/iperf3.go

    • CLI iperf3 模式。
    • 服务端创建一次性高端口,客户端执行本机 iperf3,完成后回写记录。
  • internal/probe

    • TCP/HTTP 探测样本和摘要计算。
  • internal/store

    • JSONL 历史记录读写。
    • 单条记录可能很大,读取端已提高到 64 MiB 单行上限。
  • internal/geo

    • IP 地区/运营商解析。
    • 优先内置 ip2region_v4.xdb,再走在线 fallback。
  • internal/limit

    • 程序级带宽限速。
    • 生产当前默认不限速。
  • web/static

    • 前端 HTML/CSS/JS。
    • 一键 CLI 命令由 web/static/app.js 根据当前 window.location.origin 生成。
  • data/ip2region_v4.xdb

    • 内置 IPv4 数据库。
  • deploy/netstable.service

    • systemd 服务模板。
  • .gitea/workflows/ci.yml

    • Gitea Actions:测试、vet、release artifact。

关键业务约束

这些约束比局部代码风格更重要,不能破坏。

  1. 同一时间只能有一个真正测速会话。

    • Server.activeID 是全局锁。
    • 普通 Web/CLI HTTP 测试和 iperf3 测试必须共用同一个锁。
    • 其它请求必须排队,而不是并发跑测速。
  2. 队列必须 FIFO。

    • 新请求在有 active 会话时进入 Server.queue。
    • active 完成、取消或过期后只提升队列头。
    • 手动停止或 CLI 中断必须释放队列。
  3. 已产生样本的半截测试必须保存。

    • CLI HTTP 中断:如果 samples 非空,先 complete partial,再返回原始中断错误。
    • 浏览器停止/刷新:如果 state.samples.length > 0,优先 complete partial。
    • 还没轮到、没有样本的取消只释放队列,不写空记录。
  4. complete 成功写入记录后才释放会话。

    • 如果 Store.Append 失败,complete 应返回 500,并保留会话以便重试。
    • 不能先 takeSession 再写盘,否则会丢记录。
  5. 历史记录必须保护隐私。

    • API 输出的 IP 必须脱敏。
    • ISP 缺失时显示 未知运营商。
    • 不要在前端、日志或 API 输出完整用户 IP,除非用户明确要求调试且范围受控。
  6. 大样本记录必须可保存和读取。

    • complete body 上限:64 MiB。
    • JSONL 单行读取上限:64 MiB。
    • 不要把限制改回 2 MiB 或默认 scanner 64 KiB。
  7. 峰值口径不能用单次极短请求直接当最大值。

    • 现有逻辑使用持续窗口计算 max,避免计时误差导致几百 Mbps 的假峰值。
  8. 生产默认不限速。

    • -bandwidth-limit-mbps 0 表示不限速。
    • 如果用户要求限速,必须同时更新页面显示和服务启动参数。

常用开发命令

本地测试:

go test ./... -count=1
go vet ./...

运行服务端:

go run ./cmd/netstable serve -listen 127.0.0.1:18080 -data /tmp/netstable-records.jsonl -geo-base ''

运行 CLI HTTP 测试:

go run ./cmd/netstable client -server http://127.0.0.1:18080 -duration 15

构建本机二进制:

make build

构建 Linux release 包:

make release VERSION=v0.1.0

生成文件:

  • dist/netstable-v0.1.0-linux-amd64.tar.gz
  • dist/netstable-v0.1.0-linux-arm64.tar.gz
  • dist/checksums.txt

更新 IP 数据库:

make update-ip-db

更新数据库后必须检查 THIRD_PARTY.md 和 LICENSES/ip2region-LICENSE.md 是否仍然准确。

测试要求

最小规则:

  • 修改 Go 行为:跑对应 package 测试,再跑 go test ./... -count=1。
  • 修改公共服务端路径:同时跑 go vet ./...。
  • 修改 release/deploy/CI:跑 make release VERSION=v0.1.0。
  • 修改前端 JS:跑 node --check web/static/app.js 和 go test ./web -count=1。
  • 修改生产相关配置:先本地校验配置,再线上验证接口。

重点回归测试名称:

  • TestCompleteEndpointAcceptsLargeSamplePayload
  • TestCompleteEndpointCanRetryWhenPersistFails
  • TestRunPersistsPartialRecordWhenCanceledAfterSamples
  • TestRunCompletesBrowserCompatibleSpeedTest
  • TestRunIperf3CompletesOneOffServerSession
  • TestCancelActiveTestPromotesQueuedSession
  • TestCancelQueuedTestRemovesItAndUpdatesPositions
  • TestEventsEndpointIsRemoved
  • TestBrowserSummaryDoesNotTreatShortBurstsAsSustainedMax
  • TestStoreListsLargeRecordsWithFullSampleCurves

API 速查

  • GET /

    • Web UI。
  • GET /api/config

    • 返回带宽限制标签。
  • GET /api/records?limit=100

    • 返回历史记录,输出前会脱敏 IP。
    • limit 范围:1 到 1000。
  • GET /api/health/events?limit=1

    • SSE 心跳,用于页面连接状态。
  • POST /api/tests

    • 创建 HTTP/Web 测速会话。
    • 返回 queue/download/upload/complete/cancel URL。
  • GET /api/tests/{id}/queue

    • 查询排队位置。
  • POST /api/tests/{id}/complete

    • 提交浏览器或 HTTP CLI 样本并写历史。
  • POST /api/tests/{id}/cancel

    • 取消还未产生样本的会话或释放队列。
  • GET /api/download?testId={id}&bytes={n}

    • 下载测速数据。
  • POST /api/upload?testId={id}

    • 上传测速数据。
  • POST /api/iperf3/sessions

    • 创建一次性 iperf3 服务端会话。
  • POST /api/iperf3/sessions/{id}/complete

    • 提交 iperf3 结果并写历史。
  • GET /downloads/netstable-linux-amd64.tar.gz

    • CLI 一键下载包。
  • GET /downloads/netstable-linux-arm64.tar.gz

    • CLI 一键下载包。

生产环境概览

最后核对时间:2026-09-15 Asia/Shanghai。

主入口

  • URL:https://speedtest.hzct.obscura.work/
  • HTTPS:Caddy 自动申请 Let's Encrypt 证书。
  • 证书核对结果:
    • Subject:CN = speedtest.hzct.obscura.work
    • Issuer:Let's Encrypt
    • 当前证书有效期:2026-08-09 到 2026-11-07
  • HTTP 会 308 跳转 HTTPS。

生产节点

节点 用途 状态备注
103.46.93.244 主域名节点,Caddy 80/443 反代到本机 NetStable 18080 HTTPS/API 正常;2026-09-15 检查时 SSH 连接被远端关闭,部署前先恢复 SSH
110.42.44.35 直连备用节点,http://110.42.44.35:18080/ 2026-09-15 HTTP/API 正常,SSH 可用
103.46.93.38 历史直连节点,http://103.46.93.38:18080/ 2026-09-15 检查时 HTTP 空响应,SSH host key 变化;不要盲目信任或部署,必须先人工确认主机身份

主节点端口

103.46.93.244 预期监听:

  • :80 Caddy HTTP,自动跳转 HTTPS。
  • :443 Caddy HTTPS。
  • :18080 NetStable 服务。
  • 127.0.0.1:2019 Caddy admin。

主节点 Caddy 配置

路径:

/etc/caddy/Caddyfile

关键站点块:

speedtest.hzct.obscura.work {
	reverse_proxy 127.0.0.1:18080
}

最近已知备份:

/etc/caddy/Caddyfile.bak-20260610110050

修改 Caddy 后必须校验:

caddy fmt --overwrite /etc/caddy/Caddyfile
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
systemctl is-active caddy

验证 HTTPS:

curl -fsS -I https://speedtest.hzct.obscura.work/
curl -fsS https://speedtest.hzct.obscura.work/api/config | jq .
echo | openssl s_client -servername speedtest.hzct.obscura.work -connect speedtest.hzct.obscura.work:443 2>/dev/null | openssl x509 -noout -subject -issuer -dates

查看 Caddy 证书日志:

journalctl -u caddy --no-pager -n 100

systemd 服务

模板文件:

deploy/netstable.service

线上预期服务名:

netstable.service

预期服务配置:

ExecStart=/usr/local/bin/netstable -listen 0.0.0.0:18080 -data /var/lib/netstable/records.jsonl -downloads-dir /var/lib/netstable/downloads

常用命令:

systemctl status netstable --no-pager -l
systemctl restart netstable
systemctl is-active netstable
journalctl -u netstable --no-pager -n 100

注意:

  • 当前模板没有启用 -trust-proxy-headers。
  • 如果要通过 Caddy 获取真实客户端 IP,必须同时满足:
    • NetStable 只接受可信代理流量,或公网 18080 被防火墙挡住。
    • 服务启动参数显式加 -trust-proxy-headers。
  • 不要在 18080 仍公网直连时直接开启 -trust-proxy-headers,否则用户可伪造 X-Forwarded-For。

生产路径

  • 二进制:/usr/local/bin/netstable
  • 历史记录:/var/lib/netstable/records.jsonl
  • CLI 下载包目录:/var/lib/netstable/downloads
  • amd64 下载包:/var/lib/netstable/downloads/netstable-linux-amd64.tar.gz
  • arm64 下载包:/var/lib/netstable/downloads/netstable-linux-arm64.tar.gz

发布和部署手册

1. 发布前检查

git status --short --branch
go test ./... -count=1
go vet ./...
make release VERSION=v0.1.0

确认生成包:

ls -lh dist
cat dist/checksums.txt

2. 部署到可 SSH 节点

优先用 SSH 标准输入流上传。不要直接 scp 覆盖 /usr/local/bin/netstable,运行中的 Linux 二进制可能导致写入失败。

单节点部署模板:

host=110.42.44.35
ssh -o BatchMode=yes -o ConnectTimeout=10 root@$host 'mkdir -p /var/lib/netstable/downloads'
ssh -o BatchMode=yes -o ConnectTimeout=10 root@$host 'cat > /tmp/netstable.new' < dist/netstable-v0.1.0-linux-amd64/netstable
ssh -o BatchMode=yes -o ConnectTimeout=10 root@$host 'cat > /var/lib/netstable/downloads/netstable-linux-amd64.tar.gz' < dist/netstable-v0.1.0-linux-amd64.tar.gz
ssh -o BatchMode=yes -o ConnectTimeout=10 root@$host 'cat > /var/lib/netstable/downloads/netstable-linux-arm64.tar.gz' < dist/netstable-v0.1.0-linux-arm64.tar.gz
ssh -o BatchMode=yes -o ConnectTimeout=10 root@$host 'install -m 0755 /tmp/netstable.new /usr/local/bin/netstable && rm -f /tmp/netstable.new && systemctl restart netstable && systemctl is-active netstable'

多节点部署前先确认每台 SSH 身份和服务状态,不要无脑批量推。

3. 部署后验证

本地哈希:

shasum -a 256 dist/netstable-v0.1.0-linux-amd64/netstable

远端哈希:

ssh root@SERVER 'sha256sum /usr/local/bin/netstable'

直连节点检查:

curl -fsS "http://SERVER:18080/api/config" | jq .
curl -fsS "http://SERVER:18080/api/records?limit=1" | jq .
curl -fsS -r 0-127 "http://SERVER:18080/downloads/netstable-linux-amd64.tar.gz" >/tmp/netstable-range.bin
wc -c /tmp/netstable-range.bin

域名检查:

curl -fsS -I https://speedtest.hzct.obscura.work/
curl -fsS https://speedtest.hzct.obscura.work/api/config | jq .
curl -fsS "https://speedtest.hzct.obscura.work/api/records?limit=1" | jq .
curl -fsS "https://speedtest.hzct.obscura.work/api/health/events?limit=1"
curl -fsS -r 0-127 "https://speedtest.hzct.obscura.work/downloads/netstable-linux-amd64.tar.gz" >/tmp/netstable-https-range.bin
wc -c /tmp/netstable-https-range.bin

4. 推送 Gitea

git status --short --branch
git add <changed-files>
git commit -m "<message>"
git push origin HEAD:main
git ls-remote origin refs/heads/main

如果用户只要求文档变更,也要提交并推送,除非用户明确不要。

回滚手册

代码回滚

优先回滚到上一个已知好提交,重新构建并部署。

git log --oneline --decorate -10
git checkout <known-good-commit>
go test ./... -count=1
make release VERSION=v0.1.0

然后按“部署到可 SSH 节点”重新上传。

不要对用户工作区执行 git reset --hard,除非用户明确要求。

Caddy 回滚

先找备份:

ls -lh /etc/caddy/Caddyfile.bak-*

恢复:

cp /etc/caddy/Caddyfile.bak-YYYYMMDDHHMMSS /etc/caddy/Caddyfile
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
systemctl is-active caddy

数据回滚

历史记录是 append-only JSONL。修复坏记录时优先备份再处理:

cp /var/lib/netstable/records.jsonl /var/lib/netstable/records.jsonl.bak-$(date +%Y%m%d%H%M%S)

不要直接清空生产记录,除非用户明确要求。

排障手册

记录接口 500:failed to load records

常见原因:

  • JSONL 单行超过读取限制。
  • 某行 JSON 损坏。
  • 文件权限或磁盘问题。

检查:

journalctl -u netstable --no-pager -n 100
ls -lh /var/lib/netstable/records.jsonl
tail -n 3 /var/lib/netstable/records.jsonl

本项目已经把单行读取上限提高到 64 MiB;不要改回默认 scanner 行为。

complete 保存失败

原则:

  • 写记录失败时,会话必须保留,客户端可以重试。
  • 如果重试变成 404,说明 complete 路径又被改坏了。

优先跑:

go test ./internal/web -run TestCompleteEndpointCanRetryWhenPersistFails -count=1 -v

用户停止后队列不释放

检查:

  • 浏览器停止时有没有调用 partial complete 或 cancel。
  • CLI 中断时有没有调用 partial complete 或 cancel。
  • takeSession、finishSession、promoteNextLocked 是否被绕过。

优先跑:

go test ./internal/client -run TestRunCancelsQueuedSessionWhenContextStops -count=1 -v
go test ./internal/web -run 'TestCancelActiveTestPromotesQueuedSession|TestCancelQueuedTestRemovesItAndUpdatesPositions' -count=1 -v

峰值速度明显虚高

不要直接相信单个样本的 mbps。检查持续吞吐统计:

go test ./internal/web -run 'TestBrowserSummaryUsesSustainedTransferRatesForMax|TestBrowserSummaryDoesNotTreatShortBurstsAsSustainedMax' -count=1 -v

域名 HTTPS 失败

检查:

curl -I https://speedtest.hzct.obscura.work/
systemctl status caddy --no-pager -l
journalctl -u caddy --no-pager -n 100
caddy validate --config /etc/caddy/Caddyfile

如果证书过期或申请失败,先看 Caddy ACME 日志。不要手动复制不明来源证书覆盖 Caddy 管理目录。

直连 18080 正常但域名记录 IP 不准

原因通常是反向代理后没有启用可信代理头,或启用了但公网 18080 仍可被用户直连伪造。

正确处理顺序:

  1. 先决定是否只允许通过 Caddy 访问主节点。
  2. 用防火墙限制公网直接访问 18080,或让 NetStable 只监听 127.0.0.1:18080。
  3. 再给服务加 -trust-proxy-headers。
  4. 测试 IP 脱敏、ISP 和地区显示。

不要只加 -trust-proxy-headers。

SSH host key 变化

103.46.93.38 在 2026-09-15 检查时出现 host key changed。处理方式:

  1. 不要直接 ssh-keygen -R 后继续部署。
  2. 先通过控制台或可信渠道确认服务器是否重装、迁移或 IP 复用。
  3. 确认无误后再更新 ~/.ssh/known_hosts。
  4. 更新后先只跑只读检查,再部署。

安全和隐私

  • 不要把完整用户 IP 写到公开 API 响应。
  • 不要把 Gitea token、密码、SSH 私钥写入仓库、日志或最终回复。
  • Gitea 凭据由本机 Git credential helper 管理;不要打印明文。
  • 生产记录包含用户网络信息,下载或复制前先确认用途。
  • 对外开放的下载文件名必须限制在已知二进制包名,不能做任意文件下载。

CI 和 Release

Gitea Actions 文件:

.gitea/workflows/ci.yml

当前行为:

  • push 到 main、master、codex/**:运行测试和 vet。
  • pull request:运行测试和 vet。
  • tag v* 或手动触发:构建 release tar.gz 和 checksums,并上传 artifact。

如果改 Makefile、release 包内容或运行时依赖,必须同步检查 CI。

最终交付检查清单

代码或文档改动完成后:

git status --short --branch
go test ./... -count=1
go vet ./...

按改动类型补充:

  • 前端 JS:node --check web/static/app.js
  • release:make release VERSION=v0.1.0
  • 域名/证书:curl -I https://speedtest.hzct.obscura.work/

然后提交并推送:

git add <changed-files>
git commit -m "<message>"
git push origin HEAD:main

最终回复必须说明:

  • 改了什么。
  • 跑了哪些验证。
  • 是否已提交和推送 Gitea。
  • 如涉及生产,列出线上验证结果或明确说明未部署。