mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
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.
This commit is contained in:
parent
8f5a7b9434
commit
6f6c7fc17a
1 changed files with 102 additions and 1 deletions
103
database/db.go
103
database/db.go
|
|
@ -181,7 +181,7 @@ func runSeeders(isUsersEmpty bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if empty && isUsersEmpty {
|
if empty && isUsersEmpty {
|
||||||
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix"}
|
seeders := []string{"UserPasswordHash", "ClientsTable", "InboundClientsArrayFix", "InboundClientTgIdFix", "InboundClientSubIdFix", "FreedomFinalRulesReverseFix"}
|
||||||
for _, name := range seeders {
|
for _, name := range seeders {
|
||||||
if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
|
if err := db.Create(&model.HistoryOfSeeders{SeederName: name}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -255,6 +255,12 @@ func runSeeders(isUsersEmpty bool) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !slices.Contains(seedersHistory, "FreedomFinalRulesReverseFix") {
|
||||||
|
if err := normalizeFreedomFinalRules(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
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
|
// normalizeClientJSONFields coerces loosely-typed numeric fields in a raw
|
||||||
// settings.clients entry so json.Unmarshal into model.Client doesn't fail
|
// settings.clients entry so json.Unmarshal into model.Client doesn't fail
|
||||||
// when older rows wrote tgId/limitIp/totalGB/etc. as strings. Empty strings
|
// when older rows wrote tgId/limitIp/totalGB/etc. as strings. Empty strings
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue