fix(xray): read an outbound protocol id the way the core does (#6521)

* fix(xray): read an outbound protocol id the way the core does

xray-core lowercases a protocol id before it looks up the handler
(infra/conf/loader.go: `id = strings.ToLower(id)`), so a template that
spells the direct outbound "Freedom" runs as freedom. The three config
rewriters compared the id case-sensitively, so such an outbound was
skipped while the seed was recorded as applied: the refused
sockopt.addressPortStrategy stayed and xray-core refused to start.

* fix(xray): match an outbound protocol id case-insensitively

xray-core lowercases a protocol id before looking up its handler, so a
template that spells the direct outbound "Freedom" runs as freedom while
this rewriter skipped it and left the deprecated placement in place.

* fix(xray): re-run the freedom finalRules rewrite where its seeder was gated

Both finalRules seeders recorded their rows before the predicate could see
an outbound spelled "Freedom", and a recorded row is never re-run, so the
corrected predicates alone left the #6037 private-egress hardening
unapplied on every panel that had already run them. The new one-shot
seeder replays both rewrites, and only when the config actually carries a
differently spelled freedom outbound, so stock lowercase configs stay
byte-identical.
This commit is contained in:
BlindMaster24
2026-09-14 19:52:27 +03:00
committed by GitHub
parent 84c5aef4a1
commit 4d6db1c961
3 changed files with 336 additions and 5 deletions
+82 -5
View File
@@ -1254,7 +1254,7 @@ func runSeeders(isUsersEmpty bool) error {
}
if empty && isUsersEmpty {
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix2", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock", "InboundRealityFinalmaskTcpStrip", "ApiTokensHash", "LegacyProxySettingsCleanup", "OutboundRemovedKeysFix", "FreedomDomainStrategyFix", "DNSOutboundLegacyKeysFix", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "ResetIpLimitNoFail2ban"}
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix2", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock", "UppercaseFreedomFinalRulesFix", "InboundRealityFinalmaskTcpStrip", "ApiTokensHash", "LegacyProxySettingsCleanup", "OutboundRemovedKeysFix", "FreedomDomainStrategyFix", "DNSOutboundLegacyKeysFix", "WireguardPeersToClients", "MtprotoSecretsToClients", "NodeInboundsAdopted", "ResetIpLimitNoFail2ban"}
for _, name := range seeders {
if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
return err
@@ -1347,6 +1347,12 @@ func runSeeders(isUsersEmpty bool) error {
}
}
if !slices.Contains(seedersHistory, "UppercaseFreedomFinalRulesFix") {
if err := fixUppercaseFreedomFinalRules(); err != nil {
return err
}
}
if !slices.Contains(seedersHistory, "InboundRealityFinalmaskTcpStrip") {
if err := stripRealityFinalmaskTcp(); err != nil {
return err
@@ -1625,7 +1631,7 @@ func rewriteRemovedOutboundKeys(raw string) (string, bool, error) {
delete(obj, "proxySettings")
changed = true
}
if proto, _ := obj["protocol"].(string); proto == "freedom" {
if proto, _ := obj["protocol"].(string); strings.EqualFold(proto, "freedom") {
if sockopt := outboundSockopt(obj, false); sockopt != nil {
if _, present := sockopt["addressPortStrategy"]; present {
delete(sockopt, "addressPortStrategy")
@@ -1711,7 +1717,7 @@ func rewriteFreedomDomainStrategy(raw string) (string, bool, error) {
if !ok {
continue
}
if proto, _ := obj["protocol"].(string); proto != "freedom" {
if proto, _ := obj["protocol"].(string); !strings.EqualFold(proto, "freedom") {
continue
}
settings, hasSettings := obj["settings"].(map[string]any)
@@ -2144,7 +2150,7 @@ func rewriteFreedomFinalRules(raw string) (string, bool, error) {
if !ok {
continue
}
if proto, _ := obj["protocol"].(string); proto != "freedom" {
if proto, _ := obj["protocol"].(string); !strings.EqualFold(proto, "freedom") {
continue
}
settings, ok := obj["settings"].(map[string]any)
@@ -2247,7 +2253,7 @@ func rewriteFreedomFinalRulesPrivateEgress(raw string) (string, bool, error) {
if !ok {
continue
}
if proto, _ := obj["protocol"].(string); proto != "freedom" {
if proto, _ := obj["protocol"].(string); !strings.EqualFold(proto, "freedom") {
continue
}
settings, ok := obj["settings"].(map[string]any)
@@ -2276,6 +2282,77 @@ func rewriteFreedomFinalRulesPrivateEgress(raw string) (string, bool, error) {
return string(out), true, nil
}
func fixUppercaseFreedomFinalRules() error {
var setting model.Setting
err := db.Model(model.Setting{}).Where("key = ?", "xrayTemplateConfig").First(&setting).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return db.Create(&model.HistoryOfSeeders{SeederName: "UppercaseFreedomFinalRulesFix"}).Error
}
if err != nil {
return err
}
updated, changed, rErr := rewriteUppercaseFreedomFinalRules(setting.Value)
if rErr != nil {
log.Printf("UppercaseFreedomFinalRulesFix: skip (invalid xrayTemplateConfig json): %v", rErr)
return db.Create(&model.HistoryOfSeeders{SeederName: "UppercaseFreedomFinalRulesFix"}).Error
}
return db.Transaction(func(tx *gorm.DB) error {
if changed {
if err := tx.Model(&model.Setting{}).Where("key = ?", "xrayTemplateConfig").
Update("value", updated).Error; err != nil {
return err
}
}
return tx.Create(&model.HistoryOfSeeders{SeederName: "UppercaseFreedomFinalRulesFix"}).Error
})
}
// Re-runs both finalRules rewrites, because the rows of the two seeders above it
// already exist on any panel that walked past a differently spelled outbound.
func rewriteUppercaseFreedomFinalRules(raw string) (string, bool, error) {
if !hasNonLowercaseFreedomOutbound(raw) {
return raw, false, nil
}
reversed, reversedChanged, err := rewriteFreedomFinalRules(raw)
if err != nil {
return raw, false, err
}
hardened, hardenedChanged, err := rewriteFreedomFinalRulesPrivateEgress(reversed)
if err != nil {
return raw, false, err
}
if !reversedChanged && !hardenedChanged {
return raw, false, nil
}
return hardened, true, nil
}
func hasNonLowercaseFreedomOutbound(raw string) bool {
if strings.TrimSpace(raw) == "" {
return false
}
var cfg map[string]any
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
return false
}
outbounds, ok := cfg["outbounds"].([]any)
if !ok {
return false
}
for _, ob := range outbounds {
obj, ok := ob.(map[string]any)
if !ok {
continue
}
if proto, _ := obj["protocol"].(string); strings.EqualFold(proto, "freedom") && proto != "freedom" {
return true
}
}
return false
}
func stripRealityFinalmaskTcp() error {
var inbounds []model.Inbound
if err := db.Find(&inbounds).Error; err != nil {