Commit Graph

6 Commits

Author SHA1 Message Date
wangjia aaa3384e3b feat(agent): route 加 resolve action,堵住 ip_cidr 目的地的域名形式绕过
sing-box route 的 ip_cidr 匹配的是已解析的连接目的地地址;客户端若发的是域名
形式(如 nas.51yanmei.com:5001)而不先解析,ip_cidr + port 的拒绝规则不命中,
请求会绕过 ACL 直接落到 final:direct——DSM/RDP/SSH 因而对任何持有有效 dp_uuid
的 pangolin 用户可达,不受白名单约束。攻击者只需让客户端连接目的地时使用域名
而非 IP 即可绕过。

用户已拍板方案:在 sniff 之后紧跟加一条 {"action":"resolve"},让节点自己解析
目的地,使域名形式收敛到 ip_cidr 规则上(已用本机 sing-box 1.13.13 确认接受
[{"action":"sniff"},{"action":"resolve"},...] 这个形状,TestRenderedConfig_
PassesSingBoxCheck 也覆盖了这条产物)。route 规则顺序变为:
sniff → resolve → ACL 放行 → ACL 拒绝 → WARP。resolve 与 sniff 同条件——只在
产出 route 块时才出现,唯一且紧跟 sniff。

代价(用户已知情接受):节点从此会对连接目的地做 DNS 解析,是对"节点只见不透明
dp_uuid、不知道目的地"这条隐私姿态的一次主动后退,详见设计文档 §12。

