Path: blob/dev/pkg/protocols/common/honeypotdetector/honeypotdetector.go
4538 views
package honeypotdetector12import (3"fmt"4"net"5"net/url"6"strings"7"sync"8)910// Detector tracks honeypot likelihood by counting distinct template matches per normalized host.11// Once a host reaches the configured threshold, it becomes flagged.12type Detector struct {13threshold int14hosts sync.Map // map[normalizedHost]*hostState15}1617type hostState struct {18mu sync.Mutex19templateIDs map[string]struct{}20flagged bool21}2223// New creates a new honeypot detector.24func New(threshold int) *Detector {25if threshold <= 0 {26threshold = 127}28return &Detector{29threshold: threshold,30}31}3233// Threshold returns the distinct template count required to flag a host.34func (d *Detector) Threshold() int {35if d == nil {36return 037}38return d.threshold39}4041// RecordMatch records a match for the given host and templateID.42//43// It returns true only when the host has just become flagged (i.e. crossed the threshold).44func (d *Detector) RecordMatch(host, templateID string) bool {45if d == nil {46return false47}4849normalizedHost := normalizeHostKey(host)50if normalizedHost == "" || templateID == "" {51return false52}5354stateAny, _ := d.hosts.LoadOrStore(normalizedHost, &hostState{55templateIDs: make(map[string]struct{}),56})57state := stateAny.(*hostState)5859state.mu.Lock()60defer state.mu.Unlock()6162if state.flagged {63return false64}65if _, ok := state.templateIDs[templateID]; ok {66return false67}6869state.templateIDs[templateID] = struct{}{}70if len(state.templateIDs) >= d.threshold {71state.flagged = true72state.templateIDs = nil73return true74}75return false76}7778// IsFlagged returns whether the given host is flagged.79func (d *Detector) IsFlagged(host string) bool {80if d == nil {81return false82}8384normalizedHost := normalizeHostKey(host)85if normalizedHost == "" {86return false87}8889stateAny, ok := d.hosts.Load(normalizedHost)90if !ok {91return false92}93state := stateAny.(*hostState)9495state.mu.Lock()96defer state.mu.Unlock()97return state.flagged98}99100// Summary returns a short string with the total number of flagged hosts.101func (d *Detector) Summary() string {102if d == nil {103return "honeypot-detected hosts: 0"104}105106var flagged int107d.hosts.Range(func(_, v any) bool {108state := v.(*hostState)109state.mu.Lock()110if state.flagged {111flagged++112}113state.mu.Unlock()114return true115})116117return fmt.Sprintf("honeypot-detected hosts: %d", flagged)118}119120// NormalizeHostKey normalizes host strings so different input formats map to the same key.121func NormalizeHostKey(input string) string {122return normalizeHostKey(input)123}124125func normalizeHostKey(input string) string {126s := strings.TrimSpace(input)127if s == "" {128return ""129}130131// Strip trailing slashes early.132s = strings.TrimRight(s, "/")133134// If an absolute URL is present, parse it to reliably extract host and optional port.135if strings.Contains(s, "://") {136u, err := url.Parse(s)137if err == nil && u != nil {138host := u.Hostname()139port := u.Port()140if host == "" {141return ""142}143host = normalizeHostWithoutPort(host)144if port != "" {145return net.JoinHostPort(host, port)146}147return host148}149// fall through if parsing fails150}151152// Remove any path suffix (we only care about the authority).153if idx := strings.IndexByte(s, '/'); idx >= 0 {154s = s[:idx]155}156157// If it looks like host:port (including bracketed IPv6), try SplitHostPort first.158if host, port, err := net.SplitHostPort(s); err == nil {159host = normalizeHostWithoutPort(host)160if port == "" {161return host162}163return net.JoinHostPort(host, port)164}165166// Handle bracketed IPv6 without port: [2001:db8::1]167if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") {168host := strings.TrimSuffix(strings.TrimPrefix(s, "["), "]")169return normalizeHostWithoutPort(host)170}171172// Handle bare IPv6 or host without port.173return normalizeHostWithoutPort(s)174}175176func normalizeHostWithoutPort(host string) string {177h := strings.TrimSpace(host)178if h == "" {179return ""180}181h = strings.TrimPrefix(h, "[")182h = strings.TrimSuffix(h, "]")183h = strings.ToLower(h)184185if ip := net.ParseIP(h); ip != nil {186return ip.String()187}188return h189}190191192