init
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user