Files
pangolin/server/internal/agentd/render.go
T
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

110 lines
3.3 KiB
Go

package agentd
import (
"encoding/json"
agentv1 "github.com/wangjia/pangolin/server/internal/pb/agentv1"
)
// renderSingboxConfig produces a complete sing-box SERVER config JSON for the node:
// a VLESS+REALITY inbound and a Hysteria2 inbound, each carrying one user per
// provisioned credential. REALITY users key on the dp_uuid; Hy2 users key on the
// derived password (DeriveHy2Password) — both from the same dp_uuid source.
//
// Only the opaque dp_uuid is ever written; no account identity touches the node.
// clash_api endpoint(loopback only)供 agent 读取节点累计流量做用量统计。
// v2ray_api 未编入该 sing-box 构建、clash /connections 又不暴露用户,故只能
// 取节点总量(downloadTotal/uploadTotal),由 agent 按当前 dp_uuid 归属/分摊。
const (
clashAPIAddr = "127.0.0.1:19090"
clashAPISecret = "pangolin-local-stats"
)
func renderSingboxConfig(creds []Cred, reality *agentv1.RealityInbound, hy2 *agentv1.Hy2Inbound, deriveKey string) ([]byte, error) {
cfg := map[string]any{
"log": map[string]any{"level": "warn", "timestamp": true},
"inbounds": buildInbounds(creds, reality, hy2, deriveKey),
"outbounds": []any{map[string]any{"type": "direct", "tag": "direct"}},
"experimental": map[string]any{
"clash_api": map[string]any{
"external_controller": clashAPIAddr,
"secret": clashAPISecret,
},
},
}
return json.MarshalIndent(cfg, "", " ")
}
func buildInbounds(creds []Cred, reality *agentv1.RealityInbound, hy2 *agentv1.Hy2Inbound, deriveKey string) []any {
inbounds := make([]any, 0, 2)
if reality != nil {
users := make([]any, 0, len(creds))
for _, c := range creds {
if c.Protocol == agentv1.ProtocolReality || c.Protocol == agentv1.ProtocolBoth {
flow := c.Flow
if flow == "" {
flow = DefaultFlow
}
users = append(users, map[string]any{
"name": c.DpUUID,
"uuid": c.DpUUID,
"flow": flow,
})
}
}
realityTLS := map[string]any{
"enabled": true,
"server_name": reality.ServerName,
"reality": map[string]any{
"enabled": true,
"private_key": reality.PrivateKey,
"short_id": []string{reality.ShortID},
"handshake": map[string]any{
"server": reality.HandshakeServer,
"server_port": reality.HandshakePort,
},
},
}
inbounds = append(inbounds, map[string]any{
"type": "vless",
"tag": "reality-in",
"listen": "::",
"listen_port": reality.ListenPort,
"users": users,
"tls": realityTLS,
})
}
if hy2 != nil {
users := make([]any, 0, len(creds))
for _, c := range creds {
if c.Protocol == agentv1.ProtocolHy2 || c.Protocol == agentv1.ProtocolBoth {
users = append(users, map[string]any{
"name": c.DpUUID,
"password": DeriveHy2Password(c.DpUUID, deriveKey),
})
}
}
hy2In := map[string]any{
"type": "hysteria2",
"tag": "hy2-in",
"listen": "::",
"listen_port": hy2.ListenPort,
"users": users,
"tls": map[string]any{
"enabled": true,
"alpn": []string{"h3"},
"certificate_path": hy2.CertPath,
"key_path": hy2.KeyPath,
},
}
if hy2.Masquerade != "" {
hy2In["masquerade"] = hy2.Masquerade
}
inbounds = append(inbounds, hy2In)
}
return inbounds
}