64a1a64c3f
节点 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>
137 lines
3.9 KiB
Go
137 lines
3.9 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.
|
|
//
|
|
// 用量统计走 v2ray_api StatsService(loopback gRPC):节点 sing-box 编入
|
|
// with_v2ray_api,stats.users 列出全部 dp_uuid → 每个用户独立的
|
|
// user>>>{dp_uuid}>>>traffic>>>uplink|downlink 计数器,agent 按用户精确读取
|
|
// (替代旧的 clash 节点总量分摊)。clash_api 仍保留作本地调试。
|
|
const (
|
|
clashAPIAddr = "127.0.0.1:19090"
|
|
clashAPISecret = "pangolin-local-stats"
|
|
v2rayAPIAddr = "127.0.0.1:19091"
|
|
)
|
|
|
|
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,
|
|
},
|
|
"v2ray_api": map[string]any{
|
|
"listen": v2rayAPIAddr,
|
|
"stats": map[string]any{
|
|
"enabled": true,
|
|
"users": statsUsers(creds),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return json.MarshalIndent(cfg, "", " ")
|
|
}
|
|
|
|
// statsUsers 收集所有去重 dp_uuid,供 v2ray_api stats.users 按用户开启流量计数器。
|
|
func statsUsers(creds []Cred) []string {
|
|
seen := make(map[string]struct{}, len(creds))
|
|
users := make([]string, 0, len(creds))
|
|
for _, c := range creds {
|
|
if c.DpUUID == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[c.DpUUID]; ok {
|
|
continue
|
|
}
|
|
seen[c.DpUUID] = struct{}{}
|
|
users = append(users, c.DpUUID)
|
|
}
|
|
return users
|
|
}
|
|
|
|
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
|
|
}
|