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