diff --git a/internal/corecfg/manager.go b/internal/corecfg/manager.go index d289611..6db2328 100644 --- a/internal/corecfg/manager.go +++ b/internal/corecfg/manager.go @@ -381,7 +381,7 @@ func (m *Manager) mergeSubscriptions(subs []*model.Subscription) (map[string]int combined = deepMerge(combined, data) } - combined["rules"] = dedupeMatchRule(combined["rules"]) + combined["rules"] = dedupeMatchRule(dedupeGeoIPRule(combined["rules"])) mergeProxySelection(combined, selectionGroups) return combined, proxies, true @@ -408,6 +408,55 @@ func dedupeMatchRule(rulesRaw interface{}) interface{} { return append(filtered, "MATCH,all proxies") } +// dedupeGeoIPRule keeps only the last occurrence of each "GEOIP," +// 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," 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 { return editor.OpenFileInEditor(path) } diff --git a/internal/corecfg/manager_test.go b/internal/corecfg/manager_test.go index 63379fb..1a53b31 100644 --- a/internal/corecfg/manager_test.go +++ b/internal/corecfg/manager_test.go @@ -31,3 +31,30 @@ func TestDedupeMatchRuleNonList(t *testing.T) { 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) + } +}