package singbox import ( "encoding/json" "net" "strconv" ) // ---------------------------------------------------------------- VLESS func (c *conv) convertVLESS() error { frag := c.base("vless") for _, cl := range c.in.Clients { 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) } } if users := ProjectVLESSUsers(c.in.Clients, c.in.DisableFlow); 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) } // B2 amendment: record settings.encryption whenever the key exists // (any string value, including "none" and ""). The legacy link // generator emits the encryption= parameter whenever the key is // present, so byte-exact link regeneration from the new model // needs the presence bit, not just the non-default values. if enc, ok := c.set["encryption"].(string); ok { 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") if users := ProjectVMessUsers(c.in.Clients); 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") for _, cl := range c.in.Clients { 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) } } if users := ProjectTrojanUsers(c.in.Clients); 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) // B2 amendment: the fragment is discarded, but the share link // for such an inbound is still generated today (the client // password comes from the identity layer, the server password // only exists here) — preserve it panel-side so link // regeneration from the new model stays byte-exact. if pw := gStr(c.set, "password"); pw != "" { c.panel("ssPassword", pw) } c.res.Fragment = nil return nil } frag := c.base("shadowsocks") frag["method"] = method if pw := gStr(c.set, "password"); pw != "" { frag["password"] = pw } if users := ProjectShadowsocksUsers(c.in.Clients); 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 // socksAccountUsers projects the fixed settings.accounts entries — // the part of a mixed/socks user list that does NOT come from the // identity layer. func (c *conv) socksAccountUsers() []any { users := []any{} for _, a := range gArr(c.set, "accounts") { am, _ := a.(map[string]any) if am == nil { continue } user := gStr(am, "user") if user == "" { continue } users = append(users, map[string]any{"username": user, "password": gStr(am, "pass")}) } return users } func (c *conv) socksUsers() []any { users := []any{} seen := map[string]bool{} for _, u := range c.socksAccountUsers() { um, _ := u.(map[string]any) seen[gStr(um, "username")] = true users = append(users, u) } 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) // B2 amendment: the auth mode is preserved panel-side. The stored // fragment alone cannot distinguish "noauth with clients attached" // from "password auth with currently zero users", and generation // must not start demanding credentials on a noauth inbound (nor // drop auth on a password inbound whose users are all disabled). if a := gStr(c.set, "auth"); a != "" { c.panel("socksAuth", a) } // B2 amendment: the fixed accounts are recorded panel-side as // their own list. Inside the fragment's merged users list they // are indistinguishable from identity clients, so generation // could not otherwise tell a fixed account apart from a client // that has since been detached (and would leak the detached // client's stored entry forever). if accounts := c.socksAccountUsers(); len(accounts) > 0 { c.panel("socksAccounts", accounts) } 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) if users := ProjectHysteriaUsers(c.in.Clients, version); 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": c.fragmentTag(), } 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 } if peers := ProjectWireguardPeers(c.in.Clients); 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")) } // B2 amendment: settings.dns (a plain string) is read only by the // share-link generator (dns= parameter); it has no endpoint-side // counterpart, so it is preserved panel-side for link regeneration. if dns, ok := c.set["dns"].(string); ok && dns != "" { c.panel("wireguardDns", dns) } 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": c.fragmentTag(), } 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 }