Files
tendrils/broadcast.go
Ian Gulliver bbd938b924 Add broadcast packet tracking with rate monitoring
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:40:39 -08:00

210 lines
4.3 KiB
Go

package tendrils
import (
"context"
"log"
"net"
"sync"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/pcap"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
type BroadcastSample struct {
Time time.Time
Packets uint64
Bytes uint64
}
type BroadcastStats struct {
mu sync.RWMutex
samples []BroadcastSample
totalPackets uint64
totalBytes uint64
windowSize time.Duration
lastNotify time.Time
notifyMinRate time.Duration
t *Tendrils
}
type BroadcastStatsResponse struct {
TotalPackets uint64 `json:"total_packets"`
TotalBytes uint64 `json:"total_bytes"`
PacketsPerS float64 `json:"packets_per_s"`
BytesPerS float64 `json:"bytes_per_s"`
WindowSecs float64 `json:"window_secs"`
}
func NewBroadcastStats(t *Tendrils) *BroadcastStats {
return &BroadcastStats{
samples: []BroadcastSample{},
windowSize: 60 * time.Second,
notifyMinRate: 1 * time.Second,
t: t,
}
}
func (b *BroadcastStats) Record(packets, bytes uint64) {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
b.samples = append(b.samples, BroadcastSample{
Time: now,
Packets: packets,
Bytes: bytes,
})
b.totalPackets += packets
b.totalBytes += bytes
cutoff := now.Add(-b.windowSize)
for len(b.samples) > 0 && b.samples[0].Time.Before(cutoff) {
b.samples = b.samples[1:]
}
if now.Sub(b.lastNotify) >= b.notifyMinRate {
b.lastNotify = now
b.t.NotifyUpdate()
}
}
func (b *BroadcastStats) GetStats() BroadcastStatsResponse {
b.mu.RLock()
defer b.mu.RUnlock()
now := time.Now()
cutoff := now.Add(-b.windowSize)
var windowPackets, windowBytes uint64
var oldestTime time.Time
for _, s := range b.samples {
if s.Time.After(cutoff) {
if oldestTime.IsZero() || s.Time.Before(oldestTime) {
oldestTime = s.Time
}
windowPackets += s.Packets
windowBytes += s.Bytes
}
}
var windowSecs float64
if !oldestTime.IsZero() {
windowSecs = now.Sub(oldestTime).Seconds()
}
if windowSecs < 1 {
windowSecs = 1
}
return BroadcastStatsResponse{
TotalPackets: b.totalPackets,
TotalBytes: b.totalBytes,
PacketsPerS: float64(windowPackets) / windowSecs,
BytesPerS: float64(windowBytes) / windowSecs,
WindowSecs: windowSecs,
}
}
func (t *Tendrils) listenBroadcast(ctx context.Context, iface net.Interface) {
handle, err := pcap.OpenLive(iface.Name, 65536, true, 5*time.Second)
if err != nil {
log.Printf("[ERROR] broadcast: failed to open interface %s: %v", iface.Name, err)
return
}
defer handle.Close()
if err := handle.SetBPFFilter("ether broadcast"); err != nil {
log.Printf("[ERROR] broadcast: failed to set BPF filter on %s: %v", iface.Name, err)
return
}
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
packets := packetSource.Packets()
for {
select {
case <-ctx.Done():
return
case packet, ok := <-packets:
if !ok {
return
}
t.handleBroadcastPacket(packet)
}
}
}
func (t *Tendrils) handleBroadcastPacket(packet gopacket.Packet) {
if t.broadcast == nil {
return
}
packetLen := uint64(len(packet.Data()))
t.broadcast.Record(1, packetLen)
if t.DebugBroadcast {
log.Printf("[broadcast] packet: %d bytes", packetLen)
}
}
func (t *Tendrils) pingBroadcast(ctx context.Context, iface net.Interface) {
_, broadcast := getInterfaceIPv4(iface)
if broadcast == nil {
return
}
t.sendBroadcastPing(broadcast, iface.Name)
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
t.sendBroadcastPing(broadcast, iface.Name)
}
}
}
func (t *Tendrils) sendBroadcastPing(broadcast net.IP, ifaceName string) {
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
if t.DebugARP {
log.Printf("[broadcast] %s: failed to create icmp socket: %v", ifaceName, err)
}
return
}
defer conn.Close()
msg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: 1,
Seq: 1,
Data: []byte("tendrils"),
},
}
msgBytes, err := msg.Marshal(nil)
if err != nil {
return
}
_, err = conn.WriteTo(msgBytes, &net.IPAddr{IP: broadcast})
if err != nil {
if t.DebugARP {
log.Printf("[broadcast] %s: failed to send ping to %s: %v", ifaceName, broadcast, err)
}
return
}
if t.DebugARP {
log.Printf("[broadcast] %s: sent ping to %s", ifaceName, broadcast)
}
}