package rulematch import ( "net" "net/url" "strings" "github.com/prism/proxy/internal/country" "github.com/prism/proxy/internal/store" ) type Result struct { Matched bool `json:"matched"` Host string `json:"host"` Port string `json:"port,omitempty"` RuleLine string `json:"rule_line"` RuleType string `json:"rule_type"` Payload string `json:"payload"` Target string `json:"target"` TargetLabel string `json:"target_label"` Source string `json:"source"` Priority int `json:"priority"` SkippedGeo int `json:"skipped_geo"` Note string `json:"note,omitempty"` } func ParseHost(raw string) (host, port string) { raw = strings.TrimSpace(raw) if raw == "" { return "", "" } if !strings.Contains(raw, "://") { raw = "http://" + raw } u, err := url.Parse(raw) if err != nil { return strings.TrimSpace(raw), "" } host = u.Hostname() port = u.Port() if host == "" { host = strings.TrimSpace(raw) } return strings.ToLower(host), port } func Match(rules []store.Rule, defaultPolicy, rawURL string, geoReady bool) Result { host, port := ParseHost(rawURL) if host == "" { return Result{Note: "无法解析 URL 或域名"} } skippedGeo := 0 for _, r := range rules { if !r.Enabled { continue } ruleType := strings.ToUpper(strings.TrimSpace(r.RuleType)) if ruleType == "MATCH" { continue } if isGeoRule(ruleType) { if !geoReady { skippedGeo++ continue } // Geo rules need Mihomo geodata engine; skip in offline matcher. skippedGeo++ continue } if matchesRule(ruleType, r.Payload, host, port) { line := formatLine(r) return Result{ Matched: true, Host: host, Port: port, RuleLine: line, RuleType: ruleType, Payload: r.Payload, Target: r.Target, TargetLabel: targetLabel(r.Target), Source: r.Source, Priority: r.Priority, SkippedGeo: skippedGeo, } } } policy := defaultPolicy if policy == "" { policy = "DIRECT" } return Result{ Matched: true, Host: host, Port: port, RuleLine: "MATCH," + policy, RuleType: "MATCH", Payload: "", Target: policy, TargetLabel: targetLabel(policy), Source: "system", Priority: 999999, SkippedGeo: skippedGeo, Note: "未命中任何前置规则,使用默认出站", } } func isGeoRule(ruleType string) bool { switch ruleType { case "GEOIP", "GEOSITE": return true default: return false } } func matchesRule(ruleType, payload, host, port string) bool { payload = strings.TrimSpace(payload) switch ruleType { case "DOMAIN": return host == strings.ToLower(payload) case "DOMAIN-SUFFIX": suffix := strings.ToLower(payload) return host == suffix || strings.HasSuffix(host, "."+suffix) case "DOMAIN-KEYWORD": return strings.Contains(host, strings.ToLower(payload)) case "IP-CIDR", "IP-CIDR6": return matchCIDR(host, payload) case "DST-PORT": return port != "" && port == strings.TrimSpace(payload) case "SRC-PORT": return false default: return false } } func matchCIDR(host, cidr string) bool { ip := net.ParseIP(host) if ip == nil { return false } _, network, err := net.ParseCIDR(strings.TrimSpace(cidr)) if err != nil { return false } return network.Contains(ip) } func formatLine(r store.Rule) string { if r.Payload == "" { return r.RuleType + "," + r.Target } return r.RuleType + "," + r.Payload + "," + r.Target } func targetLabel(target string) string { target = strings.TrimSpace(target) if code, ok := country.ParseTargetAlias(target); ok { return country.Name(code) + "(国家池)" } if strings.HasPrefix(target, "country_") { code := strings.TrimPrefix(target, "country_") return country.Name(code) + "(国家池)" } return target }