7a3f6e945d
Refactors TCP and UDP flow managers to enhance analyzer selection and flow binding accuracy, including O(1) UDP stream rebinding by 5-tuple. Introduces runtime stats tracking for engine and ruleset operations, exposing new APIs for granular performance and error metrics. Optimizes GeoMatcher with result caching and supports efficient geosite set matching, reducing redundant computation in ruleset expressions.
876 lines
22 KiB
Go
876 lines
22 KiB
Go
package ruleset
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/expr-lang/expr/builtin"
|
|
|
|
"github.com/expr-lang/expr"
|
|
"github.com/expr-lang/expr/ast"
|
|
"github.com/expr-lang/expr/conf"
|
|
"github.com/expr-lang/expr/parser"
|
|
"github.com/expr-lang/expr/vm"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"git.difuse.io/Difuse/Mellaris/analyzer"
|
|
"git.difuse.io/Difuse/Mellaris/modifier"
|
|
"git.difuse.io/Difuse/Mellaris/ruleset/builtins"
|
|
"git.difuse.io/Difuse/Mellaris/ruleset/builtins/geo"
|
|
)
|
|
|
|
// ExprRule is the external representation of an expression rule.
|
|
type ExprRule struct {
|
|
Name string `yaml:"name"`
|
|
Action string `yaml:"action"`
|
|
Log bool `yaml:"log"`
|
|
Modifier ModifierEntry `yaml:"modifier"`
|
|
Expr string `yaml:"expr"`
|
|
StartTime string `yaml:"start_time"`
|
|
StopTime string `yaml:"stop_time"`
|
|
Weekdays []string `yaml:"weekdays"`
|
|
}
|
|
|
|
type ModifierEntry struct {
|
|
Name string `yaml:"name"`
|
|
Args map[string]interface{} `yaml:"args"`
|
|
}
|
|
|
|
func ExprRulesFromYAML(file string) ([]ExprRule, error) {
|
|
bs, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var rules []ExprRule
|
|
err = yaml.Unmarshal(bs, &rules)
|
|
return rules, err
|
|
}
|
|
|
|
// compiledExprRule is the internal, compiled representation of an expression rule.
|
|
type compiledExprRule struct {
|
|
Name string
|
|
Action *Action // fallthrough if nil
|
|
Log bool
|
|
ModInstance modifier.Instance
|
|
Program *vm.Program
|
|
GeoSiteConditions []string
|
|
StartTimeSecs int // seconds since midnight, -1 if unset
|
|
StopTimeSecs int // seconds since midnight, -1 if unset
|
|
Weekdays []time.Weekday
|
|
WeekdaysNegated bool
|
|
}
|
|
|
|
var _ Ruleset = (*exprRuleset)(nil)
|
|
|
|
var (
|
|
envPool = sync.Pool{
|
|
New: func() any {
|
|
return make(map[string]any, 16)
|
|
},
|
|
}
|
|
subMapPool = sync.Pool{
|
|
New: func() any {
|
|
return make(map[string]any, 8)
|
|
},
|
|
}
|
|
)
|
|
|
|
type exprRuleset struct {
|
|
Rules []compiledExprRule
|
|
Ans []analyzer.Analyzer
|
|
Logger Logger
|
|
GeoMatcher *geo.GeoMatcher
|
|
stats *statsCounters
|
|
}
|
|
|
|
func (r *exprRuleset) Analyzers(info StreamInfo) []analyzer.Analyzer {
|
|
return r.Ans
|
|
}
|
|
|
|
func (r *exprRuleset) Match(info StreamInfo) MatchResult {
|
|
start := time.Now()
|
|
if r.stats != nil {
|
|
r.stats.MatchCalls.Add(1)
|
|
defer func() {
|
|
r.stats.MatchLatencyNanos.Add(uint64(time.Since(start).Nanoseconds()))
|
|
}()
|
|
}
|
|
|
|
env := envPool.Get().(map[string]any)
|
|
clear(env)
|
|
macMap, ipMap, portMap := populateExprEnv(env, info)
|
|
releaseEnv := func() {
|
|
clear(env)
|
|
envPool.Put(env)
|
|
putSubMap(macMap)
|
|
putSubMap(ipMap)
|
|
putSubMap(portMap)
|
|
}
|
|
now := time.Now()
|
|
for _, rule := range r.Rules {
|
|
if !matchTime(now, rule.StartTimeSecs, rule.StopTimeSecs, rule.Weekdays, rule.WeekdaysNegated) {
|
|
continue
|
|
}
|
|
v, err := vm.Run(rule.Program, env)
|
|
if err != nil {
|
|
if r.stats != nil {
|
|
r.stats.MatchErrors.Add(1)
|
|
}
|
|
r.Logger.MatchError(info, rule.Name, err)
|
|
continue
|
|
}
|
|
if vBool, ok := v.(bool); ok && vBool {
|
|
if rule.Log {
|
|
logInfo := info
|
|
if len(rule.GeoSiteConditions) > 0 && r.GeoMatcher != nil {
|
|
logInfo = addGeoSiteLogMetadata(logInfo, r.GeoMatcher, rule.GeoSiteConditions)
|
|
}
|
|
r.Logger.Log(logInfo, rule.Name)
|
|
}
|
|
if rule.Action != nil {
|
|
releaseEnv()
|
|
return MatchResult{
|
|
Action: *rule.Action,
|
|
ModInstance: rule.ModInstance,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
releaseEnv()
|
|
return MatchResult{
|
|
Action: ActionMaybe,
|
|
}
|
|
}
|
|
|
|
func (r *exprRuleset) Stats() Stats {
|
|
if r == nil || r.stats == nil {
|
|
return Stats{}
|
|
}
|
|
return Stats{
|
|
MatchCalls: r.stats.MatchCalls.Load(),
|
|
MatchErrors: r.stats.MatchErrors.Load(),
|
|
MatchLatencyNanos: r.stats.MatchLatencyNanos.Load(),
|
|
LookupCalls: r.stats.LookupCalls.Load(),
|
|
LookupErrors: r.stats.LookupErrors.Load(),
|
|
LookupLatencyNanos: r.stats.LookupLatencyNanos.Load(),
|
|
}
|
|
}
|
|
|
|
// CompileExprRules compiles a list of expression rules into a ruleset.
|
|
// It returns an error if any of the rules are invalid, or if any of the analyzers
|
|
// used by the rules are unknown (not provided in the analyzer list).
|
|
func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier.Modifier, config *BuiltinConfig) (Ruleset, error) {
|
|
var compiledRules []compiledExprRule
|
|
fullAnMap := analyzersToMap(ans)
|
|
fullModMap := modifiersToMap(mods)
|
|
depAnMap := make(map[string]analyzer.Analyzer)
|
|
stats := &statsCounters{}
|
|
funcMap, geoMatcher := buildFunctionMap(config, stats)
|
|
// Compile all rules and build a map of analyzers that are used by the rules.
|
|
for _, rule := range rules {
|
|
if rule.Action == "" && !rule.Log {
|
|
return nil, fmt.Errorf("rule %q must have at least one of action or log", rule.Name)
|
|
}
|
|
var action *Action
|
|
if rule.Action != "" {
|
|
a, ok := actionStringToAction(rule.Action)
|
|
if !ok {
|
|
return nil, fmt.Errorf("rule %q has invalid action %q", rule.Name, rule.Action)
|
|
}
|
|
action = &a
|
|
}
|
|
visitor := &idVisitor{Variables: make(map[string]bool), Identifiers: make(map[string]bool)}
|
|
patcher := &idPatcher{FuncMap: funcMap, GeoMatcher: geoMatcher}
|
|
program, err := expr.Compile(rule.Expr,
|
|
func(c *conf.Config) {
|
|
c.Strict = false
|
|
c.Expect = reflect.Bool
|
|
c.Visitors = append(c.Visitors, visitor, patcher)
|
|
for name, f := range funcMap {
|
|
c.Functions[name] = &builtin.Function{
|
|
Name: name,
|
|
Func: f.Func,
|
|
Types: f.Types,
|
|
}
|
|
}
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rule %q has invalid expression: %w", rule.Name, err)
|
|
}
|
|
if patcher.Err != nil {
|
|
return nil, fmt.Errorf("rule %q failed to patch expression: %w", rule.Name, patcher.Err)
|
|
}
|
|
for name := range visitor.Identifiers {
|
|
// Skip built-in analyzers & user-defined variables
|
|
if isBuiltInAnalyzer(name) || visitor.Variables[name] {
|
|
continue
|
|
}
|
|
if f, ok := funcMap[name]; ok {
|
|
// Built-in function, initialize if necessary
|
|
if f.InitFunc != nil {
|
|
if err := f.InitFunc(); err != nil {
|
|
return nil, fmt.Errorf("rule %q failed to initialize function %q: %w", rule.Name, name, err)
|
|
}
|
|
}
|
|
} else if a, ok := fullAnMap[name]; ok {
|
|
// Analyzer, add to dependency map
|
|
depAnMap[name] = a
|
|
}
|
|
}
|
|
startSecs := -1
|
|
if rule.StartTime != "" {
|
|
startSecs, err = parseTimeOfDay(rule.StartTime)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rule %q has invalid start_time: %w", rule.Name, err)
|
|
}
|
|
}
|
|
stopSecs := -1
|
|
if rule.StopTime != "" {
|
|
stopSecs, err = parseTimeOfDay(rule.StopTime)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rule %q has invalid stop_time: %w", rule.Name, err)
|
|
}
|
|
}
|
|
weekdays, weekdaysNegated, err := parseWeekdays(rule.Weekdays)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rule %q has invalid weekdays: %w", rule.Name, err)
|
|
}
|
|
cr := compiledExprRule{
|
|
Name: rule.Name,
|
|
Action: action,
|
|
Log: rule.Log,
|
|
Program: program,
|
|
GeoSiteConditions: extractGeoSiteConditions(rule.Expr),
|
|
StartTimeSecs: startSecs,
|
|
StopTimeSecs: stopSecs,
|
|
Weekdays: weekdays,
|
|
WeekdaysNegated: weekdaysNegated,
|
|
}
|
|
if action != nil && *action == ActionModify {
|
|
mod, ok := fullModMap[rule.Modifier.Name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("rule %q uses unknown modifier %q", rule.Name, rule.Modifier.Name)
|
|
}
|
|
modInst, err := mod.New(rule.Modifier.Args)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rule %q failed to create modifier instance: %w", rule.Name, err)
|
|
}
|
|
cr.ModInstance = modInst
|
|
}
|
|
compiledRules = append(compiledRules, cr)
|
|
}
|
|
// Convert the analyzer map to a list.
|
|
var depAns []analyzer.Analyzer
|
|
for _, a := range depAnMap {
|
|
depAns = append(depAns, a)
|
|
}
|
|
return &exprRuleset{
|
|
Rules: compiledRules,
|
|
Ans: depAns,
|
|
Logger: config.Logger,
|
|
GeoMatcher: geoMatcher,
|
|
stats: stats,
|
|
}, nil
|
|
}
|
|
|
|
func populateExprEnv(m map[string]any, info StreamInfo) (macMap, ipMap, portMap map[string]any) {
|
|
macMap = getSubMap()
|
|
ipMap = getSubMap()
|
|
portMap = getSubMap()
|
|
|
|
macMap["src"] = info.SrcMAC.String()
|
|
macMap["dst"] = info.DstMAC.String()
|
|
ipMap["src"] = info.SrcIP.String()
|
|
ipMap["dst"] = info.DstIP.String()
|
|
portMap["src"] = info.SrcPort
|
|
portMap["dst"] = info.DstPort
|
|
|
|
m["id"] = info.ID
|
|
m["proto"] = info.Protocol.String()
|
|
m["mac"] = macMap
|
|
m["ip"] = ipMap
|
|
m["port"] = portMap
|
|
for anName, anProps := range info.Props {
|
|
if len(anProps) != 0 {
|
|
m[anName] = anProps
|
|
}
|
|
}
|
|
return macMap, ipMap, portMap
|
|
}
|
|
|
|
func getSubMap() map[string]any {
|
|
m := subMapPool.Get().(map[string]any)
|
|
clear(m)
|
|
return m
|
|
}
|
|
|
|
func putSubMap(m map[string]any) {
|
|
if m == nil {
|
|
return
|
|
}
|
|
clear(m)
|
|
subMapPool.Put(m)
|
|
}
|
|
|
|
func isBuiltInAnalyzer(name string) bool {
|
|
switch name {
|
|
case "id", "proto", "mac", "ip", "port":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func actionStringToAction(action string) (Action, bool) {
|
|
switch strings.ToLower(action) {
|
|
case "allow":
|
|
return ActionAllow, true
|
|
case "block":
|
|
return ActionBlock, true
|
|
case "drop":
|
|
return ActionDrop, true
|
|
case "modify":
|
|
return ActionModify, true
|
|
default:
|
|
return ActionMaybe, false
|
|
}
|
|
}
|
|
|
|
// analyzersToMap converts a list of analyzers to a map of name -> analyzer.
|
|
// This is for easier lookup when compiling rules.
|
|
func analyzersToMap(ans []analyzer.Analyzer) map[string]analyzer.Analyzer {
|
|
anMap := make(map[string]analyzer.Analyzer)
|
|
for _, a := range ans {
|
|
anMap[a.Name()] = a
|
|
}
|
|
return anMap
|
|
}
|
|
|
|
// modifiersToMap converts a list of modifiers to a map of name -> modifier.
|
|
// This is for easier lookup when compiling rules.
|
|
func modifiersToMap(mods []modifier.Modifier) map[string]modifier.Modifier {
|
|
modMap := make(map[string]modifier.Modifier)
|
|
for _, m := range mods {
|
|
modMap[m.Name()] = m
|
|
}
|
|
return modMap
|
|
}
|
|
|
|
// idVisitor is a visitor that collects all identifiers in an expression.
|
|
// This is for determining which analyzers are used by the expression.
|
|
type idVisitor struct {
|
|
Variables map[string]bool
|
|
Identifiers map[string]bool
|
|
}
|
|
|
|
func (v *idVisitor) Visit(node *ast.Node) {
|
|
if varNode, ok := (*node).(*ast.VariableDeclaratorNode); ok {
|
|
v.Variables[varNode.Name] = true
|
|
} else if idNode, ok := (*node).(*ast.IdentifierNode); ok {
|
|
v.Identifiers[idNode.Value] = true
|
|
}
|
|
}
|
|
|
|
// idPatcher patches the AST during expr compilation, replacing certain values with
|
|
// their internal representations for better runtime performance.
|
|
type idPatcher struct {
|
|
FuncMap map[string]*Function
|
|
GeoMatcher *geo.GeoMatcher
|
|
Err error
|
|
}
|
|
|
|
func (p *idPatcher) Visit(node *ast.Node) {
|
|
if p.tryPatchGeoSiteORChain(node) {
|
|
return
|
|
}
|
|
switch (*node).(type) {
|
|
case *ast.CallNode:
|
|
callNode := (*node).(*ast.CallNode)
|
|
if callNode.Callee == nil {
|
|
// Ignore invalid call nodes
|
|
return
|
|
}
|
|
if f, ok := p.FuncMap[callNode.Callee.String()]; ok {
|
|
if f.PatchFunc != nil {
|
|
if err := f.PatchFunc(&callNode.Arguments); err != nil {
|
|
p.Err = err
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *idPatcher) tryPatchGeoSiteORChain(node *ast.Node) bool {
|
|
if p == nil || p.GeoMatcher == nil {
|
|
return false
|
|
}
|
|
terms, ok := collectGeoSiteORChain(*node)
|
|
if !ok || len(terms) < 2 {
|
|
return false
|
|
}
|
|
hostExpr := strings.TrimSpace(terms[0].hostExpr)
|
|
if hostExpr == "" {
|
|
return false
|
|
}
|
|
conditions := make([]string, 0, len(terms))
|
|
for _, term := range terms {
|
|
if strings.TrimSpace(term.hostExpr) != hostExpr {
|
|
return false
|
|
}
|
|
conditions = append(conditions, term.condition)
|
|
}
|
|
normalized := normalizeUniqueLowerStrings(conditions)
|
|
if len(normalized) < 2 {
|
|
return false
|
|
}
|
|
hostNode, err := parser.Parse(hostExpr)
|
|
if err != nil || hostNode == nil || hostNode.Node == nil {
|
|
return false
|
|
}
|
|
call := &ast.CallNode{
|
|
Callee: &ast.IdentifierNode{Value: "geosite_set"},
|
|
Arguments: []ast.Node{
|
|
hostNode.Node,
|
|
&ast.ConstantNode{Value: &geo.SiteConditionSet{Conditions: normalized}},
|
|
},
|
|
}
|
|
ast.Patch(node, call)
|
|
return true
|
|
}
|
|
|
|
type geositeTerm struct {
|
|
hostExpr string
|
|
condition string
|
|
}
|
|
|
|
func collectGeoSiteORChain(node ast.Node) ([]geositeTerm, bool) {
|
|
switch n := node.(type) {
|
|
case *ast.BinaryNode:
|
|
if n.Operator != "or" && n.Operator != "||" {
|
|
return nil, false
|
|
}
|
|
left, ok := collectGeoSiteORChain(n.Left)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
right, ok := collectGeoSiteORChain(n.Right)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
out := make([]geositeTerm, 0, len(left)+len(right))
|
|
out = append(out, left...)
|
|
out = append(out, right...)
|
|
return out, true
|
|
case *ast.CallNode:
|
|
idNode, ok := n.Callee.(*ast.IdentifierNode)
|
|
if !ok || len(n.Arguments) < 2 {
|
|
return nil, false
|
|
}
|
|
name := strings.ToLower(idNode.Value)
|
|
if name == "geosite" {
|
|
condNode, ok := n.Arguments[1].(*ast.StringNode)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
return []geositeTerm{{
|
|
hostExpr: n.Arguments[0].String(),
|
|
condition: condNode.Value,
|
|
}}, true
|
|
}
|
|
if name != "geosite_set" {
|
|
return nil, false
|
|
}
|
|
setNode, ok := n.Arguments[1].(*ast.ConstantNode)
|
|
if !ok || setNode.Value == nil {
|
|
return nil, false
|
|
}
|
|
set, ok := setNode.Value.(*geo.SiteConditionSet)
|
|
if !ok || set == nil {
|
|
return nil, false
|
|
}
|
|
if len(set.Conditions) == 0 {
|
|
return nil, false
|
|
}
|
|
out := make([]geositeTerm, 0, len(set.Conditions))
|
|
hostExpr := n.Arguments[0].String()
|
|
for _, condition := range set.Conditions {
|
|
out = append(out, geositeTerm{hostExpr: hostExpr, condition: condition})
|
|
}
|
|
return out, true
|
|
default:
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
type Function struct {
|
|
InitFunc func() error
|
|
PatchFunc func(args *[]ast.Node) error
|
|
Func func(params ...any) (any, error)
|
|
Types []reflect.Type
|
|
}
|
|
|
|
func buildFunctionMap(config *BuiltinConfig, stats *statsCounters) (map[string]*Function, *geo.GeoMatcher) {
|
|
geoMatcher := geo.NewGeoMatcher(config.GeoSiteFilename, config.GeoIpFilename)
|
|
return map[string]*Function{
|
|
"geoip": {
|
|
InitFunc: geoMatcher.LoadGeoIP,
|
|
PatchFunc: nil,
|
|
Func: func(params ...any) (any, error) {
|
|
return geoMatcher.MatchGeoIp(params[0].(string), params[1].(string)), nil
|
|
},
|
|
Types: []reflect.Type{reflect.TypeOf(geoMatcher.MatchGeoIp)},
|
|
},
|
|
"geosite": {
|
|
InitFunc: geoMatcher.LoadGeoSite,
|
|
PatchFunc: nil,
|
|
Func: func(params ...any) (any, error) {
|
|
return geoMatcher.MatchGeoSite(params[0].(string), params[1].(string)), nil
|
|
},
|
|
Types: []reflect.Type{reflect.TypeOf(geoMatcher.MatchGeoSite)},
|
|
},
|
|
"geosite_set": {
|
|
InitFunc: geoMatcher.LoadGeoSite,
|
|
PatchFunc: nil,
|
|
Func: func(params ...any) (any, error) {
|
|
return geoMatcher.MatchGeoSiteSet(params[0].(string), params[1].(*geo.SiteConditionSet)), nil
|
|
},
|
|
Types: []reflect.Type{
|
|
reflect.TypeOf((func(string, *geo.SiteConditionSet) bool)(nil)),
|
|
},
|
|
},
|
|
"cidr": {
|
|
InitFunc: nil,
|
|
PatchFunc: func(args *[]ast.Node) error {
|
|
cidrStringNode, ok := (*args)[1].(*ast.StringNode)
|
|
if !ok {
|
|
return fmt.Errorf("cidr: invalid argument type")
|
|
}
|
|
cidr, err := builtins.CompileCIDR(cidrStringNode.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
(*args)[1] = &ast.ConstantNode{Value: cidr}
|
|
return nil
|
|
},
|
|
Func: func(params ...any) (any, error) {
|
|
return builtins.MatchCIDR(params[0].(string), params[1].(*net.IPNet)), nil
|
|
},
|
|
Types: []reflect.Type{reflect.TypeOf(builtins.MatchCIDR)},
|
|
},
|
|
"lookup": {
|
|
InitFunc: nil,
|
|
PatchFunc: func(args *[]ast.Node) error {
|
|
var serverStr *ast.StringNode
|
|
if len(*args) > 1 {
|
|
// Has the optional server argument
|
|
var ok bool
|
|
serverStr, ok = (*args)[1].(*ast.StringNode)
|
|
if !ok {
|
|
return fmt.Errorf("lookup: invalid argument type")
|
|
}
|
|
}
|
|
r := &net.Resolver{
|
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
if serverStr != nil {
|
|
address = serverStr.Value
|
|
}
|
|
return config.ProtectedDialContext(ctx, network, address)
|
|
},
|
|
}
|
|
if len(*args) > 1 {
|
|
(*args)[1] = &ast.ConstantNode{Value: r}
|
|
} else {
|
|
*args = append(*args, &ast.ConstantNode{Value: r})
|
|
}
|
|
return nil
|
|
},
|
|
Func: func(params ...any) (any, error) {
|
|
start := time.Now()
|
|
if stats != nil {
|
|
stats.LookupCalls.Add(1)
|
|
defer func() {
|
|
stats.LookupLatencyNanos.Add(uint64(time.Since(start).Nanoseconds()))
|
|
}()
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
|
defer cancel()
|
|
out, err := params[1].(*net.Resolver).LookupHost(ctx, params[0].(string))
|
|
if err != nil && stats != nil {
|
|
stats.LookupErrors.Add(1)
|
|
}
|
|
return out, err
|
|
},
|
|
Types: []reflect.Type{
|
|
reflect.TypeOf((func(string, *net.Resolver) []string)(nil)),
|
|
},
|
|
},
|
|
}, geoMatcher
|
|
}
|
|
|
|
func matchTime(now time.Time, startSecs, stopSecs int, weekdays []time.Weekday, negated bool) bool {
|
|
if startSecs >= 0 || stopSecs >= 0 {
|
|
currentSecs := now.Hour()*3600 + now.Minute()*60 + now.Second()
|
|
if startSecs >= 0 && stopSecs >= 0 {
|
|
if startSecs <= stopSecs {
|
|
if currentSecs < startSecs || currentSecs > stopSecs {
|
|
return false
|
|
}
|
|
} else {
|
|
if currentSecs < startSecs && currentSecs > stopSecs {
|
|
return false
|
|
}
|
|
}
|
|
} else if startSecs >= 0 {
|
|
if currentSecs < startSecs {
|
|
return false
|
|
}
|
|
} else if currentSecs > stopSecs {
|
|
return false
|
|
}
|
|
}
|
|
if len(weekdays) > 0 {
|
|
current := now.Weekday()
|
|
found := false
|
|
for _, d := range weekdays {
|
|
if current == d {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if negated == found {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func parseTimeOfDay(s string) (int, error) {
|
|
t, err := time.Parse("15:04:05", s)
|
|
if err != nil {
|
|
return -1, fmt.Errorf("invalid time %q (expected hh:mm:ss)", s)
|
|
}
|
|
return t.Hour()*3600 + t.Minute()*60 + t.Second(), nil
|
|
}
|
|
|
|
func parseWeekdays(days []string) ([]time.Weekday, bool, error) {
|
|
if len(days) == 0 {
|
|
return nil, false, nil
|
|
}
|
|
negated := false
|
|
parsed := make([]time.Weekday, 0, len(days))
|
|
for i, d := range days {
|
|
d = strings.TrimSpace(d)
|
|
if i == 0 && strings.HasPrefix(d, "!") {
|
|
negated = true
|
|
d = strings.TrimSpace(strings.TrimPrefix(d, "!"))
|
|
}
|
|
var wd time.Weekday
|
|
switch strings.ToLower(d) {
|
|
case "sun", "sunday":
|
|
wd = time.Sunday
|
|
case "mon", "monday":
|
|
wd = time.Monday
|
|
case "tue", "tues", "tuesday":
|
|
wd = time.Tuesday
|
|
case "wed", "wednesday":
|
|
wd = time.Wednesday
|
|
case "thu", "thur", "thurs", "thursday":
|
|
wd = time.Thursday
|
|
case "fri", "friday":
|
|
wd = time.Friday
|
|
case "sat", "saturday":
|
|
wd = time.Saturday
|
|
default:
|
|
return nil, false, fmt.Errorf("invalid weekday %q", d)
|
|
}
|
|
parsed = append(parsed, wd)
|
|
}
|
|
return parsed, negated, nil
|
|
}
|
|
|
|
const rulesetLogMetaKey = "_ruleset"
|
|
|
|
func addGeoSiteLogMetadata(info StreamInfo, gm *geo.GeoMatcher, conditions []string) StreamInfo {
|
|
hosts := extractGeoSiteHostCandidates(info)
|
|
if len(hosts) == 0 {
|
|
return info
|
|
}
|
|
matchedGeoSites := matchGeoSiteConditions(hosts, conditions, gm.MatchGeoSite)
|
|
if len(matchedGeoSites) == 0 {
|
|
return info
|
|
}
|
|
clonedProps := cloneCombinedPropMap(info.Props)
|
|
clonedProps[rulesetLogMetaKey] = analyzer.PropMap{
|
|
"geosite": matchedGeoSites,
|
|
"hosts": hosts,
|
|
}
|
|
info.Props = clonedProps
|
|
return info
|
|
}
|
|
|
|
func extractGeoSiteHostCandidates(info StreamInfo) []string {
|
|
out := make([]string, 0, 4)
|
|
seen := make(map[string]struct{}, 4)
|
|
add := func(raw string) {
|
|
host := normalizeHost(raw)
|
|
if host == "" {
|
|
return
|
|
}
|
|
if _, ok := seen[host]; ok {
|
|
return
|
|
}
|
|
seen[host] = struct{}{}
|
|
out = append(out, host)
|
|
}
|
|
if sni, ok := info.Props.Get("tls", "req.sni").(string); ok {
|
|
add(sni)
|
|
}
|
|
if sni, ok := info.Props.Get("quic", "req.sni").(string); ok {
|
|
add(sni)
|
|
}
|
|
if host, ok := info.Props.Get("http", "req.headers.host").(string); ok {
|
|
add(host)
|
|
}
|
|
if addr, ok := info.Props.Get("socks", "req.addr").(string); ok {
|
|
add(addr)
|
|
}
|
|
qs := info.Props.Get("dns", "questions")
|
|
switch v := qs.(type) {
|
|
case []analyzer.PropMap:
|
|
for _, q := range v {
|
|
if name, ok := q["name"].(string); ok {
|
|
add(name)
|
|
}
|
|
}
|
|
case []interface{}:
|
|
for _, item := range v {
|
|
switch q := item.(type) {
|
|
case analyzer.PropMap:
|
|
if name, ok := q["name"].(string); ok {
|
|
add(name)
|
|
}
|
|
case map[string]interface{}:
|
|
if name, ok := q["name"].(string); ok {
|
|
add(name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func normalizeHost(raw string) string {
|
|
s := strings.TrimSpace(strings.ToLower(raw))
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
// Handle bracketed host:port first, then unbracketed host:port.
|
|
if strings.HasPrefix(s, "[") {
|
|
if host, _, err := net.SplitHostPort(s); err == nil {
|
|
s = host
|
|
}
|
|
} else if strings.Count(s, ":") == 1 {
|
|
if host, _, err := net.SplitHostPort(s); err == nil {
|
|
s = host
|
|
}
|
|
}
|
|
s = strings.TrimPrefix(s, "[")
|
|
s = strings.TrimSuffix(s, "]")
|
|
s = strings.TrimSuffix(s, ".")
|
|
if s == "" || net.ParseIP(s) != nil {
|
|
return ""
|
|
}
|
|
return s
|
|
}
|
|
|
|
func matchGeoSiteConditions(hosts, conditions []string, matchFn func(site, condition string) bool) []string {
|
|
out := make([]string, 0, len(conditions))
|
|
seen := make(map[string]struct{}, len(conditions))
|
|
for _, cond := range conditions {
|
|
c := strings.TrimSpace(strings.ToLower(cond))
|
|
if c == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[c]; ok {
|
|
continue
|
|
}
|
|
for _, host := range hosts {
|
|
if matchFn(host, c) {
|
|
seen[c] = struct{}{}
|
|
out = append(out, c)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneCombinedPropMap(in analyzer.CombinedPropMap) analyzer.CombinedPropMap {
|
|
if in == nil {
|
|
return analyzer.CombinedPropMap{}
|
|
}
|
|
out := make(analyzer.CombinedPropMap, len(in)+1)
|
|
for k, v := range in {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func extractGeoSiteConditions(expression string) []string {
|
|
tree, err := parser.Parse(expression)
|
|
if err != nil || tree == nil || tree.Node == nil {
|
|
return nil
|
|
}
|
|
root := tree.Node
|
|
v := &geositeCallVisitor{
|
|
conditions: make([]string, 0, 4),
|
|
}
|
|
ast.Walk(&root, v)
|
|
return normalizeUniqueLowerStrings(v.conditions)
|
|
}
|
|
|
|
type geositeCallVisitor struct {
|
|
conditions []string
|
|
}
|
|
|
|
func (v *geositeCallVisitor) Visit(node *ast.Node) {
|
|
callNode, ok := (*node).(*ast.CallNode)
|
|
if !ok || callNode.Callee == nil || len(callNode.Arguments) < 2 {
|
|
return
|
|
}
|
|
idNode, ok := callNode.Callee.(*ast.IdentifierNode)
|
|
if !ok || strings.ToLower(idNode.Value) != "geosite" {
|
|
return
|
|
}
|
|
stringNode, ok := callNode.Arguments[1].(*ast.StringNode)
|
|
if !ok {
|
|
return
|
|
}
|
|
v.conditions = append(v.conditions, stringNode.Value)
|
|
}
|
|
|
|
func normalizeUniqueLowerStrings(in []string) []string {
|
|
out := make([]string, 0, len(in))
|
|
seen := make(map[string]struct{}, len(in))
|
|
for _, v := range in {
|
|
s := strings.TrimSpace(strings.ToLower(v))
|
|
if s == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[s]; ok {
|
|
continue
|
|
}
|
|
seen[s] = struct{}{}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|