feat: dedupe geoip rules

This commit is contained in:
2026-07-20 17:37:37 +08:00
parent 7ff3daa9d3
commit 8c574bf054
2 changed files with 77 additions and 1 deletions

View File

@ -381,7 +381,7 @@ func (m *Manager) mergeSubscriptions(subs []*model.Subscription) (map[string]int
combined = deepMerge(combined, data) combined = deepMerge(combined, data)
} }
combined["rules"] = dedupeMatchRule(combined["rules"]) combined["rules"] = dedupeMatchRule(dedupeGeoIPRule(combined["rules"]))
mergeProxySelection(combined, selectionGroups) mergeProxySelection(combined, selectionGroups)
return combined, proxies, true return combined, proxies, true
@ -408,6 +408,55 @@ func dedupeMatchRule(rulesRaw interface{}) interface{} {
return append(filtered, "MATCH,all proxies") return append(filtered, "MATCH,all proxies")
} }
// dedupeGeoIPRule keeps only the last occurrence of each "GEOIP,<payload>"
// rule contributed by the merged subscriptions — several subscriptions
// commonly ship their own copy of the same rule (e.g. "GEOIP,CN,DIRECT"),
// and concatenating them would leave redundant earlier copies ahead of the
// one that should actually apply.
func dedupeGeoIPRule(rulesRaw interface{}) interface{} {
rules, ok := rulesRaw.([]interface{})
if !ok {
return rulesRaw
}
lastIndex := make(map[string]int)
for i, item := range rules {
if key, ok := geoIPRuleKey(item); ok {
lastIndex[key] = i
}
}
filtered := make([]interface{}, 0, len(rules))
for i, item := range rules {
if key, ok := geoIPRuleKey(item); ok && lastIndex[key] != i {
continue
}
filtered = append(filtered, item)
}
return filtered
}
// geoIPRuleKey returns the "GEOIP,<payload>" portion of a rule line (i.e.
// everything but its target and any trailing "no-resolve" modifier), and
// whether the line is a GEOIP rule at all.
func geoIPRuleKey(item interface{}) (string, bool) {
line, ok := item.(string)
if !ok || !strings.HasPrefix(line, "GEOIP,") {
return "", false
}
body := line
if idx := strings.LastIndex(body, ","); idx != -1 && strings.EqualFold(body[idx+1:], "no-resolve") {
body = body[:idx]
}
idx := strings.LastIndex(body, ",")
if idx == -1 {
return line, true
}
return body[:idx], true
}
func openInEditor(path string) bool { func openInEditor(path string) bool {
return editor.OpenFileInEditor(path) return editor.OpenFileInEditor(path)
} }

View File

@ -31,3 +31,30 @@ func TestDedupeMatchRuleNonList(t *testing.T) {
t.Fatalf("expected nil passthrough, got %#v", got) t.Fatalf("expected nil passthrough, got %#v", got)
} }
} }
func TestDedupeGeoIPRule(t *testing.T) {
rules := []interface{}{
"GEOIP,CN,DIRECT",
"DOMAIN,example.com,sub-a | Proxy",
"GEOIP,CN,sub-b | Proxy,no-resolve",
"GEOIP,JP,sub-b | Proxy",
}
got := dedupeGeoIPRule(rules)
want := []interface{}{
"DOMAIN,example.com,sub-a | Proxy",
"GEOIP,CN,sub-b | Proxy,no-resolve",
"GEOIP,JP,sub-b | Proxy",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %#v, want %#v", got, want)
}
}
func TestDedupeGeoIPRuleNonList(t *testing.T) {
if got := dedupeGeoIPRule(nil); got != nil {
t.Fatalf("expected nil passthrough, got %#v", got)
}
}