package automation import ( "fmt" "regexp" "strconv" ) // applyFilters implements the "filters" ssm feature: for each rule, it // scopes down to the proxies matched by rule.Match, decides which of those // are unwanted, and strips them out of every proxy-group's `proxies` list // in finalConfig — both groups merged in from subscriptions and ones built // by a proxy-groups rule. A scoped proxy is unwanted if rule.Compares is // empty (Match alone acts as a denylist), or if it satisfies every entry in // Compares (AND'd) when Compares is non-empty; a proxy whose name doesn't // match a Compares entry's Pattern at all can't be evaluated and is left // alone. Returns one error per rule with an invalid pattern/operator/match; // other rules still run. func applyFilters(finalConfig map[string]interface{}, rules []FilterRule, proxies []ProxyRef) []error { var errs []error for _, rule := range rules { candidates, err := matchProxies(proxies, rule.Match) if err != nil { errs = append(errs, fmt.Errorf("filter '%s': %w", rule.Name, err)) continue } compares, err := compileCompares(rule.Compares) if err != nil { errs = append(errs, fmt.Errorf("filter '%s': %w", rule.Name, err)) continue } unwanted := make(map[string]bool, len(candidates)) for _, p := range candidates { if len(compares) == 0 || satisfiesCompares(compares, p.OriginalName) { unwanted[p.DisplayName] = true } } if len(unwanted) == 0 { fmt.Printf("ℹ️ ssm: filter '%s' matched 0 proxies to remove\n", rule.Name) continue } removeFromProxyGroups(finalConfig, unwanted) } return errs } // removeFromProxyGroups strips every proxy in unwanted out of each proxy // group's `proxies` list, leaving group references, DIRECT/REJECT, etc. // untouched. func removeFromProxyGroups(finalConfig map[string]interface{}, unwanted map[string]bool) { groups, _ := finalConfig["proxy-groups"].([]interface{}) for _, g := range groups { group, ok := g.(map[string]interface{}) if !ok { continue } list, ok := group["proxies"].([]interface{}) if !ok { continue } kept := make([]interface{}, 0, len(list)) for _, item := range list { if name, ok := item.(string); ok && unwanted[name] { continue } kept = append(kept, item) } group["proxies"] = kept } } type compiledCompare struct { re *regexp.Regexp cmp func(a, b float64) bool value float64 } func compileCompares(rules []CompareRule) ([]compiledCompare, error) { compiled := make([]compiledCompare, 0, len(rules)) for _, c := range rules { re, err := regexp.Compile(c.Pattern) if err != nil { return nil, fmt.Errorf("invalid pattern %q: %w", c.Pattern, err) } cmp, err := comparator(c.Operator) if err != nil { return nil, err } compiled = append(compiled, compiledCompare{re: re, cmp: cmp, value: c.Value}) } return compiled, nil } func satisfiesCompares(compares []compiledCompare, name string) bool { for _, c := range compares { m := c.re.FindStringSubmatch(name) if len(m) < 2 { return false } extracted, err := strconv.ParseFloat(m[1], 64) if err != nil { return false } if !c.cmp(extracted, c.value) { return false } } return true } func comparator(op string) (func(a, b float64) bool, error) { switch op { case "<": return func(a, b float64) bool { return a < b }, nil case "<=": return func(a, b float64) bool { return a <= b }, nil case ">": return func(a, b float64) bool { return a > b }, nil case ">=": return func(a, b float64) bool { return a >= b }, nil case "==", "=": return func(a, b float64) bool { return a == b }, nil case "!=": return func(a, b float64) bool { return a != b }, nil default: return nil, fmt.Errorf("unknown operator %q (expected <, <=, >, >=, ==, !=)", op) } }