fc3a66dc83
Go 控制面(SQLite + REST API)嵌入 Mihomo 数据面,Vue 3 多语言 Web 管理台。 含订阅/节点/规则/出站/监控/日志、OpenAPI、API Key、Gitea Actions CI(runs-on: ubuntu-latest) 与开源文档;镜像发布至 git.rc707blog.top Container Registry。 Co-authored-by: Cursor <cursoragent@cursor.com>
169 lines
3.8 KiB
Go
169 lines
3.8 KiB
Go
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
|
|
}
|