Add CIDR support for expr (#62)

* feat: add cidr support for expr

* docs: add example for cidr

* minor code tweaks

---------

Co-authored-by: Toby <tobyxdd@gmail.com>
This commit is contained in:
Rinka 2024-02-18 06:21:12 +08:00 committed by GitHub
parent 94cfe7b2c1
commit ebff4308e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 74 additions and 1 deletions

View File

@ -130,6 +130,10 @@ workers:
- name: block CN geoip - name: block CN geoip
action: block action: block
expr: geoip(string(ip.dst), "cn") expr: geoip(string(ip.dst), "cn")
- name: block cidr
action: block
expr: cidr(string(ip.dst), "192.168.0.0/16")
``` ```
#### サポートされるアクション #### サポートされるアクション

View File

@ -136,6 +136,10 @@ to [Expr Language Definition](https://expr-lang.org/docs/language-definition).
- name: block CN geoip - name: block CN geoip
action: block action: block
expr: geoip(string(ip.dst), "cn") expr: geoip(string(ip.dst), "cn")
- name: block cidr
action: block
expr: cidr(string(ip.dst), "192.168.0.0/16")
``` ```
#### Supported actions #### Supported actions

View File

@ -131,6 +131,10 @@ workers:
- name: block CN geoip - name: block CN geoip
action: block action: block
expr: geoip(string(ip.dst), "cn") expr: geoip(string(ip.dst), "cn")
- name: block cidr
action: block
expr: cidr(string(ip.dst), "192.168.0.0/16")
``` ```
#### 支持的 action #### 支持的 action

18
ruleset/builtins/cidr.go Normal file
View File

@ -0,0 +1,18 @@
package builtins
import (
"net"
)
func MatchCIDR(ip string, cidr *net.IPNet) bool {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return false
}
return cidr.Contains(ipAddr)
}
func CompileCIDR(cidr string) (*net.IPNet, error) {
_, ipNet, err := net.ParseCIDR(cidr)
return ipNet, err
}

View File

@ -2,6 +2,7 @@ package ruleset
import ( import (
"fmt" "fmt"
"net"
"os" "os"
"reflect" "reflect"
"strings" "strings"
@ -14,6 +15,7 @@ import (
"github.com/apernet/OpenGFW/analyzer" "github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/modifier" "github.com/apernet/OpenGFW/modifier"
"github.com/apernet/OpenGFW/ruleset/builtins"
"github.com/apernet/OpenGFW/ruleset/builtins/geo" "github.com/apernet/OpenGFW/ruleset/builtins/geo"
) )
@ -100,17 +102,21 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
return nil, fmt.Errorf("rule %q has invalid action %q", rule.Name, rule.Action) return nil, fmt.Errorf("rule %q has invalid action %q", rule.Name, rule.Action)
} }
visitor := &idVisitor{Identifiers: make(map[string]bool)} visitor := &idVisitor{Identifiers: make(map[string]bool)}
patcher := &idPatcher{}
program, err := expr.Compile(rule.Expr, program, err := expr.Compile(rule.Expr,
func(c *conf.Config) { func(c *conf.Config) {
c.Strict = false c.Strict = false
c.Expect = reflect.Bool c.Expect = reflect.Bool
c.Visitors = append(c.Visitors, visitor) c.Visitors = append(c.Visitors, visitor, patcher)
registerBuiltinFunctions(c.Functions, geoMatcher) registerBuiltinFunctions(c.Functions, geoMatcher)
}, },
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("rule %q has invalid expression: %w", rule.Name, err) 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 { for name := range visitor.Identifiers {
if isBuiltInAnalyzer(name) { if isBuiltInAnalyzer(name) {
continue continue
@ -126,6 +132,8 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
if err := geoMatcher.LoadGeoSite(); err != nil { if err := geoMatcher.LoadGeoSite(); err != nil {
return nil, fmt.Errorf("rule %q failed to load geosite: %w", rule.Name, err) return nil, fmt.Errorf("rule %q failed to load geosite: %w", rule.Name, err)
} }
case "cidr":
// No initialization needed for CIDR.
default: default:
a, ok := fullAnMap[name] a, ok := fullAnMap[name]
if !ok { if !ok {
@ -179,6 +187,13 @@ func registerBuiltinFunctions(funcMap map[string]*ast.Function, geoMatcher *geo.
}, },
Types: []reflect.Type{reflect.TypeOf(geoMatcher.MatchGeoSite)}, Types: []reflect.Type{reflect.TypeOf(geoMatcher.MatchGeoSite)},
} }
funcMap["cidr"] = &ast.Function{
Name: "cidr",
Func: func(params ...any) (any, error) {
return builtins.MatchCIDR(params[0].(string), params[1].(*net.IPNet)), nil
},
Types: []reflect.Type{reflect.TypeOf((func(string, string) bool)(nil)), reflect.TypeOf(builtins.MatchCIDR)},
}
} }
func streamInfoToExprEnv(info StreamInfo) map[string]interface{} { func streamInfoToExprEnv(info StreamInfo) map[string]interface{} {
@ -247,6 +262,8 @@ func modifiersToMap(mods []modifier.Modifier) map[string]modifier.Modifier {
return modMap 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 { type idVisitor struct {
Identifiers map[string]bool Identifiers map[string]bool
} }
@ -256,3 +273,29 @@ func (v *idVisitor) Visit(node *ast.Node) {
v.Identifiers[idNode.Value] = true 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 {
Err error
}
func (p *idPatcher) Visit(node *ast.Node) {
switch (*node).(type) {
case *ast.CallNode:
callNode := (*node).(*ast.CallNode)
switch callNode.Func.Name {
case "cidr":
cidrStringNode, ok := callNode.Arguments[1].(*ast.StringNode)
if !ok {
return
}
cidr, err := builtins.CompileCIDR(cidrStringNode.Value)
if err != nil {
p.Err = err
return
}
callNode.Arguments[1] = &ast.ConstantNode{Value: cidr}
}
}
}