Files
sing-vis/internal/engine/rules.go
T
2026-07-24 13:29:16 +00:00

315 lines
9.2 KiB
Go

package engine
import (
"strings"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json/badoption"
)
// ---- field extraction ----
func fieldsFromRoute(r option.RawDefaultRule) matchFields {
mf := matchFields{
domain: r.Domain,
domainSuffix: r.DomainSuffix,
domainKeyword: r.DomainKeyword,
domainRegex: r.DomainRegex,
ipCIDR: r.IPCIDR,
ipIsPrivate: r.IPIsPrivate,
srcIPCIDR: r.SourceIPCIDR,
srcIPIsPriv: r.SourceIPIsPrivate,
port: r.Port,
portRange: r.PortRange,
srcPort: r.SourcePort,
srcPortRange: r.SourcePortRange,
network: r.Network,
ruleSet: r.RuleSet,
rsMatchSource: r.RuleSetIPCIDRMatchSource || r.Deprecated_RulesetIPCIDRMatchSource,
invert: r.Invert,
}
addUnknownList(&mf, "inbound", r.Inbound)
addUnknownList(&mf, "protocol", r.Protocol)
addUnknownList(&mf, "client", r.Client)
addUnknownList(&mf, "auth_user", r.AuthUser)
addUnknownList(&mf, "user", r.User)
addUnknownList(&mf, "process_name", r.ProcessName)
addUnknownList(&mf, "process_path", r.ProcessPath)
addUnknownList(&mf, "process_path_regex", r.ProcessPathRegex)
addUnknownList(&mf, "package_name", r.PackageName)
addUnknownList(&mf, "package_name_regex", r.PackageNameRegex)
addUnknownList(&mf, "wifi_ssid", r.WIFISSID)
addUnknownList(&mf, "wifi_bssid", r.WIFIBSSID)
addUnknownList(&mf, "source_mac_address", r.SourceMACAddress)
addUnknownList(&mf, "source_hostname", r.SourceHostname)
addUnknownList(&mf, "preferred_by", r.PreferredBy)
addUnknownDeprecated(&mf, "geosite", r.Geosite)
addUnknownDeprecated(&mf, "geoip", r.GeoIP)
addUnknownDeprecated(&mf, "source_geoip", r.SourceGeoIP)
if r.ClashMode != "" {
mf.unknowns = append(mf.unknowns, condKV{"clash_mode", r.ClashMode})
}
if r.IPVersion != 0 {
mf.unknowns = append(mf.unknowns, condKV{"ip_version", intStr(r.IPVersion)})
}
if r.NetworkIsExpensive {
mf.unknowns = append(mf.unknowns, condKV{"network_is_expensive", "true"})
}
if r.NetworkIsConstrained {
mf.unknowns = append(mf.unknowns, condKV{"network_is_constrained", "true"})
}
if len(r.NetworkType) > 0 {
mf.unknowns = append(mf.unknowns, condKV{"network_type", interfaceTypes(r.NetworkType)})
}
return mf
}
func fieldsFromDNS(r option.RawDefaultDNSRule) matchFields {
mf := matchFields{
domain: r.Domain,
domainSuffix: r.DomainSuffix,
domainKeyword: r.DomainKeyword,
domainRegex: r.DomainRegex,
srcIPCIDR: r.SourceIPCIDR,
srcIPIsPriv: r.SourceIPIsPrivate,
port: r.Port,
portRange: r.PortRange,
srcPort: r.SourcePort,
srcPortRange: r.SourcePortRange,
network: r.Network,
queryType: r.QueryType,
ruleSet: r.RuleSet,
rsMatchSource: r.RuleSetIPCIDRMatchSource || r.Deprecated_RulesetIPCIDRMatchSource,
invert: r.Invert,
}
// DNS ip_cidr / ip_is_private / ip_accept_any and response_* are response
// filters, not query-routing conditions.
if len(r.IPCIDR) > 0 {
mf.dnsFilter = append(mf.dnsFilter, condKV{"ip_cidr", joinVals(r.IPCIDR)})
}
if r.IPIsPrivate {
mf.dnsFilter = append(mf.dnsFilter, condKV{"ip_is_private", "true"})
}
if r.IPAcceptAny {
mf.dnsFilter = append(mf.dnsFilter, condKV{"ip_accept_any", "true"})
}
if r.ResponseRcode != nil {
mf.dnsFilter = append(mf.dnsFilter, condKV{"response_rcode", "set"})
}
if r.MatchResponse != nil {
mf.dnsFilter = append(mf.dnsFilter, condKV{"match_response", "set"})
}
addUnknownList(&mf, "inbound", r.Inbound)
addUnknownList(&mf, "protocol", r.Protocol)
addUnknownList(&mf, "auth_user", r.AuthUser)
addUnknownList(&mf, "user", r.User)
addUnknownList(&mf, "outbound", r.Outbound)
addUnknownList(&mf, "process_name", r.ProcessName)
addUnknownList(&mf, "process_path", r.ProcessPath)
addUnknownList(&mf, "package_name", r.PackageName)
addUnknownList(&mf, "wifi_ssid", r.WIFISSID)
addUnknownList(&mf, "wifi_bssid", r.WIFIBSSID)
addUnknownDeprecated(&mf, "geosite", r.Geosite)
if r.ClashMode != "" {
mf.unknowns = append(mf.unknowns, condKV{"clash_mode", r.ClashMode})
}
if r.IPVersion != 0 {
mf.unknowns = append(mf.unknowns, condKV{"ip_version", intStr(r.IPVersion)})
}
return mf
}
func fieldsFromHeadless(r option.DefaultHeadlessRule) matchFields {
mf := matchFields{
domainKeyword: r.DomainKeyword,
domainRegex: r.DomainRegex,
srcIPCIDR: r.SourceIPCIDR,
port: r.Port,
portRange: r.PortRange,
srcPort: r.SourcePort,
srcPortRange: r.SourcePortRange,
network: r.Network,
queryType: r.QueryType,
invert: r.Invert,
}
// Prefer pre-compiled matchers (present in binary .srs rule sets).
if r.DomainMatcher != nil {
mf.rawDomain = r.DomainMatcher
} else {
mf.domain = r.Domain
mf.domainSuffix = r.DomainSuffix
}
if r.IPSet != nil {
mf.rawIPSet = r.IPSet
} else {
mf.ipCIDR = r.IPCIDR
}
if r.AdGuardDomainMatcher != nil || len(r.AdGuardDomain) > 0 {
mf.unknowns = append(mf.unknowns, condKV{"adguard_domain", "«set»"})
}
addUnknownList(&mf, "process_name", r.ProcessName)
addUnknownList(&mf, "process_path", r.ProcessPath)
addUnknownList(&mf, "package_name", r.PackageName)
addUnknownList(&mf, "wifi_ssid", r.WIFISSID)
addUnknownList(&mf, "wifi_bssid", r.WIFIBSSID)
if r.NetworkIsExpensive {
mf.unknowns = append(mf.unknowns, condKV{"network_is_expensive", "true"})
}
if r.NetworkIsConstrained {
mf.unknowns = append(mf.unknowns, condKV{"network_is_constrained", "true"})
}
if len(r.NetworkType) > 0 {
mf.unknowns = append(mf.unknowns, condKV{"network_type", interfaceTypes(r.NetworkType)})
}
return mf
}
func addUnknownList(mf *matchFields, field string, v badoption.Listable[string]) {
if len(v) > 0 {
mf.unknowns = append(mf.unknowns, condKV{field, joinVals(v)})
}
}
func addUnknownDeprecated(mf *matchFields, field string, v badoption.Listable[string]) {
if len(v) > 0 {
mf.unknowns = append(mf.unknowns, condKV{field + " (deprecated/removed)", joinVals(v)})
}
}
func interfaceTypes(v badoption.Listable[option.InterfaceType]) string {
parts := make([]string, 0, len(v))
for _, t := range v {
parts = append(parts, string(t))
}
return strings.Join(parts, ", ")
}
func intStr(i int) string { return joinVals([]string{itoa(i)}) }
func itoa(i int) string {
if i == 0 {
return "0"
}
neg := i < 0
if neg {
i = -i
}
var b [20]byte
pos := len(b)
for i > 0 {
pos--
b[pos] = byte('0' + i%10)
i /= 10
}
if neg {
pos--
b[pos] = '-'
}
return string(b[pos:])
}
// ---- rule-node evaluation (conditions only; action handled by caller) ----
// evalRuleNode evaluates a route/DNS rule's match conditions recursively.
func (ec *evalCtx) evalRuleNode(r option.Rule, dns bool) RuleEval {
if r.Type == "logical" {
return ec.evalLogical(r.LogicalOptions.Mode, r.LogicalOptions.Rules, r.LogicalOptions.Invert, dns)
}
var mf matchFields
if dns {
// A DNS rule's default variant is carried on a separate type; caller
// passes route-shaped rules only via evalDNSRuleNode. This branch is for
// route rules.
}
mf = fieldsFromRoute(r.DefaultOptions.RawDefaultRule)
status, conds := ec.evalFields(mf)
return RuleEval{
Type: "default",
Status: status,
Invert: mf.invert,
Conditions: conds,
Summary: summarize(conds, mf.invert),
}
}
// evalDNSRuleNode evaluates a DNS rule's match conditions recursively.
func (ec *evalCtx) evalDNSRuleNode(r option.DNSRule) RuleEval {
if r.Type == "logical" {
return ec.evalLogicalDNS(r.LogicalOptions.Mode, r.LogicalOptions.Rules, r.LogicalOptions.Invert)
}
mf := fieldsFromDNS(r.DefaultOptions.RawDefaultDNSRule)
status, conds := ec.evalFields(mf)
return RuleEval{
Type: "default",
Status: status,
Invert: mf.invert,
Conditions: conds,
Summary: summarize(conds, mf.invert),
}
}
func (ec *evalCtx) evalLogical(mode string, rules []option.Rule, invert bool, dns bool) RuleEval {
if mode == "" {
mode = "and"
}
var subs []RuleEval
var statuses []string
for _, sub := range rules {
se := ec.evalRuleNode(sub, dns)
subs = append(subs, se)
statuses = append(statuses, se.Status)
}
var status string
if mode == "or" {
status = orStatus(statuses)
} else {
status = andStatus(statuses)
}
if invert {
status = invertStatus(status)
}
return RuleEval{Type: "logical", Mode: mode, Status: status, Invert: invert, Sub: subs, Summary: "logical " + mode}
}
func (ec *evalCtx) evalLogicalDNS(mode string, rules []option.DNSRule, invert bool) RuleEval {
if mode == "" {
mode = "and"
}
var subs []RuleEval
var statuses []string
for _, sub := range rules {
se := ec.evalDNSRuleNode(sub)
subs = append(subs, se)
statuses = append(statuses, se.Status)
}
var status string
if mode == "or" {
status = orStatus(statuses)
} else {
status = andStatus(statuses)
}
if invert {
status = invertStatus(status)
}
return RuleEval{Type: "logical", Mode: mode, Status: status, Invert: invert, Sub: subs, Summary: "logical " + mode}
}
func summarize(conds []CondEval, invert bool) string {
if len(conds) == 0 {
return "(match all)"
}
parts := make([]string, 0, len(conds))
for _, c := range conds {
v := c.Value
if len(v) > 40 {
v = v[:40] + "…"
}
parts = append(parts, c.Field+"="+v)
}
s := strings.Join(parts, " ")
if invert {
s = "NOT(" + s + ")"
}
return s
}