Improves flow handling and adds runtime stats APIs
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.
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer"
|
||||
)
|
||||
|
||||
type analyzerSelector struct {
|
||||
mode AnalyzerSelectionMode
|
||||
stats *statsCounters
|
||||
}
|
||||
|
||||
func newAnalyzerSelector(mode AnalyzerSelectionMode, stats *statsCounters) *analyzerSelector {
|
||||
if mode == "" {
|
||||
mode = AnalyzerSelectionModeSignature
|
||||
}
|
||||
return &analyzerSelector{mode: mode, stats: stats}
|
||||
}
|
||||
|
||||
func (s *analyzerSelector) SelectTCP(ans []analyzer.Analyzer, payload []byte) []analyzer.Analyzer {
|
||||
if s == nil || s.mode == AnalyzerSelectionModeAlways || len(ans) <= 1 {
|
||||
return ans
|
||||
}
|
||||
allowed := tcpAllowedAnalyzers(payload)
|
||||
if len(allowed) == 0 {
|
||||
return ans
|
||||
}
|
||||
out := make([]analyzer.Analyzer, 0, len(ans))
|
||||
for _, a := range ans {
|
||||
name := strings.ToLower(a.Name())
|
||||
if _, known := knownTCPAnalyzers[name]; !known {
|
||||
out = append(out, a)
|
||||
continue
|
||||
}
|
||||
if allowed[name] {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
s.recordSelection(len(ans), len(out))
|
||||
if len(out) == 0 {
|
||||
return ans
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *analyzerSelector) SelectUDP(ans []analyzer.Analyzer, payload []byte) []analyzer.Analyzer {
|
||||
if s == nil || s.mode == AnalyzerSelectionModeAlways || len(ans) <= 1 {
|
||||
return ans
|
||||
}
|
||||
allowed := udpAllowedAnalyzers(payload)
|
||||
if len(allowed) == 0 {
|
||||
return ans
|
||||
}
|
||||
out := make([]analyzer.Analyzer, 0, len(ans))
|
||||
for _, a := range ans {
|
||||
name := strings.ToLower(a.Name())
|
||||
if _, known := knownUDPAnalyzers[name]; !known {
|
||||
out = append(out, a)
|
||||
continue
|
||||
}
|
||||
if allowed[name] {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
s.recordSelection(len(ans), len(out))
|
||||
if len(out) == 0 {
|
||||
return ans
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *analyzerSelector) recordSelection(total, selected int) {
|
||||
if s == nil || s.stats == nil || total <= 0 {
|
||||
return
|
||||
}
|
||||
s.stats.AnalyzerSelectionsTotal.Add(1)
|
||||
if selected < total {
|
||||
s.stats.AnalyzerSelectionsPruned.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
knownTCPAnalyzers = map[string]struct{}{
|
||||
"fet": {},
|
||||
"http": {},
|
||||
"socks": {},
|
||||
"ssh": {},
|
||||
"tls": {},
|
||||
"trojan": {},
|
||||
"dns": {},
|
||||
"openvpn": {},
|
||||
}
|
||||
knownUDPAnalyzers = map[string]struct{}{
|
||||
"dns": {},
|
||||
"openvpn": {},
|
||||
"quic": {},
|
||||
"wireguard": {},
|
||||
}
|
||||
)
|
||||
|
||||
func tcpAllowedAnalyzers(payload []byte) map[string]bool {
|
||||
allowed := make(map[string]bool, 4)
|
||||
if looksLikeTLS(payload) {
|
||||
allowed["tls"] = true
|
||||
allowed["trojan"] = true
|
||||
allowed["fet"] = true
|
||||
}
|
||||
if looksLikeHTTP(payload) {
|
||||
allowed["http"] = true
|
||||
allowed["fet"] = true
|
||||
}
|
||||
if looksLikeSSH(payload) {
|
||||
allowed["ssh"] = true
|
||||
allowed["fet"] = true
|
||||
}
|
||||
if looksLikeSOCKS(payload) {
|
||||
allowed["socks"] = true
|
||||
allowed["fet"] = true
|
||||
}
|
||||
if looksLikeDNSTCP(payload) {
|
||||
allowed["dns"] = true
|
||||
allowed["fet"] = true
|
||||
}
|
||||
if len(allowed) == 0 {
|
||||
return nil
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
func udpAllowedAnalyzers(payload []byte) map[string]bool {
|
||||
allowed := make(map[string]bool, 4)
|
||||
if looksLikeWireGuard(payload) {
|
||||
allowed["wireguard"] = true
|
||||
}
|
||||
if looksLikeOpenVPN(payload) {
|
||||
allowed["openvpn"] = true
|
||||
}
|
||||
if looksLikeQUIC(payload) {
|
||||
allowed["quic"] = true
|
||||
}
|
||||
if looksLikeDNSUDP(payload) {
|
||||
allowed["dns"] = true
|
||||
}
|
||||
if len(allowed) == 0 {
|
||||
return nil
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
func looksLikeTLS(payload []byte) bool {
|
||||
if len(payload) < 3 {
|
||||
return false
|
||||
}
|
||||
return (payload[0] == 0x16 || payload[0] == 0x17) && payload[1] == 0x03 && payload[2] <= 0x09
|
||||
}
|
||||
|
||||
func looksLikeHTTP(payload []byte) bool {
|
||||
if len(payload) < 3 {
|
||||
return false
|
||||
}
|
||||
head := strings.ToUpper(string(payload[:3]))
|
||||
switch head {
|
||||
case "GET", "HEA", "POS", "PUT", "DEL", "CON", "OPT", "TRA", "PAT":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func looksLikeSSH(payload []byte) bool {
|
||||
return len(payload) >= 4 && bytes.HasPrefix(payload, []byte("SSH-"))
|
||||
}
|
||||
|
||||
func looksLikeSOCKS(payload []byte) bool {
|
||||
if len(payload) < 2 {
|
||||
return false
|
||||
}
|
||||
return payload[0] == 0x04 || payload[0] == 0x05
|
||||
}
|
||||
|
||||
func looksLikeDNSTCP(payload []byte) bool {
|
||||
if len(payload) < 14 {
|
||||
return false
|
||||
}
|
||||
msgLen := int(payload[0])<<8 | int(payload[1])
|
||||
if msgLen <= 0 || msgLen+2 > len(payload) {
|
||||
return false
|
||||
}
|
||||
qd := int(payload[6])<<8 | int(payload[7])
|
||||
an := int(payload[8])<<8 | int(payload[9])
|
||||
return qd+an > 0
|
||||
}
|
||||
|
||||
func looksLikeDNSUDP(payload []byte) bool {
|
||||
if len(payload) < 12 {
|
||||
return false
|
||||
}
|
||||
qd := int(payload[4])<<8 | int(payload[5])
|
||||
an := int(payload[6])<<8 | int(payload[7])
|
||||
ns := int(payload[8])<<8 | int(payload[9])
|
||||
ar := int(payload[10])<<8 | int(payload[11])
|
||||
return qd+an+ns+ar > 0
|
||||
}
|
||||
|
||||
func looksLikeQUIC(payload []byte) bool {
|
||||
if len(payload) < 6 {
|
||||
return false
|
||||
}
|
||||
// Long header with non-zero version.
|
||||
if payload[0]&0x80 == 0 {
|
||||
return false
|
||||
}
|
||||
version := uint32(payload[1])<<24 | uint32(payload[2])<<16 | uint32(payload[3])<<8 | uint32(payload[4])
|
||||
return version != 0
|
||||
}
|
||||
|
||||
func looksLikeOpenVPN(payload []byte) bool {
|
||||
if len(payload) == 0 {
|
||||
return false
|
||||
}
|
||||
opcode := payload[0] >> 3
|
||||
return opcode >= 1 && opcode <= 11
|
||||
}
|
||||
|
||||
func looksLikeWireGuard(payload []byte) bool {
|
||||
if len(payload) < 4 {
|
||||
return false
|
||||
}
|
||||
if payload[0] < 1 || payload[0] > 4 {
|
||||
return false
|
||||
}
|
||||
return payload[1] == 0 && payload[2] == 0 && payload[3] == 0
|
||||
}
|
||||
Reference in New Issue
Block a user