packagepacketloggerimport("context""fmt""log/slog""net""time""honeypot/internal/database""honeypot/internal/honeypot""honeypot/internal/logger""honeypot/internal/types""honeypot/internal/utils""github.com/google/gopacket""github.com/google/gopacket/layers""github.com/google/gopacket/pcap")const(HoneypotType=types.HoneypotTypePacketLoggerHoneypotLabel="Packet"DefaultSnapshotLen=1600ShutdownTimeout=1*time.Second)// Config holds the configuration for the packet logger.typeConfigstruct{InterfacestringBpfExpressionstring}// packetLoggerHoneypot implements the honeypot.Honeypot interface.typepacketLoggerHoneypotstruct{configConfiglogger*slog.Logger}// New creates a new packet logger honeypot instance.funcNew(cfgConfig)honeypot.Honeypot{return&packetLoggerHoneypot{config:cfg,}}// Name returns the name of this honeypot.func(h*packetLoggerHoneypot)Name()types.HoneypotType{returnHoneypotType}// Label returns the label of this honeypot.func(h*packetLoggerHoneypot)Label()string{returnHoneypotLabel}// Start starts the packet logger.func(h*packetLoggerHoneypot)Start(ctxcontext.Context,l*slog.Logger)error{h.logger=liferr:=h.validateInterface();err!=nil{returnerr}ifaceIP,err:=h.getInterfaceIP()iferr!=nil{returnerr}handle,err:=h.openPacketCapture()iferr!=nil{returnerr}deferhandle.Close()bpfFilter:=h.buildBPFFilter(ifaceIP)iferr:=handle.SetBPFFilter(bpfFilter);err!=nil{logger.LogError(h.logger,HoneypotType,"set_bpf_filter_failed",err,[]any{"bpf_expression",bpfFilter,})returnerr}logger.LogInfo(h.logger,HoneypotType,"honeypot listening",[]any{"interface",h.config.Interface,"bpf_expression",bpfFilter,})h.setupGracefulShutdown(ctx,handle)h.processPackets(ctx,handle)logger.LogInfo(h.logger,HoneypotType,"honeypot shutdown complete",nil)returnnil}// validateInterface checks if the configured interface exists.func(h*packetLoggerHoneypot)validateInterface()error{devices,err:=pcap.FindAllDevs()iferr!=nil{logger.LogError(h.logger,HoneypotType,"find_devices_failed",err,nil)returnerr}for_,d:=rangedevices{ifd.Name==h.config.Interface{returnnil}}err=fmt.Errorf("interface %s not found",h.config.Interface)logger.LogError(h.logger,HoneypotType,"interface_not_found",err,[]any{"interface",h.config.Interface,})returnerr}// getInterfaceIP extracts the IPv4 address from the configured interface.func(h*packetLoggerHoneypot)getInterfaceIP()(net.IP,error){iface,err:=net.InterfaceByName(h.config.Interface)iferr!=nil{logger.LogError(h.logger,HoneypotType,"get_interface_failed",err,[]any{"interface",h.config.Interface,})returnnil,err}addrs,err:=iface.Addrs()iferr!=nil{logger.LogError(h.logger,HoneypotType,"get_interface_addrs_failed",err,[]any{"interface",iface.Name,})returnnil,err}for_,addr:=rangeaddrs{ifipNet,ok:=addr.(*net.IPNet);ok&&ipNet.IP.To4()!=nil{returnipNet.IP,nil}}err=fmt.Errorf("no IPv4 address found on interface %s",iface.Name)logger.LogError(h.logger,HoneypotType,"get_iface_ip_failed",err,[]any{"interface",iface.Name,})returnnil,err}// openPacketCapture opens a live packet capture handle.func(h*packetLoggerHoneypot)openPacketCapture()(*pcap.Handle,error){handle,err:=pcap.OpenLive(h.config.Interface,DefaultSnapshotLen,true,pcap.BlockForever)iferr!=nil{logger.LogError(h.logger,HoneypotType,"open_live_failed",err,[]any{"interface",h.config.Interface,})returnnil,err}returnhandle,nil}// buildBPFFilter constructs the BPF filter expression.func(h*packetLoggerHoneypot)buildBPFFilter(ifaceIPnet.IP)string{bpfSrcHost:=fmt.Sprintf("not src host %s",ifaceIP.String())bpfSyn:="tcp[tcpflags] & tcp-syn != 0 or udp or icmp[0] == 8"bpfDNSResponse:="not src port 53"// exclude DNS responses from dns lookupsbpf:=fmt.Sprintf("(%s) and (%s) and (%s)",bpfSrcHost,bpfSyn,bpfDNSResponse)ifh.config.BpfExpression!=""{bpf=fmt.Sprintf("(%s) and (%s)",bpf,h.config.BpfExpression)}returnbpf}// setupGracefulShutdown handles graceful shutdown on context cancellation.func(h*packetLoggerHoneypot)setupGracefulShutdown(ctxcontext.Context,handle*pcap.Handle){gofunc(){<-ctx.Done()handle.Close()}()}// processPackets processes incoming packets and logs relevant events.func(h*packetLoggerHoneypot)processPackets(ctxcontext.Context,handle*pcap.Handle){packetSource:=gopacket.NewPacketSource(handle,handle.LinkType())forpacket:=rangepacketSource.Packets(){ifctx.Err()!=nil{break}h.handlePacket(packet)}}// handlePacket processes a single packet and logs relevant events.func(h*packetLoggerHoneypot)handlePacket(packetgopacket.Packet){ipLayer:=packet.Layer(layers.LayerTypeIPv4)ifipLayer==nil{return}ip,ok:=ipLayer.(*layers.IPv4)if!ok{return}iftcpLayer:=packet.Layer(layers.LayerTypeTCP);tcpLayer!=nil{iftcp,ok:=tcpLayer.(*layers.TCP);ok{h.handleTCPPacket(ip,tcp)}}ifudpLayer:=packet.Layer(layers.LayerTypeUDP);udpLayer!=nil{ifudp,ok:=udpLayer.(*layers.UDP);ok{h.handleUDPPacket(ip,udp)}}ificmpLayer:=packet.Layer(layers.LayerTypeICMPv4);icmpLayer!=nil{ificmp,ok:=icmpLayer.(*layers.ICMPv4);ok{h.handleICMPPacket(ip,icmp)}}}// handleTCPPacket processes TCP packets and logs SYN packets.func(h*packetLoggerHoneypot)handleTCPPacket(ip*layers.IPv4,tcp*layers.TCP){iftcp.SYN&&!tcp.ACK{remotePort:=utils.SanitizePort(tcp.SrcPort.String(),h.logger)dstPort:=utils.SanitizePort(tcp.DstPort.String(),h.logger)logger.LogEvent(h.logger,types.LogEvent{Type:HoneypotType,Event:types.EventTCPPacket,RemoteAddr:ip.SrcIP.String(),RemotePort:remotePort,DstPort:dstPort,})}}// handleUDPPacket processes UDP packets and logs UDP packets.func(h*packetLoggerHoneypot)handleUDPPacket(ip*layers.IPv4,udp*layers.UDP){remotePort:=utils.SanitizePort(udp.SrcPort.String(),h.logger)dstPort:=utils.SanitizePort(udp.DstPort.String(),h.logger)logger.LogEvent(h.logger,types.LogEvent{Type:HoneypotType,Event:types.EventUDPPacket,RemoteAddr:ip.SrcIP.String(),RemotePort:remotePort,DstPort:dstPort,})}// handleICMPPacket processes ICMP packets and logs echo requests.func(h*packetLoggerHoneypot)handleICMPPacket(ip*layers.IPv4,icmp*layers.ICMPv4){ificmp.TypeCode.Type()==8{// 8 is echo requestlogger.LogEvent(h.logger,types.LogEvent{Type:HoneypotType,Event:types.EventICMPPacket,RemoteAddr:ip.SrcIP.String(),})}}func(h*packetLoggerHoneypot)GetScores(db*database.Database,intervalstring)honeypot.ScoreMap{// get scores for addresses with more than 10 packets on more than 5 different ports in 12 hoursquery:=` SELECT remote_addr, COUNT(DISTINCT dst_port) as port_count, COUNT(*) AS packet_count
FROM honeypot_events
WHERE type = 'packetlogger' AND time >= now() - INTERVAL '12 HOURS'
GROUP BY remote_addr HAVING COUNT(DISTINCT dst_port) >= 5 AND COUNT(*) >= 10
`rows,err:=db.DB.Query(query)iferr!=nil{returnhoneypot.ScoreMap{}}deferrows.Close()portScanScores:=honeypot.ScoreMap{}forrows.Next(){varipstringvarportCountuintvarpacketCountuinterr:=rows.Scan(&ip,&portCount,&packetCount)iferr!=nil{returnhoneypot.ScoreMap{}}portScanScores[ip]=honeypot.Score{Score:portCount*50,Tags:[]types.Tag{types.TagPortScan}}}//get scores for addresses with 9 or more pings on more than 1 minuteconstcountThreshold=9constbucketSize="60 seconds"query=fmt.Sprintf(` WITH raw_counts AS (
SELECT
time,
remote_addr,
COUNT(*) OVER (
ORDER BY time
RANGE BETWEEN INTERVAL %s PRECEDING AND CURRENT ROW
) as rolling_count
FROM honeypot_events
WHERE event = 'icmp_packet' AND time >= now() - INTERVAL %s
),
peaks AS (
SELECT
time,
remote_addr,
rolling_count,
CASE
WHEN time - LAG(time) OVER (ORDER BY time) <= INTERVAL '10 seconds'
THEN 0
ELSE 1
END AS is_new_event
FROM raw_counts
WHERE rolling_count >= %d
),
peak_groups AS (
SELECT
time,
remote_addr,
SUM(is_new_event) OVER (ORDER BY time) AS group_id
FROM peaks
),
scan_windows AS (
SELECT
group_id,
MIN(time) AS scan_start,
MAX(time) AS scan_end,
COUNT(*) AS peak_packets_count,
list(DISTINCT remote_addr) AS unique_ips
FROM peak_groups
GROUP BY group_id
)
SELECT
scan_start,
scan_end,
(
SELECT COUNT(*)
FROM honeypot_events
WHERE event = 'icmp_packet'
AND time >= s.scan_start - INTERVAL %s
AND time <= s.scan_end
) AS total_involved_packets,
unique_ips
FROM scan_windows s
ORDER BY scan_start DESC
`,interval,bucketSize,countThreshold,bucketSize)rows,err=db.DB.Query(query)iferr!=nil{returnhoneypot.ScoreMap{}}deferrows.Close()pingScores:=[]honeypot.ScoreMap{}forrows.Next(){varscanStarttime.TimevarscanEndtime.TimevartotalInvolvedPacketsuintvaruniqueIps[]interface{}err:=rows.Scan(&scanStart,&scanEnd,&totalInvolvedPackets,&uniqueIps)iferr!=nil{returnhoneypot.ScoreMap{}}for_,ip:=rangeuniqueIps{pingScores=append(pingScores,honeypot.ScoreMap{ip.(string):honeypot.Score{Score:totalInvolvedPackets*50,Tags:[]types.Tag{types.TagPingScan}},})}}pingScoresMap:=honeypot.MergeScores(pingScores...)// get high traffic ipsquery=fmt.Sprintf(` SELECT remote_addr, COUNT(*) as packet_count
FROM honeypot_events
WHERE type = 'packetlogger' AND time >= now() - INTERVAL %s
GROUP BY remote_addr HAVING COUNT(*) >= 200
ORDER BY packet_count DESC
`,interval)rows,err=db.DB.Query(query)iferr!=nil{returnhoneypot.ScoreMap{}}deferrows.Close()highTrafficScores:=honeypot.ScoreMap{}forrows.Next(){varipstringvarpacketCountuinterr:=rows.Scan(&ip,&packetCount)iferr!=nil{returnhoneypot.ScoreMap{}}highTrafficScores[ip]=honeypot.Score{Score:packetCount,Tags:[]types.Tag{types.TagHighTraffic}}}// get botnet IPs by subnet (/18, /20, /22, /24) if there are more than 50 events// in the subnet and more than 5 ports used in the subnetquery=fmt.Sprintf(`WITH filtered_ips AS (
SELECT
remote_ip_int,
COUNT(*) AS ip_count,
COUNT(DISTINCT dst_port) AS port_count
FROM honeypot_events
WHERE type = 'packetlogger'
AND time >= now() - INTERVAL %s
GROUP BY remote_ip_int
),
subnet_stats AS (
SELECT
mask,
(remote_ip_int >> (32 - mask)) AS subnet,
COUNT(*) AS addr_count,
SUM(ip_count) AS event_count,
SUM(port_count) AS port_count
FROM filtered_ips
CROSS JOIN (VALUES (18), (20), (22), (24)) AS masks(mask)
GROUP BY mask, subnet
HAVING COUNT(*) >= 5 AND SUM(ip_count) * SUM(port_count) >= 50
)
SELECT
f.remote_ip_int,
s.mask,
s.event_count,
s.port_count
FROM filtered_ips f
JOIN subnet_stats s
ON (f.remote_ip_int >> (32 - s.mask)) = s.subnet
`,interval)rows,err=db.DB.Query(query)iferr!=nil{returnhoneypot.ScoreMap{}}deferrows.Close()typeAddressBotnetScorestruct{ipIntuint32packetCountuintportCountuintmaskint}scores:=[]AddressBotnetScore{}forrows.Next(){varipIntuint32varmaskintvarpacketCountuintvarportCountuintiferr:=rows.Scan(&ipInt,&mask,&packetCount,&portCount);err!=nil{returnhoneypot.ScoreMap{}}scores=append(scores,AddressBotnetScore{ipInt:ipInt,mask:mask,packetCount:packetCount,portCount:portCount,})}temp:=honeypot.ScoreMap{}for_,s:=rangescores{ip:=utils.IntToIP(s.ipInt)subnet,err:=maskIP(ip,s.mask)iferr!=nil{continue}temp[subnet]=honeypot.Score{Score:s.packetCount*s.portCount,Tags:[]types.Tag{types.TagBotnet},}}botnetScores:=filterSubnets(temp)returnhoneypot.MergeScores(portScanScores,pingScoresMap,highTrafficScores,botnetScores,)}// filterSubnets removes subnets that contain other subnets in the map,// keeping only the most specific (smallest) subnets.funcfilterSubnets(scoreshoneypot.ScoreMap)honeypot.ScoreMap{result:=honeypot.ScoreMap{}forsubnet,score:=rangescores{isGeneral:=falseforexistingSubnet:=rangeresult{ifNetIncludesNet(subnet,existingSubnet){// The new subnet is more general than an existing one.isGeneral=truebreak}}if!isGeneral{// Remove any existing subnets that are more general than the new oneforexistingSubnet:=rangeresult{ifNetIncludesNet(existingSubnet,subnet){delete(result,existingSubnet)}}result[subnet]=score}}returnresult}// MaskIP returns the network address for an IP and mask size.// Example: MaskIP("192.168.156.123", 16) -> "192.168.0.0"funcmaskIP(ipStrstring,maskBitsint)(string,error){ip:=net.ParseIP(ipStr)ifip==nil{return"",fmt.Errorf("invalid IP address: %s",ipStr)}ip=ip.To4()ifip==nil{return"",fmt.Errorf("only IPv4 is supported")}mask:=net.CIDRMask(maskBits,32)network:=ip.Mask(mask)returnfmt.Sprintf("%s/%d",network.String(),maskBits),nil}// NetIncludesNet checks if one network includes another.// Example: NetIncludesNet("192.168.0.0/16", "192.168.156.123/32") -> truefuncNetIncludesNet(net1string,net2string)bool{_,ip1Net,err:=net.ParseCIDR(net1)iferr!=nil{returnfalse}_,ip2Net,err:=net.ParseCIDR(net2)iferr!=nil{returnfalse}mask1,_:=ip1Net.Mask.Size()mask2,_:=ip2Net.Mask.Size()returnmask1<=mask2&&ip1Net.Contains(ip2Net.IP)}func(h*packetLoggerHoneypot)Ports()[]uint16{returnnil}