TDD:更新 TestBuildRoute_Matrix 的四态断言(规则数 2/3/4 → 3/4/5,新增 resolve
计数/位置断言),新增 TestBuildRoute_ResolvePositionedRightAfterSniff(RED:此前
resolve 不存在)与 TestBuildRoute_NeverConfigured_NoResolve(未配置态不受影响)。
确认既有 TestRender_Warp_InjectsEndpointAndRoute(只断言首条 sniff、末条 warp,
不断言精确条数)未受影响,原样绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:44:58 +08:00
wangjia 4155fc8542 refactor(agent): route 块改由 buildRoute 统一产出,合并 ACL 与 WARP
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 01:52:39 +08:00
wangjia eb7c3c1062 feat(agent): WARP 域名分流 —— 命中域名走 Cloudflare 干净出口(#29)
节点 sing-box 渲染新增可选 WARP 分流:节点本地 warp.json(默认
<StateDir>/warp.json)配 WARP 凭证 + 域名清单 → 渲染时注入一个 userspace
WireGuard(WARP)endpoint + route(sniff 取 SNI/Host → domain_suffix 命中走
warp,其余 final=direct)。sing-box 1.11+ endpoints 语法,system=false 用户态
不依赖内核 wg 模块。

- 运营改域名只需编辑 warp.json + 重启 agent(sing-box 无热重载),即「配置的方式」。
- warp.json 不存在/enabled=false/域名空/凭证缺/坏 JSON → 一律按未启用,配置与旧
  节点逐字节一致,坏配置绝不产出无法启动的 sing-box config(渲染读失败仅记日志)。
- WARP 凭证节点私有(wgcf 注册免费匿名账号),不入 git、不经控制面。
- 测试:注入 endpoint+route/无配置无 route/禁用或残缺不注入/坏 JSON 优雅退化。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:40:00 +08:00
wangjia 64a1a64c3f feat(server): 按用户精确统计流量 — agent 改读 v2ray_api StatsService
节点 sing-box 编入 with_v2ray_api 后,每个 dp_uuid 有独立计数器
user>>>{dp_uuid}>>>traffic>>>uplink|downlink。agent 用 QueryStats(reset=true)
取窗口 delta,按用户精确上报(替代旧 clash 节点总量分摊:单用户准、多用户近似)。

- render.go: experimental.v2ray_api(loopback :19091)+ stats.users 列全部 dp_uuid
- v2rayapi/: vendor sing-box stats.pb.go(消息)+ 手写 client(用 v2ray 规范
  ServiceName 路径,绕开生成代码的 experimental.v2rayapi.* 误名)
- usage_v2ray.go: V2RayUsageSource,uplink/downlink 天然用户视角(无需 swap)
- agent.UseV2RayUsage() 取代 UseClashUsage();clash_api 保留作本地调试

节点需部署带 with_v2ray_api 的 sing-box(已编好 linux/amd64 二进制)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:18:33 +08:00
wangjia a6b35268ab feat(agent): #7 真实流量采集 — clash_api 节点总量 → usage_daily
sing-box 该构建未编入 v2ray_api、clash /connections 也不暴露用户,故无法做
准确 per-user 计费。改取节点累计总量(clash downloadTotal/uploadTotal)做
窗口增量,按当前 dp_uuid 均摊上报(单用户=准确,多用户近似,已注释标注)。

- render.go:节点 sing-box 配置加 experimental.clash_api(loopback+secret)。
- ClashUsageSource:轮询 /connections 总量,delta + 重启回绕保护;clash 的
  up/down 是代理视角,对调为用户视角(下载→bytes_down)。
- SingBox.DpUUIDs() 供归属;Agent.UseClashUsage() 在 cmd/agent 接上。

已节点 live 验证:usage_daily 实时入库,下载增量正确落 bytes_down。
局限:多 dp_uuid 时均摊(准确计费需重编 sing-box 带 with_v2ray_api)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 06:01:24 +08:00
wangjia f03d2dc8a6 feat(agent): node agent — enroll/mTLS, heartbeat, command stream, sing-box 用户表 (tsk__R8M4jEw43JR)
与控制面同仓同 go.mod,新增节点 agent 实现:

- proto/agent/v1/agent.proto + internal/pb/agentv1:冻结的控制面↔agent gRPC 契约
  (Enroll/Register/Heartbeat/Subscribe/Ack/ReportUsage)。仓库尚无 protoc 流水线,
  暂以手写 Go 类型 + JSON gRPC codec 实现,与 proto 1:1 对应,待 protoc 接入即可替换。
- internal/agentd:
  - enroll.go:首启生成 EC 密钥+CSR,持 bootstrap token 调 Enroll 换 90d 节点证书
    (CN=node_uuid),落 /etc/pangolin-agent/,此后 mTLS。
  - conn.go(agent.go)+creds.go:mTLS 主动拨号 + 指数退避重连;重连携带 last_command_id;
    Register 取 ConfigSnapshot 全量配置覆盖本地。
  - heartbeat.go:30s 上报 peer/带宽/CPU + config_version;need_full_resync→全量同步。
  - command.go:消费 Subscribe,Upsert/Revoke/Rotate/ApplyConfig/Lifecycle 幂等处理后
    Ack(at-least-once,按 command_id 去重)。
  - singbox.go+render.go:内存用户表 + 落盘 state.json(仅 dp_uuid+expires_at);任何变更
    渲染完整 sing-box 配置(REALITY users[uuid,flow] + Hy2 users[派生口令])→ 500ms 去抖
    合并 → systemd 重启。
  - ttl.go:凭证 TTL 定时移除并上报。
  - usage.go:按 dp_uuid 聚合上报,绝无 user_id/email/目的地址。
  - derive.go:Hy2 口令 = HMAC-SHA256(key, dp_uuid),与控制面同源派生。
- cmd/agent:入口(flag/env 配置)。
- infra/cloud-init/{node.yaml.tmpl,install-node.sh,README.md}:一段式安装,下载锁定版本
  二进制并校验 SHA-256,systemd 拉管,首启即 Enroll/Register。shellcheck -S warning 通过。

测试(bufconn mock 控制面,无需 docker):Enroll→Register→Heartbeat 全流转;Upsert/Revoke
渲染正确;Rotate 宽限期新旧并存到点移除;TTL 自动移除并上报;断流重连 last_command_id
续发不丢不重;need_full_resync 触发重注册;state.json 恢复;去抖合并;扫描确认无身份字段。
go test -race ./internal/agentd/... ./internal/pb/... 通过;go vet ./... 通过。

落实 doc/04 §2 节点无状态化与 doc/06 §3 数据面红线(节点仅见 dp_uuid)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:26:58 +08:00