315 lines
9.2 KiB
Go
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
|
|
}
|