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

649 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 主分支:
```bash
git push origin HEAD:main
```
提交前后检查:
```bash
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` 表示不限速。
- 如果用户要求限速,必须同时更新页面显示和服务启动参数。
## 常用开发命令
本地测试:
```bash
go test ./... -count=1
go vet ./...
```
运行服务端:
```bash
go run ./cmd/netstable serve -listen 127.0.0.1:18080 -data /tmp/netstable-records.jsonl -geo-base ''
```
运行 CLI HTTP 测试:
```bash
go run ./cmd/netstable client -server http://127.0.0.1:18080 -duration 15
```
构建本机二进制:
```bash
make build
```
构建 Linux release 包:
```bash
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 数据库:
```bash
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 配置
路径:
```bash
/etc/caddy/Caddyfile
```
关键站点块:
```caddyfile
speedtest.hzct.obscura.work {
reverse_proxy 127.0.0.1:18080
}
```
最近已知备份:
```bash
/etc/caddy/Caddyfile.bak-20260610110050
```
修改 Caddy 后必须校验:
```bash
caddy fmt --overwrite /etc/caddy/Caddyfile
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
systemctl is-active caddy
```
验证 HTTPS:
```bash
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 证书日志:
```bash
journalctl -u caddy --no-pager -n 100
```
### systemd 服务
模板文件:
```bash
deploy/netstable.service
```
线上预期服务名:
```bash
netstable.service
```
预期服务配置:
```ini
ExecStart=/usr/local/bin/netstable -listen 0.0.0.0:18080 -data /var/lib/netstable/records.jsonl -downloads-dir /var/lib/netstable/downloads
```
常用命令:
```bash
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. 发布前检查
```bash
git status --short --branch
go test ./... -count=1
go vet ./...
make release VERSION=v0.1.0
```
确认生成包:
```bash
ls -lh dist
cat dist/checksums.txt
```
### 2. 部署到可 SSH 节点
优先用 SSH 标准输入流上传。不要直接 `scp` 覆盖 `/usr/local/bin/netstable`,运行中的 Linux 二进制可能导致写入失败。
单节点部署模板:
```bash
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. 部署后验证
本地哈希:
```bash
shasum -a 256 dist/netstable-v0.1.0-linux-amd64/netstable
```
远端哈希:
```bash
ssh root@SERVER 'sha256sum /usr/local/bin/netstable'
```
直连节点检查:
```bash
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
```
域名检查:
```bash
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
```bash
git status --short --branch
git add <changed-files>
git commit -m "<message>"
git push origin HEAD:main
git ls-remote origin refs/heads/main
```
如果用户只要求文档变更,也要提交并推送,除非用户明确不要。
## 回滚手册
### 代码回滚
优先回滚到上一个已知好提交,重新构建并部署。
```bash
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 回滚
先找备份:
```bash
ls -lh /etc/caddy/Caddyfile.bak-*
```
恢复:
```bash
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。修复坏记录时优先备份再处理:
```bash
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 损坏。
- 文件权限或磁盘问题。
检查:
```bash
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 路径又被改坏了。
优先跑:
```bash
go test ./internal/web -run TestCompleteEndpointCanRetryWhenPersistFails -count=1 -v
```
### 用户停止后队列不释放
检查:
- 浏览器停止时有没有调用 partial complete 或 cancel。
- CLI 中断时有没有调用 partial complete 或 cancel。
- `takeSession`、`finishSession`、`promoteNextLocked` 是否被绕过。
优先跑:
```bash
go test ./internal/client -run TestRunCancelsQueuedSessionWhenContextStops -count=1 -v
go test ./internal/web -run 'TestCancelActiveTestPromotesQueuedSession|TestCancelQueuedTestRemovesItAndUpdatesPositions' -count=1 -v
```
### 峰值速度明显虚高
不要直接相信单个样本的 `mbps`。检查持续吞吐统计:
```bash
go test ./internal/web -run 'TestBrowserSummaryUsesSustainedTransferRatesForMax|TestBrowserSummaryDoesNotTreatShortBurstsAsSustainedMax' -count=1 -v
```
### 域名 HTTPS 失败
检查:
```bash
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 文件:
```bash
.gitea/workflows/ci.yml
```
当前行为:
- push 到 `main`、`master`、`codex/**`:运行测试和 vet。
- pull request:运行测试和 vet。
- tag `v*` 或手动触发:构建 release tar.gz 和 checksums,并上传 artifact。
如果改 Makefile、release 包内容或运行时依赖,必须同步检查 CI。
## 最终交付检查清单
代码或文档改动完成后:
```bash
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/`
然后提交并推送:
```bash
git add <changed-files>
git commit -m "<message>"
git push origin HEAD:main
```
最终回复必须说明:
- 改了什么。
- 跑了哪些验证。
- 是否已提交和推送 Gitea。
- 如涉及生产,列出线上验证结果或明确说明未部署。