package singbox import ( "encoding/json" "fmt" "net" "strconv" ) // ---------------------------------------------------------------- VLESS func (c *conv) convertVLESS() error { frag := c.base("vless") users := make([]any, 0, len(c.in.Clients)) for _, cl := range c.in.Clients { u := map[string]any{"name": cl.Email, "uuid": cl.UUID} if f := c.effectiveFlow(cl); f != "" { u["flow"] = f } if len(cl.Reverse) > 0 { c.unmapped("vless client reverse", map[string]any{ "email": cl.Email, "reverse": jsonRawString(cl.Reverse), }, "sing-box vless 无 reverse 反向代理对应(N4),整客户其余字段照常直映。", "Q1", true) } users = append(users, u) } if len(users) > 0 { frag["users"] = users } if dec := gStr(c.set, "decryption"); dec != "" && dec != "none" { c.unmapped("settings.decryption(vlessenc / ML-KEM 加密串)", dec, "sing-box vless 无对应加密层;带 vlessenc 的入站迁过去即明文 vless,行为与安全性变化,待 Q1。", "Q1", true) } if enc := gStr(c.set, "encryption"); enc != "" && enc != "none" { c.panel("vlessEncryption", enc) } if !c.applyStream(frag, "vless") { c.res.Fragment = nil return nil } c.registerFallbacks() c.res.Fragment = frag return nil } // ---------------------------------------------------------------- VMess func (c *conv) convertVMess() error { frag := c.base("vmess") users := make([]any, 0, len(c.in.Clients)) for _, cl := range c.in.Clients { u := map[string]any{"name": cl.Email, "uuid": cl.UUID} if cl.Security != "" { u["security"] = cl.Security } u["alter_id"] = cl.AlterID users = append(users, u) } if len(users) > 0 { frag["users"] = users } if !c.applyStream(frag, "vmess") { c.res.Fragment = nil return nil } c.res.Fragment = frag return nil } // --------------------------------------------------------------- Trojan func (c *conv) convertTrojan() error { frag := c.base("trojan") users := make([]any, 0, len(c.in.Clients)) for _, cl := range c.in.Clients { u := map[string]any{"name": cl.Email, "password": cl.Password} if f := c.effectiveFlow(cl); f != "" { // sing-box trojan users have no flow field; generation writes // it into the xray user today, so this is a real (if small) // behavioural item owned by Q1/B3. c.unmapped("trojan client flow", f, "sing-box trojan 用户无 flow 字段;flow 客户端(vision)实际协商行为待 Q1/B3 定。", "Q1/B3", true) } users = append(users, u) } if len(users) > 0 { frag["users"] = users } if !c.applyStream(frag, "trojan") { c.res.Fragment = nil return nil } c.registerFallbacks() c.res.Fragment = frag return nil } // ---------------------------------------------------------- Shadowsocks func (c *conv) convertShadowsocks() error { method := gStr(c.set, "method") if removedShadowsocksCipher(method) { c.unmapped("shadowsocks method="+method, map[string]any{"method": method}, "sing-box 已移除 cfb/ctr 类 cipher,无承载;入站整体保留待 Q1。", "Q1", true) c.res.Fragment = nil return nil } frag := c.base("shadowsocks") frag["method"] = method if pw := gStr(c.set, "password"); pw != "" { frag["password"] = pw } users := make([]any, 0, len(c.in.Clients)) for _, cl := range c.in.Clients { users = append(users, map[string]any{"name": cl.Email, "password": cl.Password}) } if len(users) > 0 { frag["users"] = users } // "tcp,udp" is sing-box's default coverage and is omitted; a // restricted network value is carried over. switch n := gStr(c.set, "network"); n { case "", "tcp,udp": case "tcp", "udp": frag["network"] = n default: c.note("shadowsocks settings.network=%q 非标准值,未落 network 字段,原文在旧列保留", n) } if has(c.set, "ivCheck") { c.note("shadowsocks settings.ivCheck=%v:sing-box 无对应校验开关,登记备查(Q19 口径,待 Q1 汇总确认)", gBool(c.set, "ivCheck")) } if !c.applyStream(frag, "shadowsocks") { c.res.Fragment = nil return nil } c.res.Fragment = frag return nil } // ------------------------------------------------------- mixed / socks func (c *conv) socksUsers() []any { users := []any{} seen := map[string]bool{} for _, a := range gArr(c.set, "accounts") { am, _ := a.(map[string]any) if am == nil { continue } user := gStr(am, "user") if user == "" { continue } seen[user] = true users = append(users, map[string]any{"username": user, "password": gStr(am, "pass")}) } for _, cl := range c.in.Clients { if cl.Email == "" || seen[cl.Email] { continue } seen[cl.Email] = true users = append(users, map[string]any{"username": cl.Email, "password": cl.Password}) } return users } func (c *conv) convertMixedSocks() error { typ := "mixed" if c.in.Protocol == "socks" { typ = "socks" } frag := c.base(typ) if gStr(c.set, "auth") != "noauth" { if users := c.socksUsers(); len(users) > 0 { frag["users"] = users } } if ip := gStr(c.set, "ip"); ip != "" && ip != "127.0.0.1" { c.unmapped("settings.ip(UDP 回源地址)", ip, "sing-box mixed/socks 无回源地址对应;默认值 127.0.0.1 无损,非默认登记待 Q1。", "Q1", true) } if has(c.set, "udp") && !gBool(c.set, "udp") { c.note("settings.udp=false:sing-box mixed/socks 的 UDP 关联恒开,行为差异登记备查") } if !c.applyStream(frag, c.in.Protocol) { c.res.Fragment = nil return nil } c.res.Fragment = frag return nil } // ----------------------------------------------------------------- HTTP func (c *conv) convertHTTP() error { frag := c.base("http") users := []any{} for _, a := range gArr(c.set, "accounts") { am, _ := a.(map[string]any) if am == nil { continue } if user := gStr(am, "user"); user != "" { users = append(users, map[string]any{"username": user, "password": gStr(am, "pass")}) } } if len(users) > 0 { frag["users"] = users } if gBool(c.set, "allowTransparent") { c.unmapped("settings.allowTransparent", true, "sing-box http 入站无透明代理开关对应,待 Q1。", "Q1", true) } if !c.applyStream(frag, "http") { c.res.Fragment = nil return nil } c.res.Fragment = frag return nil } // ------------------------------------------------------------- Hysteria func (c *conv) convertHysteria() error { version := gInt(c.set, "version") if hs := gObj(c.strm, "hysteriaSettings"); hs != nil { if sv := gInt(hs, "version"); sv != 0 && version != 0 && sv != version { c.note("hysteria 版本真源冲突:settings.version=%d、streamSettings.hysteriaSettings.version=%d,按 settings 为准", version, sv) } } if version == 0 { version = 2 } typ := "hysteria2" if version == 1 { typ = "hysteria" } frag := c.base(typ) users := make([]any, 0, len(c.in.Clients)) for _, cl := range c.in.Clients { if version == 1 { users = append(users, map[string]any{"name": cl.Email, "auth": cl.Auth}) } else { users = append(users, map[string]any{"name": cl.Email, "password": cl.Auth}) } } if len(users) > 0 { frag["users"] = users } if version == 2 { if obfs := c.salamanderObfs(); obfs != nil { frag["obfs"] = obfs } if mz := gObj(c.set, "masquerade"); mz != nil { if m := convertMasquerade(mz); m != nil { frag["masquerade"] = m } } } if has(c.set, "udpIdleTimeout") && gInt(c.set, "udpIdleTimeout") != 0 { c.note("hysteria settings.udpIdleTimeout=%d:sing-box 对应字段语义未对齐,小损备查(待 Q1 汇总)", gInt(c.set, "udpIdleTimeout")) } if !c.applyStream(frag, "hysteria") { c.res.Fragment = nil return nil } c.res.Fragment = frag return nil } func convertMasquerade(mz map[string]any) map[string]any { switch gStr(mz, "type") { case "file": out := map[string]any{"type": "file"} if d := gStr(mz, "dir"); d != "" { out["directory"] = d } return out case "proxy": out := map[string]any{"type": "proxy"} if u := gStr(mz, "url"); u != "" { out["url"] = u } if gBool(mz, "rewriteHost") { out["rewrite_host"] = true } return out case "string": out := map[string]any{"type": "string"} if sc := gInt(mz, "statusCode"); sc != 0 { out["status_code"] = sc } if content := gStr(mz, "content"); content != "" { out["content"] = content } return out } return nil } // ------------------------------------------------------------ WireGuard func (c *conv) convertWireguard() error { frag := map[string]any{ "type": "wireguard", "tag": fmt.Sprintf("inbound-%d", c.in.Port), } c.res.Endpoint = true if c.in.Port != 0 { frag["listen_port"] = c.in.Port } if addr := deriveWireguardAddress(c.set); addr != "" { frag["address"] = []string{addr} } else { c.note("wireguard settings 缺 subnetIp/subnetCidr,服务端 address 未推导,原文在旧列保留") } if sk := gStr(c.set, "secretKey"); sk != "" { frag["private_key"] = sk } if mtu := gInt(c.set, "mtu"); mtu > 0 { frag["mtu"] = mtu } peers := make([]any, 0, len(c.in.Clients)) for _, cl := range c.in.Clients { p := map[string]any{"public_key": cl.PublicKey} if len(cl.AllowedIPs) > 0 { ips := make([]any, len(cl.AllowedIPs)) for i, s := range cl.AllowedIPs { ips[i] = s } p["allowed_ips"] = ips } if cl.PreSharedKey != "" { p["pre_shared_key"] = cl.PreSharedKey } if cl.KeepAlive > 0 { p["persistent_keepalive_interval"] = cl.KeepAlive } peers = append(peers, p) } if len(peers) > 0 { frag["peers"] = peers } if has(c.set, "noKernelTun") { c.note("wireguard settings.noKernelTun=%v:sing-box wireguard endpoint 的系统/内核模式差异待 B3 真机验(N7)", gBool(c.set, "noKernelTun")) } if !c.applyStream(frag, "wireguard") { c.res.Fragment = nil return nil } c.res.Fragment = frag return nil } // deriveWireguardAddress mirrors the panel's server-address derivation: // the first host address of the configured subnet (10.0.0.0/24 -> // 10.0.0.1/24). func deriveWireguardAddress(settings map[string]any) string { subnet := gStr(settings, "subnetIp") cidr := gInt(settings, "subnetCidr") if subnet == "" || cidr == 0 { return "" } ip := net.ParseIP(subnet) if ip == nil { return "" } ip4 := ip.To4() if ip4 == nil { return "" } mask := net.CIDRMask(cidr, 32) if mask == nil { return "" } network := ip4.Mask(mask) host := make(net.IP, 4) copy(host, network) host[3]++ return host.String() + "/" + strconv.Itoa(cidr) } // ------------------------------------------------------------------ TUN func (c *conv) convertTun() error { frag := map[string]any{ "type": "tun", "tag": fmt.Sprintf("inbound-%d", c.in.Port), } if name := gStr(c.set, "name"); name != "" { frag["interface_name"] = name } if gw := c.set["gateway"]; gw != nil { switch v := gw.(type) { case []any: if len(v) > 0 { frag["address"] = v } case string: if v != "" { frag["address"] = []string{v} } } } if mtu := gInt(c.set, "mtu"); mtu > 0 { frag["mtu"] = mtu } if routes := gArr(c.set, "autoSystemRoutingTable"); len(routes) > 0 { frag["auto_route"] = true frag["route_address"] = routes } frag["stack"] = "system" if dns := gArr(c.set, "dns"); len(dns) > 0 { c.unmapped("settings.dns", dns, "tun 入站本体无 dns 字段,由 sing-box 独立 dns 配置承载——属基础配置面,B2 定落点,此处登记防丢。", "B2", false) } if has(c.set, "userLevel") { lvl := gInt(c.set, "userLevel") c.unmapped("settings.userLevel", lvl, "xray policy level 概念,sing-box 无对应;0 为默认无损,非 0 待 Q1。", "Q1", lvl != 0) } if aoi := gStr(c.set, "autoOutboundsInterface"); aoi != "" && aoi != "auto" { c.note("tun settings.autoOutboundsInterface=%q 非 auto,sing-box tun 无直映字段,备查", aoi) } // TUN takes no stream/sniff handling in the current generation chain. c.res.Fragment = frag return nil } // --------------------------------------------------------------- Tunnel func (c *conv) convertTunnel() { c.unmapped("tunnel(dokodemo-door) settings", c.set, "tun/tunnel 在 sing-box 侧的落点(tun 入站 + 转发模块)跨 B2/B3,本批整体保留待 Q1。", "Q1", true) c.res.Fragment = nil } // ------------------------------------------- external-runtime protocols func (c *conv) convertExternal() { // MTProto and AmneziaWG run on external runtimes in the current // architecture; the panel keeps their full settings verbatim. c.panel("passthrough", c.set) c.note("协议 %s 由外部运行时承载(现行架构),settings 原样保留在面板侧,本批不产核心片段", c.in.Protocol) c.res.Fragment = nil } // jsonRawString unwraps a raw JSON value for embedding in another object. func jsonRawString(raw []byte) any { var v any if err := json.Unmarshal(raw, &v); err != nil { return string(raw) } return v }