来源:AGENTS.md## 隧道流量统计恒为 0:记账留在了数据面(v1.0.4 修复并上线)(原文 953 字符)

隧道流量统计恒为 0:记账留在了数据面(v1.0.4 修复并上线)

症状:面板「实时流量」恒为 0 B、流量统计页全是 0,而隧道访问一切正常

根因:1.0.1 那次「二选一」修复(内置反代接管 48080、数据面不再监听公网端口) 只搬走了流量,没搬走记账AddIn/AddOut/ConnOpened 仍只挂在 data/proxy.godata/router.godata/listener.go, 而反代这条真正承载流量的路径一次都没记过账。

通用教训:改动「谁承接流量」时,要顺着数据流问一句"原来挂在旧路径上的 副作用(统计、限速、鉴权、日志)跟过来了吗"。这类遗漏不会报错, 只会让某个数字恒为 0,主功能却完全正常——最容易被当成"还没人访问"而放过。

修法:在反代的隧道连接net.Conn)上记账,不包 ResponseWriter —— WebSocket 升级会走 Hijack 绕过 ResponseWriter,包在外层会漏掉这类连接; 包在最里层与协议无关,HTTP / SSE / WebSocket 一视同仁,口径也与数据面一致。 (proxy/tunnel_stats.go + transport.go 的 DialContext + CompileRuleWithStats

⚠️ 必须注入与 control / data / api 同一个 stats.Collector 实例: 隧道统计条目由控制面 Register 到该实例,换成另一个实例会让 AddIn/AddOut 因查不到条目而静默丢弃 —— 表现为"代码明明接了统计,面板却还是 0"。 CompileRule 的原签名保留(它还被"保存前先试编译"的调用点使用), 新逻辑走 CompileRuleWithStats

验证方式(可复现):访问一条隧道域名 3 次,断言 bytes_in/bytes_out 增量非 0; 同时断言未被访问的隧道仍为 0 —— 只看总量涨了就通过,会把"统计挂错了隧道"放过去。


来源:intranet-tunnel/docs/AGENTS-ARCHIVE.md## 反代规则同步缺陷的修复(2026-09-12 完成并部署验证)(原文 4612 字符)

反代规则同步缺陷的修复(2026-09-12 完成并部署验证)

前一节记录的「隧道变更后反代规则不自动更新」缺陷已修复并上线, 镜像 intranet-tunnel/server:1.0.11.0.0 保留便于回滚)。

缺陷一:规则不自动同步(已修)

根因proxy.Manager 早就提供了 EnsureTunnelRule / RemoveTunnelRule, 但全项目没有任何调用点。不能简单地在 control 里 import proxy —— proxy 反过来依赖 control(反代回源要走隧道),会形成循环依赖。

做法:依赖倒置,与既有的两阶段注入同一模式:

  • control 定义窄接口 TunnelRuleSyncer(参数用它自己的 Route,信息已足够)
    • ruleSyncer 字段 + SetTunnelRuleSyncer / ruleSyncerRef(与 registrar 共用一把锁)。
    • registerRoute 是隧道生效的唯一汇聚点(客户端登录、SyncClientTunnels

单隧道上线三条路径都经过它),在此挂钩即可全覆盖。

  • 注意:原实现里有 if r == nil { return } 的提前返回,会让未接数据面时

连反代规则也不更新 —— 已改为只跳过 AddRoute

  • RemoveTunnelRoute 同时摘除反代规则。
  • proxy 实现 SyncTunnelRule / DropTunnelRule,负责把 control.Route

补齐为 model.TunnelCustomDomain / RemotePort 是指针类型,用 nil 表达"不适用")。

  • internal/appproxyManager 构造后注入。

未注入时全部静默跳过,属可选增强,不影响「数据面自带域名路由」的既有路径。

缺陷二:48080 被两个监听器争抢(已修)

部署 1.0.1 时才暴露:数据面 PublicListener 与内置反代都监听 DataPort(48080), 同时启动必然一个 bind: address already in use,内置反代起不来 → 隧道域名全 502。

根因:这段「二选一」逻辑原本在旧版单文件 cmd/server/main.go 里, 代码重构拆到 internal/app/app.go丢失了,重建源码时没恢复。 (同类风险:pwsh 旁路修复的代码,一旦重构文件就会被静默丢掉。)

改动publicListener 改为按条件创建 —— proxyManager 启用时只打日志 公网流量入口由内置反向代理接管;否则维持数据面独立监听。 三处解引用补判空(Serve goroutine、drain.publicListener.Close())。

关键日志(判断规则是否装载)

公网流量入口由内置反向代理接管      ← 二选一生效
反向代理规则已重载 loaded=0 ...     ← 启动时的全量装载
隧道路由已同步到内置反向代理        ← 增量同步(本修复新增)

验证方式(可复现)

最强验证是不重启服务端

  1. delete from tunnels;(模拟"新隧道")
  2. 重启客户端
  3. 服务端日志出现 隧道路由已同步到内置反向代理 tunnel_id=N
  4. 访问域名返回 200,响应头 X-Proxy-Rule: 167772231<<24 + N

