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 }