From 6f6c7fc17a536a521baf07629b66d530cb1234e6 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 2 Jun 2026 16:07:26 +0200 Subject: [PATCH] fix(migrate): relax legacy freedom finalRules so reverse egress works on existing installs The d414e186 template change only helps fresh configs; installs already on xray-core >=26.5 keep their stored finalRules of [{allow, geoip:private}], which blocks WAN egress for reverse-proxy traffic (refs #4782, XTLS/Xray-core#6248). Add a FreedomFinalRulesReverseFix seeder that, on startup, rewrites any freedom outbound whose finalRules is exactly [{allow, ip:[geoip:private]}] to a no-condition [{allow}]. The match is exact, so custom rules (extra keys, other IPs, block actions, multiple rules) are left untouched. Runs once via history_of_seeders and is skipped on fresh installs. --- database/db.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/database/db.go b/database/db.go index fc5a9739..c2d79742 100644 --- a/database/db.go +++ b/database/db.go @@ -181,7 +181,7 @@ func runSeeders(isUsersEmpty bool) error { } if empty && isUsersEmpty { - seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix"} + seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix"} for _, name := range seeders { if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil { return err @@ -255,6 +255,12 @@ func runSeeders(isUsersEmpty bool) error { return err } } + + if !slices.Contains(seedersHistory, "FreedomFinalRulesReverseFix") { + if err := normalizeFreedomFinalRules(); err != nil { + return err + } + } return nil } @@ -401,6 +407,101 @@ func normalizeInboundClientsArray() error { }) } +func normalizeFreedomFinalRules() 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: "FreedomFinalRulesReverseFix"}).Error + } + if err != nil { + return err + } + + updated, changed, rErr := rewriteFreedomFinalRules(setting.Value) + if rErr != nil { + log.Printf("FreedomFinalRulesReverseFix: skip (invalid xrayTemplateConfig json): %v", rErr) + return db.Create(&model.HistoryOfSeeders{SeederName: "FreedomFinalRulesReverseFix"}).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: "FreedomFinalRulesReverseFix"}).Error + }) +} + +func rewriteFreedomFinalRules(raw string) (string, bool, error) { + if strings.TrimSpace(raw) == "" { + return raw, false, nil + } + var cfg map[string]any + if err := json.Unmarshal([]byte(raw), &cfg); err != nil { + return raw, false, err + } + outbounds, ok := cfg["outbounds"].([]any) + if !ok { + return raw, false, nil + } + changed := false + for _, ob := range outbounds { + obj, ok := ob.(map[string]any) + if !ok { + continue + } + if proto, _ := obj["protocol"].(string); proto != "freedom" { + continue + } + settings, ok := obj["settings"].(map[string]any) + if !ok { + continue + } + if !isLegacyPrivateOnlyFinalRules(settings["finalRules"]) { + continue + } + settings["finalRules"] = []any{map[string]any{"action": "allow"}} + changed = true + } + if !changed { + return raw, false, nil + } + out, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return raw, false, err + } + return string(out), true, nil +} + +func isLegacyPrivateOnlyFinalRules(v any) bool { + rules, ok := v.([]any) + if !ok || len(rules) != 1 { + return false + } + rule, ok := rules[0].(map[string]any) + if !ok { + return false + } + if action, _ := rule["action"].(string); action != "allow" { + return false + } + ips, ok := rule["ip"].([]any) + if !ok || len(ips) != 1 { + return false + } + if s, _ := ips[0].(string); s != "geoip:private" { + return false + } + for k := range rule { + if k != "action" && k != "ip" { + return false + } + } + return true +} + // normalizeClientJSONFields coerces loosely-typed numeric fields in a raw // settings.clients entry so json.Unmarshal into model.Client doesn't fail // when older rows wrote tgId/limitIp/totalGB/etc. as strings. Empty strings