Files
OpenGFW/ruleset/expr_test.go
T

77 lines
3.4 KiB
Go
Raw Normal View History

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) {}