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 }