package ruleset import ( "context" "net" "strings" "testing" "github.com/apernet/OpenGFW/analyzer" "github.com/apernet/OpenGFW/analyzer/tcp" "github.com/apernet/OpenGFW/analyzer/udp" "github.com/apernet/OpenGFW/modifier" modUDP "github.com/apernet/OpenGFW/modifier/udp" ) // builderExpressions are the canonical expressions produced by the visual rule // builder of the web UI (see web/frontend/src/lib/rule/compile.ts). They are // pinned here so that a change to the expression language, the analyzers or the // built-in functions cannot silently break the builder. var builderExpressions = []string{ // Domain or subdomain, single and multiple values `(string(tls?.req?.sni) == "example.com" || string(tls?.req?.sni) endsWith ".example.com")`, `((string(tls?.req?.sni) == "example.com" || string(tls?.req?.sni) endsWith ".example.com") || (string(tls?.req?.sni) == "evil.test" || string(tls?.req?.sni) endsWith ".evil.test")) && proto == "tcp"`, // CIDR and GeoIP, including negation `(cidr(ip.dst, "10.0.0.0/8") || cidr(ip.dst, "fd00::/8")) || !(geoip(ip.dst, "cn") || geoip(ip.dst, "hk"))`, // Port equality and ranges `(port.dst >= 1000 && port.dst <= 2000) && (port.src == 80 || port.src == 443)`, // Wildcards over DNS questions `any(dns?.questions ?? [], {(string(.name) endsWith ".ads.com" || string(.name) startsWith "x.")})`, // GeoSite over DNS questions `any(dns?.questions ?? [], {geosite(string(.name), "category-ads-all")})`, // HTTP fields: negation, regular expressions and header lookups `!(string(http?.req?.headers?.host) contains "tracker") && string(http?.req?.path) matches "^/api/v\\d+/" && string(get(http?.req?.headers, "user-agent")) startsWith "curl"`, // Protocol detection `(ssh != nil || trojan != nil)`, // Wildcard edge cases: "any value" and a star in the middle `string(quic?.req?.sni) != "" && string(tls?.req?.sni) matches "^www\\..*\\.com$" && ip.src == "1.2.3.4" && string(http?.req?.method) == "POST"`, } func TestCompileBuilderExpressions(t *testing.T) { analyzers := []analyzer.Analyzer{ &tcp.FETAnalyzer{}, &tcp.HTTPAnalyzer{}, &tcp.SocksAnalyzer{}, &tcp.SSHAnalyzer{}, &tcp.TLSAnalyzer{}, &tcp.TrojanAnalyzer{}, &udp.DNSAnalyzer{}, &udp.OpenVPNAnalyzer{}, &udp.QUICAnalyzer{}, &udp.WireGuardAnalyzer{}, } modifiers := []modifier.Modifier{&modUDP.DNSModifier{}} config := &BuiltinConfig{ Logger: nopLogger{}, // Point at a file that does not exist: expressions still have to // compile, only loading the database is expected to fail. GeoSiteFilename: "testdata/missing-geosite.dat", GeoIpFilename: "testdata/missing-geoip.dat", ProtectedDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { return (&net.Dialer{}).DialContext(ctx, network, address) }, } for _, expr := range builderExpressions { rules := []ExprRule{{Name: "test", Action: "block", Expr: expr}} _, err := CompileExprRules(rules, analyzers, modifiers, config) if err == nil { continue } // geoip()/geosite() need a database, which this test does not ship. // Reaching the initialization step means the expression itself is fine. if strings.Contains(err.Error(), "failed to initialize function") { continue } t.Errorf("expression failed to compile: %s\n %v", expr, err) } } type nopLogger struct{} func (nopLogger) Log(StreamInfo, string) {} func (nopLogger) MatchError(StreamInfo, string, error) {}