注意:直接删库会让内存里的旧规则残留(服务端不知道要摘除), 表现为 502 且错误信息里是旧隧道的名字。走面板 API 删除才会触发 RemoveTunnelRoute。验证时若混用了改库与重启,记得先清一遍内存状态。

⚠️ 配置漂移:生产 compose 的改动必须回写仓库

本次发现服务器上的 docker-compose.yml 比仓库版多两处挂载, 而它们正是之前踩坑后补的修复,只落在服务器、从未回写

- ./certs:/app/certs
- ./backups:/app/backups

后果:一旦从仓库重新部署,证书会再次写进匿名卷、宿主机 ./certs 恒为空, 而前置 nginx 读的正是那里。已以生产版为准回写仓库。

教训:在服务器上 sed/手改完 compose,立刻把改动同步回仓库并提交, 否则下次重建镜像/重新部署时这些修复会静默消失。

镜像更新流程(已验证)

服务器上没有源码(只有 compose、.env 与镜像 tar),部署方式是 「本地构建 → docker save → 上传 → load → 改 tag → up -d」:

# 1) 本地构建(只动 server 时增量很快)
docker build -f server/Dockerfile -t intranet-tunnel/server:1.0.1 `
  --build-arg GOPROXY=https://goproxy.cn,direct H:\Works\intranet-tunnel

# 2) 导出(单镜像 + 层去重后仅 12.6 MB)
docker save -o release/intranet-tunnel-server-1.0.1.tar intranet-tunnel/server:1.0.1

# 3) 上传(先 rm 远程同名文件,upload 有覆盖保护)
workbench upload <tar> /home/docker/intranet-tunnel/ -i i-wz9diqyfq2owhs5dn8gx

# 4) 服务器执行部署脚本(写成 .sh 上传再 sh 执行,绕开 workbench 传参坑)
docker load -i intranet-tunnel-server-1.0.1.tar
sed -i 's|intranet-tunnel/server:1.0.0|intranet-tunnel/server:1.0.1|' docker-compose.yml
docker compose up -d tunnel-server

打新 tag 而不是覆盖旧 tag1.0.0 留在本地与服务器上,出问题可立刻切回。


  • DSH 会话记录可作为源码恢复源(2026-09-12 实测,成功恢复整个项目):

项目文件被清理且无 git、无回收站条目、无卷影副本时,DSH 自己的会话记录是最后的手段。 位置:%APPDATA%\dsh-desktop\harness\sessions\<cwd转义名>\<session-id>\session.jsonl.zstd

  • 文件是多个紧邻 zstd frame 串联(增量追加)。zlib.zstdDecompressSync(整个文件) 只解第一帧

(通常只有 185 字节的会话头),必须逐帧推进:解压 buf.subarray(pos),再 buf.indexOf(MAGIC, pos+4)(MAGIC=28 b5 2f fd)作为下一帧起点。Node 的流式解压器会在第二帧 报 ZSTD_error_prefix_unknown不要用它

  • 事件为 JSONL:{"type":"tool/call","time":…,"data":{"name":"write","arguments":"{…}"}}

arguments字符串,需二次 JSON.parsewritecontenteditold_string/new_string

  • 只重放"当时成功"的操作tool/result 的文本以 Error: 开头即表示该次写入未发生

(常见于 file changed since it was read 这类守卫错误),照常重放会让状态偏离。

  • 同一会话可能被提取两次(重复文件)会导致每条 write/edit 应用两遍,症状是

method already declared / redeclared in this block。重放前务必确保每个会话只有一份事件流。

  • tool/resultread 的返回带 <path>行号内容,是独立于重放的快照,可作交叉校验;

不能用它覆盖文件(判据不可靠:结尾说明行 (End of file - total N lines)、 末尾换行等都会造成"差 1 字节"的假差异,也可能覆盖成更早的状态)。

  • 重放无法复现 pwsh 的旁路改动Move-Item/Remove-Item/git mv/Set-Content/here-string 写文件,

以及 go mod tidy 的依赖声明变更。实测缺失的 shared/muxserver/internal/appapp.goguardConfigapi/util.gomustJSON/parseIP/contextWithTimeout/decodeJSONOptional 等全部源于此。

  • PowerShell 5.1 的 Set-Content -Encoding UTF8 会写 BOM,会让 go.mod 解析失败

unexpected input character '\ufeff')。重放后必须统一清除文本文件 BOM

  • 含中文的 .ps1 必须存为 UTF-8 with BOM,否则 PS 5.1 按 ANSI 解析导致语法错误;

here-string 结束符 "@ 必须位于行首,插入代码时不要加缩进

  • 恢复脚本保留在 H:\Works\.recover\rebuild-all.ps1 为一键重建入口)。

技术文档库 / 流量统计恒为 0 0 0 sushike
2026-09-13T12:41:55.625722839Z 2026-09-13T13:37:01.464981383Z