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 }