210 lines
4.3 KiB
Go
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)
|
|
}
|
|
}
|