feat: evolve rate-filter to a more generic filter

This commit is contained in:
2026-07-28 20:40:12 +08:00
parent a3e3079af1
commit b206594eb8
15 changed files with 492 additions and 193 deletions

View File

@ -14,18 +14,22 @@ func TestParseConfig_FullSchema(t *testing.T) {
"type": "url-test",
"match": map[string]interface{}{
"subscriptions": []interface{}{"home-sub"},
"name-contains": []interface{}{"HK", "Hong Kong"},
"name-pattern": "(?i)HK|Hong Kong",
},
"url": "http://www.gstatic.com/generate_204",
"interval": 300,
},
},
"rate-filters": []interface{}{
"filters": []interface{}{
map[string]interface{}{
"name": "Cheap",
"pattern": `([\d.]+)x`,
"operator": "<=",
"value": 1.0,
"name": "Cheap",
"compares": []interface{}{
map[string]interface{}{
"pattern": `([\d.]+)x`,
"operator": "<=",
"value": 1.0,
},
},
},
},
"patches": []interface{}{
@ -52,8 +56,8 @@ func TestParseConfig_FullSchema(t *testing.T) {
if len(pg.Match.Subscriptions) != 1 || pg.Match.Subscriptions[0] != "home-sub" {
t.Fatalf("unexpected match.subscriptions: %#v", pg.Match.Subscriptions)
}
if len(pg.Match.NameContains) != 2 {
t.Fatalf("unexpected match.name-contains: %#v", pg.Match.NameContains)
if pg.Match.NamePattern != "(?i)HK|Hong Kong" {
t.Fatalf("unexpected match.name-pattern: %#v", pg.Match.NamePattern)
}
// "url" and "interval" aren't named fields on ProxyGroupRule — they must
// land in Extra via the inline tag, not get silently dropped.
@ -67,8 +71,9 @@ func TestParseConfig_FullSchema(t *testing.T) {
t.Fatalf("named field 'match' leaked into Extra: %#v", pg.Extra)
}
if len(cfg.RateFilters) != 1 || cfg.RateFilters[0].Operator != "<=" || cfg.RateFilters[0].Value != 1.0 {
t.Fatalf("unexpected rate-filters: %#v", cfg.RateFilters)
if len(cfg.Filters) != 1 || len(cfg.Filters[0].Compares) != 1 ||
cfg.Filters[0].Compares[0].Operator != "<=" || cfg.Filters[0].Compares[0].Value != 1.0 {
t.Fatalf("unexpected filters: %#v", cfg.Filters)
}
if len(cfg.Patches) != 1 || cfg.Patches[0].Path != "dns.nameserver" || cfg.Patches[0].Op != "append" {
@ -86,7 +91,7 @@ ssm:
- name: HK Nodes
type: select
match:
name-contains: [HK]
name-pattern: "HK"
patches:
- path: rules
op: prepend
@ -118,18 +123,31 @@ func TestApply_EndToEnd(t *testing.T) {
cfg := &Config{
ProxyGroups: []ProxyGroupRule{
{Name: "HK Group", Type: "select", Match: MatchRule{NameContains: []string{"HK"}}},
{Name: "HK Group", Type: "select", Match: MatchRule{NamePattern: "HK"}},
},
RateFilters: []RateFilterRule{
{Name: "Cheap", Type: "select", Pattern: `([\d.]+)x`, Operator: "<=", Value: 1.0},
Filters: []FilterRule{
{Name: "Remove Expensive", Compares: []CompareRule{
{Pattern: `([\d.]+)x`, Operator: ">", Value: 1.0},
}},
},
Patches: []PatchRule{
{Path: "rules", Op: "append", Value: []interface{}{"MATCH,DIRECT"}},
},
}
// Simulates a proxy-group already merged in from a subscription, which
// filters must also scrub even though proxy-groups rules never touch it.
finalConfig := map[string]interface{}{
"rules": []interface{}{},
"proxy-groups": []interface{}{
map[string]interface{}{
"name": "sub-a | Auto",
"type": "url-test",
"proxies": []interface{}{
"sub-a | HK 01 | 1.0x", "sub-a | HK 02 | 2.5x",
},
},
},
}
errs := Apply(finalConfig, cfg, proxies)
@ -142,23 +160,22 @@ func TestApply_EndToEnd(t *testing.T) {
t.Fatalf("expected 2 proxy-groups, got %#v", finalConfig["proxy-groups"])
}
hkGroup := groups[0].(map[string]interface{})
if hkGroup["name"] != "HK Group" {
t.Fatalf("unexpected first group: %#v", hkGroup)
subGroup := groups[0].(map[string]interface{})
if subGroup["name"] != "sub-a | Auto" {
t.Fatalf("unexpected first group: %#v", subGroup)
}
hkProxies := hkGroup["proxies"].([]interface{})
if len(hkProxies) != 2 {
t.Fatalf("expected 2 HK proxies, got %#v", hkProxies)
subProxies := subGroup["proxies"].([]interface{})
if len(subProxies) != 1 || subProxies[0] != "sub-a | HK 01 | 1.0x" {
t.Fatalf("expected the expensive proxy scrubbed from the subscription group, got %#v", subProxies)
}
cheapGroup := groups[1].(map[string]interface{})
if cheapGroup["name"] != "Cheap" {
t.Fatalf("unexpected second group: %#v", cheapGroup)
hkGroup := groups[1].(map[string]interface{})
if hkGroup["name"] != "HK Group" {
t.Fatalf("unexpected second group: %#v", hkGroup)
}
cheapProxies := cheapGroup["proxies"].([]interface{})
want := []interface{}{"sub-a | HK 01 | 1.0x", "sub-b | SG 01 | 0.5x"}
if len(cheapProxies) != 2 || cheapProxies[0] != want[0] || cheapProxies[1] != want[1] {
t.Fatalf("unexpected cheap proxies: %#v", cheapProxies)
hkProxies := hkGroup["proxies"].([]interface{})
if len(hkProxies) != 1 || hkProxies[0] != "sub-a | HK 01 | 1.0x" {
t.Fatalf("expected the expensive proxy scrubbed from the built HK Group too, got %#v", hkProxies)
}
if rules, _ := finalConfig["rules"].([]interface{}); len(rules) != 1 || rules[0] != "MATCH,DIRECT" {
@ -166,7 +183,7 @@ func TestApply_EndToEnd(t *testing.T) {
}
}
func TestApply_InvalidRateFilterDoesNotBlockOthers(t *testing.T) {
func TestApply_InvalidFilterDoesNotBlockOthers(t *testing.T) {
proxies := []ProxyRef{
{OriginalName: "HK 01", DisplayName: "sub-a | HK 01", Subscription: "sub-a"},
}
@ -174,8 +191,8 @@ func TestApply_InvalidRateFilterDoesNotBlockOthers(t *testing.T) {
ProxyGroups: []ProxyGroupRule{
{Name: "All", Type: "select"},
},
RateFilters: []RateFilterRule{
{Name: "Bad", Pattern: "(", Operator: "<="},
Filters: []FilterRule{
{Name: "Bad", Compares: []CompareRule{{Pattern: "(", Operator: "<="}}},
},
}
finalConfig := map[string]interface{}{}
@ -189,3 +206,67 @@ func TestApply_InvalidRateFilterDoesNotBlockOthers(t *testing.T) {
t.Fatalf("expected the valid proxy-groups rule to still run: %#v", finalConfig["proxy-groups"])
}
}
func TestApplyFilters_NameOnlyActsAsDenylist(t *testing.T) {
proxies := []ProxyRef{
{OriginalName: "HK 01", DisplayName: "sub-a | HK 01", Subscription: "sub-a"},
{OriginalName: "SG 01", DisplayName: "sub-b | SG 01", Subscription: "sub-b"},
}
finalConfig := map[string]interface{}{
"proxy-groups": []interface{}{
map[string]interface{}{
"name": "Auto",
"proxies": []interface{}{"sub-a | HK 01", "sub-b | SG 01", "DIRECT"},
},
},
}
errs := applyFilters(finalConfig, []FilterRule{
{Name: "No HK", Match: MatchRule{NamePattern: "^HK"}},
}, proxies)
if len(errs) != 0 {
t.Fatalf("unexpected errors: %v", errs)
}
names := finalConfig["proxy-groups"].([]interface{})[0].(map[string]interface{})["proxies"].([]interface{})
want := []interface{}{"sub-b | SG 01", "DIRECT"}
if len(names) != 2 || names[0] != want[0] || names[1] != want[1] {
t.Fatalf("expected HK proxy removed, DIRECT and SG left alone, got %#v", names)
}
}
func TestApplyFilters_MultipleComparesAreANDed(t *testing.T) {
proxies := []ProxyRef{
{OriginalName: "HK 01 | 1.0x | 50ms", DisplayName: "sub-a | HK 01 | 1.0x | 50ms", Subscription: "sub-a"},
{OriginalName: "HK 02 | 1.0x | 200ms", DisplayName: "sub-a | HK 02 | 1.0x | 200ms", Subscription: "sub-a"},
{OriginalName: "SG 01 | 2.5x | 50ms", DisplayName: "sub-b | SG 01 | 2.5x | 50ms", Subscription: "sub-b"},
}
finalConfig := map[string]interface{}{
"proxy-groups": []interface{}{
map[string]interface{}{
"name": "Auto",
"proxies": []interface{}{
"sub-a | HK 01 | 1.0x | 50ms",
"sub-a | HK 02 | 1.0x | 200ms",
"sub-b | SG 01 | 2.5x | 50ms",
},
},
},
}
errs := applyFilters(finalConfig, []FilterRule{
{Name: "Remove Cheap and Fast", Compares: []CompareRule{
{Pattern: `([\d.]+)x`, Operator: "<=", Value: 1.0},
{Pattern: `(\d+)ms`, Operator: "<=", Value: 100},
}},
}, proxies)
if len(errs) != 0 {
t.Fatalf("unexpected errors: %v", errs)
}
names := finalConfig["proxy-groups"].([]interface{})[0].(map[string]interface{})["proxies"].([]interface{})
want := []interface{}{"sub-a | HK 02 | 1.0x | 200ms", "sub-b | SG 01 | 2.5x | 50ms"}
if len(names) != 2 || names[0] != want[0] || names[1] != want[1] {
t.Fatalf("expected only the cheap+fast proxy removed (both conditions must hold), got %#v", names)
}
}