| 项 | 值 |
|---|---|
| 项目 | dsh/dsh-client |
| 分支 | v0.3.0 |
| 基准 commit (base) | 9bf33d495345f7eb016c088e4324817fcd3a3239 |
| 目标 commit (head) | 6bf30b4d600bfaf3f5b26170731ae8854639159a |
| 主题 | FRP 内网穿透整体下线 → 自研 xtun 穿透服务上线(含壳子 4 参数向导、独立服务端 server/、协议共享包 xtun/);顺带合入 v0.2.2 弹窗滚动修复 |
| 质检对比 | 修复 55 条 / 新增 161 条(引擎真实数据 codescan_issues_diff) |
| 当前版本红线 | 仍存在 1 条 Critical:app/service.go:277 命令注入风险(本增量未触及该文件,属遗留) |
结论摘要:本次为一个高自研强度的特性增量——用全新的 KCP 中转 + 打洞协议替换了整个 FRP 子系统,新增约 20 个源文件、删除 frpc 三件套与 FRP 前端界面。整体架构清晰、加密与鉴权方向正确(HKDF + ChaCha20-Poly1305 + 恒定时间比较),但新增代码中存在若干会影响可用性与资源安全的实现缺陷,集中在「客户端连接生命周期」「服务端会话/配对生命周期」「控制面并发写」三处。建议在发布 v0.3.0 前优先修复。
| 目录/文件 | 说明 |
|---|---|
xtun/proto.go | 控制面帧定义与 TCP 长度前缀 + JSON 信封编解码 |
xtun/crypto.go | HKDF 密钥派生 + ChaCha20-Poly1305 AEAD 流(计数器防重放) |
xtun/mux.go | stream-id 多路复用分帧 |
xtun/kcp_session.go | KCP 可靠 UDP 数据连接封装(依赖新增 kcp-go v5.6.6) |
xtun/punch.go | 打洞探测帧与探测窗口 |
server/config.go | 服务端配置(JSON,key/port/logLevel) |
server/registry.go | 暴露注册表(同哈希接管 / 异哈希拒绝 / 一对一配对) |
server/control.go | TCP 控制面(Challenge/HMAC/注册/匹配/接管/配对通知) |
server/kcp_relay.go | KCP 腿关联 + 源地址回报 + 两腿双向 pipe |
server/main.go | 服务端组装(Run/RunCtx,可测优雅退出) |
app/frpc.go、app/frpc_bin.go、app/frpc_toml.go 及三个对应测试app/tunnel_perm_test.go(旧模型权限测试,权限断言已并入 app/tunnel_test.go)frontend/src/components/FRPSetupModal.tsxapp/app.go(Bind 方法重写:Open/Close/Connect/Disconnect + cancel 句柄)、app/role.go(阶段状态机/事件 dsh:xtun-status)、app/tunnel.go(配置模型 server/local/remote)、app/console_windows.go(注释)frontend/src/App.tsx、components/StatusBar.tsx、components/SettingsOverlay.tsx、components/InstallGuide.tsx、components/UpdateOverlay.tsx、index.css、wailsjs/go/models.ts、wailsjs/go/app/App.d.tsgo.mod/go.sum(新增 kcp-go v5.6.6、提升 golang.org/x/crypto 为直接依赖)、wails.json(0.2.1→0.2.2)、AGENTS.md、docs/README.md、.handoff/*⚠️ 数据解读提示:55 条"修复"中,相当一部分并非真正被改写修好的缺陷,而是因为文件被删除或行号漂移而从对比中消失。逐类拆解如下。
| 文件 | 问题 | 说明 |
|---|---|---|
app/frpc.go:213 | [Critical/Security] 命令注入风险 | 该文件整体删除,风险随之移除 |
app/frpc.go:89 | 圈复杂度过高 / 嵌套过深 / 函数过长 / 扇入扇出异常 / 高耦合(5 条) | runFrpc 整体删除 |
app/frpc.go:42,48,190 | 重复代码块 ×2、goroutine 泄漏风险 | 文件删除 |
app/frpc_bin.go:60,80 | 行过长、圈复杂度过高 | 文件删除 |
app/frpc_toml_test.go:28 | 行过长 | 文件删除 |
frontend/src/components/FRPSetupModal.tsx:27 | 行过长 | 文件删除 |
小结:FRP 子系统删除后,1 条 Critical 命令注入 + 约 10 条复杂度/耦合问题一并消除。这是本次"修复数"的主要来源,属于架构级删减收益,而非逐条修复。
涉及 app/app.go(frpcCmd/frpcStage 相关字段与 Open/Connect 方法整体重写)、app/role.go、frontend/src/App.tsx、components/SettingsOverlay.tsx、components/StatusBar.tsx、components/UpdateOverlay.tsx、frontend/wailsjs/go/models.ts。这些旧问题(上帝类、高耦合、圈复杂度、重复代码块等)多数只是位置变化,对照第 3 节可见在新行号上以"新增"形式重现。
真实改善:app/tunnel.go 配置模型由 5 字段的 TunnelLocal/7 字段的 TunnelRemote 收敛为 ServerCfg/LocalCfg/RemoteCfg,字段更精炼;app/role.go 的角色状态机与事件推送结构更清晰。
| 类别 | 数量级 | 说明 |
|---|---|---|
| 复杂度类(上帝类/圈复杂度/嵌套深度/函数过长/参数过多) | ~60 | frontend/src/App.tsx 的 App(548 行、圈复杂度 89)、app/xtun_data.go、app/xtun_client.go、server/* 的测试函数为主 |
| 依赖类(高耦合/扇入扇出异常) | ~45 | 大量落在测试文件(TestRunEndToEnd fan-out 33 等),以及 app/app.go 的 Bind 方法 |
| 并发类("goroutine started without context cancellation") | ~18 | 见 3.3,多为长生命周期 goroutine 或测试 goroutine,需甄别 |
| 可维护性类(重复代码块 ~23 / 行过长) | ~30 | 多为复制粘贴的测试样板与多行函数签名 |
| 死代码类(未使用参数) | 3 | app/xtun_data.go:125,172 的 sid;server/kcp_relay.go:327 的 uc |
| Bug 类(缺少 default 分支) | 3 | app/app.go:114、、 |
[Medium/Complexity] 上帝类 @ app/app.go:66:app.App 有 34 个方法(>20)。Bind 层职责持续膨胀。[Medium/Complexity] 圈复杂度过高 @ app/xtun_client.go:217:connectLoop 复杂度 21、104 行、高耦合 19 函数——重连状态机复杂度集中,是缺陷高发区(见 4.1/4.2)。[Medium/Dependency] 高耦合 @ app/app.go:382:ConnectRemote 直接依赖 22 个函数、fan-out 22。[Medium/Complexity] 递归调用 @ app/xtun_data.go:341:close 疑似递归(判定为误报,实为 dataPath.close 调 conn.Close)。[Medium/Concurrency] goroutine 泄漏风险 @ app/xtun_data.go:263,364,437、server/kcp_relay.go:230、xtun/kcp_session.go:45:见 3.3。引擎把"启动 goroutine 未传 context"统一标为泄漏风险。实际:
xtun/kcp_session.go:45(Start 起收包/定时循环)、server/kcp_relay.go:230(pipe goroutine)、app/xtun_data.go:263,364,437(心跳泵、看门狗、accept/read 循环)——均有 done/closeC/conn.Close 等退出通道,非真泄漏,但确实缺少统一的 context 取消链路,属于可维护性隐患。server/control_test.go:23、kcp_relay_test.go 多处、xtun/kcp_session_test.go 多处):测试 goroutine,影响有限。app/app.go:111(见 4.2)与 app/xtun_client.go 每轮重连新建的 <-ctx.Done() goroutine(见 4.1)。以下为对新增代码逐文件复核后确认成立的高价值问题(已用行号核对源码)。
ConnectRemote 可能永久阻塞(匹配失败路径)位置:app/app.go:382-435(ConnectRemote)、app/xtun_client.go:192-213(Connect)
ConnectRemote 在 select { case r := <-ch ...; case <-ctx.Done() } 上等待子 goroutine 回传;而子 goroutine 调用的 client.Connect 只在数据面首次就绪或终态 ErrAuth 时才向 first 通道回传。
问题:当 ConnectAck.Ok=false(暴露名未注册 / 密码不匹配 / 已有连接在进行)时,connectLoop 按注释「非终态,退避重试」无限重连,Connect 永不返回首个结果。于是:
ConnectRemote RPC 无限挂起,前端 await ConnectRemote(name) 卡住,UI 停在"重连中…",用户以为功能坏了;--role=remote 启动路径同样被阻塞。影响:可用性缺陷,且是最常见的用户误操作(填错暴露名/密码)必然触发。
建议:为首次就绪设置超时(如 30s),或在 ConnectAck.Ok=false 且尚未 firstSent 时,把可展示错误经 first 回传(区分「暴露端离线」与「配置错误」)。
位置:app/app.go:503-534(clearExposeCancel/clearConnectCancel 与 set*Cancel)
setConnectCancel(c) 会替换 a.connectCancel;而旧会话 goroutine 结束(含错误/取消分支)时调用 clearConnectCancel() 无条件把 a.connectCancel 置 nil。若用户在「断开→重连」或「关闭→重开」的间隙触发,旧 goroutine 会把新会话的 cancel 句柄清空,导致:
DisconnectRemote / Shutdown 的 cancelConnect() 拿到 nil → 新会话无法被取消;同类问题存在于 Expose 路径(clearExposeCancel)。
建议:为 cancel 句柄引入会话序号(generation),clear 时仅当序号匹配才置 nil(compare-and-set);或把 cancel 与 client 句柄封装进一个会话对象整体替换。
位置:server/control.go:24-43(wrapper 错误分支)、server/control.go:124(handleConnect 内 reg.Put)
handleConnect 在握手后先 reg.Put(session)(把会话登记进 byID),此后任意失败(ReadFrame 错、非 Connect 帧、Match 失败、PairBegin 失败)都直接 return err;而 wrapper 的错误分支只 conn.Close()、不调用 reg.Unregister(对比成功路径会 Unregister)。后果:byID 中遗留条目永不清理,持服务端密钥者可通过反复制造失败握手无界增长内存(DoS),且残留会话会被 Get/PeerAddrOf 命中。
建议:错误分支统一 Unregister,或把 reg.Put 移至 Match 成功之后、仅在确需登记时执行。
位置:server/registry.go:118-128(ReleasePair(name))、server/control.go:37-39(onClose 里 ReleasePair(session.Name))
ReleasePair 只按暴露名释放,不校验 pairID 归属;Name 都被设为暴露名,故任一同名会话的控制连接关闭都会清掉 busyNames[name]。在"同名接管"或"旧连接异常断开"场景下,可能出现:新连接端在本应仍被占用时 Match 成功;进而 startPipeLocked(kcp_relay.go:218-243)只检查两腿是否存在、不检查该腿是否已被别的 pair pipe,理论上可把同一 DataConn 交给两个 relayPipe 并发 Read(违反 kcp_session.go 中"Read 唯一消费方"约定),导致数据混流/串流。
建议:Session 增加 PairID 字段,ReleasePair(name, pairID) 仅在归属匹配时释放;startPipeLocked 增加"腿已被 pipe"互斥标记;配对占用改为由 pair 生命周期(endPair/clearPair)驱动。
WriteFrame 非原子位置:xtun/proto.go:70-89(WriteFrame 两次 Write)、server/control.go:113,154、server/kcp_relay.go:136-144
WriteFrame 先写 4 字节长度前缀、再写载荷,不是原子操作;而同一控制连接上存在多个并发写者:Relay.Serve goroutine 写 AddrNotice,handleConnect goroutine 写 PeerConnected、handleExpose goroutine 写 Kick。并发写会造成帧字节交错 → 对端 ReadFrame 解析错位、连接异常断开。此外 SetWriteDeadline(kcp_relay.go:140,142)是连接级状态,写入与清除会与其他写者相互覆盖。
建议:为 Session 增加写互斥(或每会话单写 goroutine + 队列);WriteFrame 改为一次 Write 整帧。
位置:xtun/crypto.go(Encrypter.Seal / Decrypter.OpenFrame)、server/control.go(pairID 由进程内自增 seq 生成)
counter 为 uint32,counter++ 无溢出检查,回绕后 (base||ctr) 重复 → ChaCha20-Poly1305 nonce 复用,机密性失效(理论风险,需 2³² 帧)。HKDF(pwd, serverKey, name) 与 nonce 基值 SHA256(pairId|dir)[:8] 均由确定性输入派生,而 pairId = "p"+seq,seq 随服务端进程重启归零。服务端重启后同一暴露名/密码重新配对会再次得到 pairId="p1",nonce 基值与密钥完全一致、计数器从 0 重来 → 跨会话 nonce 复用,同一明文前缀的密文可被区分,旧密文也可被重放。建议:pairID 或 nonce 基值混入服务端下发的随机 salt(每次会话随机);counter 接近上限时返回错误或强制重派生。
位置:server/control.go:30-34,46-86
握手与后续读循环均无 SetReadDeadline。任意未认证连接只要连上后静默保持,即永久占用一个 goroutine + FD(无需服务端密钥即可发起)。建议:握手阶段设 5–10s deadline,会话空闲设超时/心跳。
位置:app/role.go:130-175(startRemoteHealth)
startRemoteHealth 只 close 旧 stopHealth 而不 join;旧 goroutine 可能正阻塞在 probeDsh(≤1.2s)。其复检条件是 role==roleRemote && bindPort==bindPort——快速重连且绑定端口不变时两条件都成立,陈旧探测失败会 cancelConnect() 掉刚建立的新会话并推送"已断开"。
建议:复检加入健康会话 token/generation。
GetTunnel 回传前端(与设计声明矛盾)位置:app/tunnel.go:65(注释「key/pwd 凭据不落前端」)、app/app.go(GetTunnel 原样返回 Server.Key、Remote[].Pwd)
WKWebView 的 JS / devtools 可直接读到服务端密钥与私有密码。建议读取侧返回脱敏 DTO,凭据仅写入路径可见。
位置:frontend/src/App.tsx:338-346(saveTunnel 吞异常)、XtunSetupModal.tsx:25(valid 未校验 key)、App.tsx:406-432(handleRemoveRemote 闭包快照)
saveTunnel 的 catch 只 setNotify、不抛出/不返回结果;向导 onSave 里 await saveTunnel(...) 后仍执行 setXtunSetupOpen(false) 与 handleOpenRemoteAccess——保存失败(写盘失败 / Go 侧 服务端地址不能为空)时仍弹"远程访问已开启",配置与运行态脱节。valid 只校验 addr/name,服务端密钥 key 无必填校验(handleOpenRemoteAccess 的前置门同样只看 addr/name);空 key 可保存并开启,最终停在 auth-failed,用户不知是配置缺失。handleRemoveRemote 基于闭包快照 tunnel + void saveTunnel(...):同一渲染帧连续删除两项时第二次基于旧列表计算,删除被回滚丢失;且删除包含凭据的 profile 无二次确认。建议:saveTunnel 返回 boolean 或 rethrow;向导与前置门统一加 key 必填;删除改函数式更新并复用 ConfirmDialog。
| 优先级 | 问题 | 位置 | 影响 |
|---|---|---|---|
| P0 | 客户端连接匹配失败导致 RPC 永久挂起 | app/app.go:382、app/xtun_client.go | 可用性(误操作必现) |
| P0 | cancel 句柄被旧会话误清 → 会话不可取消/泄漏 | app/app.go:503-534 | 资源泄漏、状态卡死 |
| P1 | 服务端错误路径不 Unregister → 内存泄漏 DoS | server/control.go:24-43,124 | 安全性/稳定性 |
| P1 | 配对释放不校验归属 + 同腿并发 pipe | server/registry.go:118-128、kcp_relay.go:218 | 数据串流风险 |
| P1 | 控制连接多写者无锁、WriteFrame 非原子 | xtun/proto.go:70、 |
总体评价:本次 xtun 替换属于高强度自研,删除了 FRP 子系统消除了 1 条 Critical 命令注入与约 10 条复杂度问题,方向正确。新增代码的协议分层(proto/crypto/mux/kcp/punch)与鉴权设计(恒定时间比较、HKDF 派生、AEAD 密文搬运)是亮点;但客户端连接生命周期与服务端会话/配对生命周期两处的实现细节尚未收敛,且控制面并发写缺乏互斥。建议按上表 P0→P1→P2 顺序推进一轮修复后再进入用户端到端验收。
说明:本报告"修复/新增"条目严格取自私
codescan_issues_diff真实数据;第 4 节深挖结论均基于 head 源码逐行核对(含行号)。
server/cmd/xtun-server/main.go | 独立二进制入口(flag 解析) |
server/Dockerfile、server/docker-compose.yml | 多阶段构建镜像 + Compose 编排 |
app/xtun_client.go | 壳子内控制会话(注册/匹配/自动重连) |
app/xtun_data.go | 数据面接线(打洞 3s 窗口 → 中继兜底 + mux + AEAD + 本地端口转发) |
frontend/src/components/XtunSetupModal.tsx | 4 参数配置向导(服务器 3 项 + 暴露身份 2 项) |
docs/superpowers/specs/2026-08-18-xtun穿透服务设计.md、docs/superpowers/plans/...、docs/手册/xtun服务端部署.md | 设计/计划/部署文档 |
scripts/release-notes-v0.2.2.md | 发布说明 |
app/xtun_client.go:172xtun/proto.go:116server/control.go:113/154| 连接错乱断连 |
| P2 | 加密 nonce 计数器回绕 + 重启后 nonce 复用 | xtun/crypto.go、server/control.go | 机密性 |
| P2 | 服务端无握手/空闲超时(慢速 DoS) | server/control.go | 安全性 |
| P2 | 健康轮询旧 goroutine 误杀新连接 | app/role.go:130 | 断连误报 |
| P2 | 凭据经 GetTunnel 回传前端 | app/tunnel.go、app/app.go | 凭据暴露 |
| P2 | 前端保存失败仍开启 / key 校验缺失 / 删除竞态 | frontend/src/App.tsx、XtunSetupModal.tsx | 配置与运行态脱节 |
| P3 | 死代码 sid/uc 参数、缺少 default 分支、重复代码块、行过长 | 多处 | 可维护性 |
| 遗留 | app/service.go:277 命令注入风险(Critical,本增量未触及) | app/service.go:277 | 安全 |