diff --git a/internal/database/db.go b/internal/database/db.go index 956bbeb76..f87bbdb5d 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -1628,6 +1628,14 @@ func isLegacyPrivateOnlyFinalRules(v any) bool { return true } +func isUnrestrictedFreedomFinalRules(v any, present bool) bool { + if !present || v == nil { + return true + } + rules, ok := v.([]any) + return ok && len(rules) == 0 +} + func hardenFreedomFinalRules() error { var setting model.Setting err := db.Model(model.Setting{}).Where("key = ?", "xrayTemplateConfig").First(&setting).Error @@ -1680,7 +1688,10 @@ func rewriteFreedomFinalRulesPrivateEgress(raw string) (string, bool, error) { if !ok { continue } - if !isAllowOnlyFinalRules(settings["finalRules"]) && !isLegacyPrivateOnlyFinalRules(settings["finalRules"]) { + finalRules, present := settings["finalRules"] + if !isUnrestrictedFreedomFinalRules(finalRules, present) && + !isAllowOnlyFinalRules(finalRules) && + !isLegacyPrivateOnlyFinalRules(finalRules) { continue } settings["finalRules"] = []any{ diff --git a/internal/database/freedom_finalrules_migration_test.go b/internal/database/freedom_finalrules_migration_test.go index 201ef9f94..596c59484 100644 --- a/internal/database/freedom_finalrules_migration_test.go +++ b/internal/database/freedom_finalrules_migration_test.go @@ -23,6 +23,24 @@ func TestRewriteFreedomFinalRulesPrivateEgress(t *testing.T) { wantChanged: true, wantRules: hardened, }, + { + name: "missing finalRules is hardened", + raw: `{"outbounds":[{"protocol":"freedom","settings":{"domainStrategy":"AsIs"},"tag":"direct"}]}`, + wantChanged: true, + wantRules: hardened, + }, + { + name: "null finalRules is hardened", + raw: `{"outbounds":[{"protocol":"freedom","settings":{"domainStrategy":"AsIs","finalRules":null},"tag":"direct"}]}`, + wantChanged: true, + wantRules: hardened, + }, + { + name: "empty finalRules is hardened", + raw: `{"outbounds":[{"protocol":"freedom","settings":{"domainStrategy":"AsIs","finalRules":[]},"tag":"direct"}]}`, + wantChanged: true, + wantRules: hardened, + }, { name: "legacy private-only allow is hardened", raw: `{"outbounds":[{"protocol":"freedom","settings":{"finalRules":[{"action":"allow","ip":["geoip:private"]}]},"tag":"direct"}]}`, @@ -76,6 +94,40 @@ func TestRewriteFreedomFinalRulesPrivateEgress(t *testing.T) { } } +func TestRewriteFreedomFinalRulesPreservesSplitRouting(t *testing.T) { + const raw = `{ + "outbounds":[{"protocol":"freedom","settings":{"domainStrategy":"AsIs"},"tag":"direct"}], + "routing":{"domainStrategy":"AsIs","rules":[ + {"type":"field","domain":["regexp:.*\\.ru$"],"outboundTag":"direct"}, + {"type":"field","network":"tcp,udp","outboundTag":"proxy"} + ]} + }` + updated, changed, err := rewriteFreedomFinalRulesPrivateEgress(raw) + if err != nil { + t.Fatalf("rewrite: %v", err) + } + if !changed { + t.Fatal("missing finalRules must be hardened") + } + var before, after map[string]any + if err := json.Unmarshal([]byte(raw), &before); err != nil { + t.Fatalf("decode before: %v", err) + } + if err := json.Unmarshal([]byte(updated), &after); err != nil { + t.Fatalf("decode after: %v", err) + } + beforeRouting, _ := json.Marshal(before["routing"]) + afterRouting, _ := json.Marshal(after["routing"]) + if string(afterRouting) != string(beforeRouting) { + t.Fatalf("split routing changed:\n got %s\nwant %s", afterRouting, beforeRouting) + } + outbound := after["outbounds"].([]any)[0].(map[string]any) + settings := outbound["settings"].(map[string]any) + if settings["domainStrategy"] != "AsIs" { + t.Fatalf("freedom domainStrategy=%v want AsIs", settings["domainStrategy"]) + } +} + func TestRewriteFreedomFinalRulesPrivateEgressInvalidJSON(t *testing.T) { _, changed, err := rewriteFreedomFinalRulesPrivateEgress("{not json") if err == nil {