From ba8194141b581498e85e284139103f82b81c5cc7 Mon Sep 17 00:00:00 2001 From: Nikita Nemirovsky Date: Thu, 2 Apr 2026 11:25:17 +0800 Subject: [PATCH 1/2] fix(sub): support Reality params for fallback inbounds via externalProxy Extract Reality settings (pbk, sni, sid, fp, spx) from stream config unconditionally rather than only when security is "reality". This allows inbounds behind a VLESS fallback (security: "none") to generate correct subscription links when externalProxy overrides security to "reality" via forceTls. Also fix genAllLinks crash when remarkModel contains characters without a corresponding entry in the orders map (e.g., 'o' when externalProxy is empty). --- sub/subService.go | 110 +++++++++++++++++---------------- web/assets/js/model/inbound.js | 4 +- 2 files changed, 60 insertions(+), 54 deletions(-) diff --git a/sub/subService.go b/sub/subService.go index 818f193b..f718411a 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -435,34 +435,37 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { } } + // Extract Reality params unconditionally so they're available when + // externalProxy overrides security to "reality" via forceTls. + realitySetting, _ := stream["realitySettings"].(map[string]any) + realitySettings, _ := searchKey(realitySetting, "settings") + if realitySetting != nil { + if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { + sNames, _ := sniValue.([]any) + params["sni"] = sNames[random.Num(len(sNames))].(string) + } + if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { + params["pbk"], _ = pbkValue.(string) + } + if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { + shortIds, _ := sidValue.([]any) + params["sid"] = shortIds[random.Num(len(shortIds))].(string) + } + if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { + if fp, ok := fpValue.(string); ok && len(fp) > 0 { + params["fp"] = fp + } + } + if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { + if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { + params["pqv"] = pqv + } + } + params["spx"] = "/" + random.Seq(15) + } + if security == "reality" { params["security"] = "reality" - realitySetting, _ := stream["realitySettings"].(map[string]any) - realitySettings, _ := searchKey(realitySetting, "settings") - if realitySetting != nil { - if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { - sNames, _ := sniValue.([]any) - params["sni"] = sNames[random.Num(len(sNames))].(string) - } - if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { - params["pbk"], _ = pbkValue.(string) - } - if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { - shortIds, _ := sidValue.([]any) - params["sid"] = shortIds[random.Num(len(shortIds))].(string) - } - if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { - if fp, ok := fpValue.(string); ok && len(fp) > 0 { - params["fp"] = fp - } - } - if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { - if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { - params["pqv"] = pqv - } - } - params["spx"] = "/" + random.Seq(15) - } if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow @@ -627,34 +630,37 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string } } + // Extract Reality params unconditionally so they're available when + // externalProxy overrides security to "reality" via forceTls. + realitySetting, _ := stream["realitySettings"].(map[string]any) + realitySettings, _ := searchKey(realitySetting, "settings") + if realitySetting != nil { + if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { + sNames, _ := sniValue.([]any) + params["sni"] = sNames[random.Num(len(sNames))].(string) + } + if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { + params["pbk"], _ = pbkValue.(string) + } + if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { + shortIds, _ := sidValue.([]any) + params["sid"] = shortIds[random.Num(len(shortIds))].(string) + } + if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { + if fp, ok := fpValue.(string); ok && len(fp) > 0 { + params["fp"] = fp + } + } + if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { + if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { + params["pqv"] = pqv + } + } + params["spx"] = "/" + random.Seq(15) + } + if security == "reality" { params["security"] = "reality" - realitySetting, _ := stream["realitySettings"].(map[string]any) - realitySettings, _ := searchKey(realitySetting, "settings") - if realitySetting != nil { - if sniValue, ok := searchKey(realitySetting, "serverNames"); ok { - sNames, _ := sniValue.([]any) - params["sni"] = sNames[random.Num(len(sNames))].(string) - } - if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok { - params["pbk"], _ = pbkValue.(string) - } - if sidValue, ok := searchKey(realitySetting, "shortIds"); ok { - shortIds, _ := sidValue.([]any) - params["sid"] = shortIds[random.Num(len(shortIds))].(string) - } - if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok { - if fp, ok := fpValue.(string); ok && len(fp) > 0 { - params["fp"] = fp - } - } - if pqvValue, ok := searchKey(realitySettings, "mldsa65Verify"); ok { - if pqv, ok := pqvValue.(string); ok && len(pqv) > 0 { - params["pqv"] = pqv - } - } - params["spx"] = "/" + random.Seq(15) - } if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index b6059cf7..111ee1ae 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -1738,7 +1738,7 @@ class Inbound extends XrayCommonClass { 'o': '', }; if (ObjectUtil.isArrEmpty(this.stream.externalProxy)) { - let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar); + let r = orderChars.split('').map(char => orders[char]).filter(x => x && x.length > 0).join(separationChar); result.push({ remark: r, link: this.genLink(addr, port, 'same', r, client) @@ -1746,7 +1746,7 @@ class Inbound extends XrayCommonClass { } else { this.stream.externalProxy.forEach((ep) => { orders['o'] = ep.remark; - let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar); + let r = orderChars.split('').map(char => orders[char]).filter(x => x && x.length > 0).join(separationChar); result.push({ remark: r, link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client) From 0bc001b80c9986f05ddb457f1a332e75ab7e33b2 Mon Sep 17 00:00:00 2001 From: Nikita Nemirovsky Date: Thu, 2 Apr 2026 11:41:04 +0800 Subject: [PATCH 2/2] feat(sub): add sni override field to externalProxy Allow externalProxy entries to specify a "sni" field that overrides the randomly selected serverName in subscription links. This is needed when an inbound has extra serverNames for fallback routing (e.g., MTProto) that should not appear in client subscription links. --- sub/subService.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/sub/subService.go b/sub/subService.go index f718411a..31c3db1d 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -492,6 +492,14 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { } else { params["security"] = security } + + // Allow externalProxy to override SNI (useful when the + // inbound has extra serverNames for fallback routing that + // should not appear in subscription links). + if sni, ok := ep["sni"].(string); ok && len(sni) > 0 { + params["sni"] = sni + } + url, _ := url.Parse(link) q := url.Query() @@ -687,6 +695,14 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string } else { params["security"] = security } + + // Allow externalProxy to override SNI (useful when the + // inbound has extra serverNames for fallback routing that + // should not appear in subscription links). + if sni, ok := ep["sni"].(string); ok && len(sni) > 0 { + params["sni"] = sni + } + url, _ := url.Parse(link) q := url.Query() @@ -854,6 +870,14 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st } else { params["security"] = security } + + // Allow externalProxy to override SNI (useful when the + // inbound has extra serverNames for fallback routing that + // should not appear in subscription links). + if sni, ok := ep["sni"].(string); ok && len(sni) > 0 { + params["sni"] = sni + } + url, _ := url.Parse(link) q := url.Query()