77 lines
3.4 KiB
Go
77 lines
3.4 KiB
Go
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) {}
|