Count traffic per client address and report the busiest ones with the port totals
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b46e0813fe
commit
ed9edbc9fd
7 files changed
+163
-17
No files matched your search
+89
@@ -0,0 +1,89 @@
|
||||
package blacktip
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"git.veillax.com/Veillax/reef/model"
|
||||
)
|
||||
|
||||
// maxTrackedSources bounds the per-port table of client addresses between two reports, so a flood
|
||||
// from spoofed or scanning addresses can't grow it without limit. Addresses past it still count
|
||||
// in the port's totals; they just aren't named.
|
||||
const maxTrackedSources = 5000
|
||||
|
||||
// sourceTable counts a port's traffic per client address until the next report takes it.
|
||||
type sourceTable struct {
|
||||
mu sync.Mutex
|
||||
by map[string]*model.SourceStat
|
||||
}
|
||||
|
||||
func (t *sourceTable) add(ip string, conns, in, out int64) {
|
||||
if ip == "" {
|
||||
return
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
s := t.by[ip]
|
||||
if s == nil {
|
||||
if len(t.by) >= maxTrackedSources {
|
||||
return
|
||||
}
|
||||
if t.by == nil {
|
||||
t.by = map[string]*model.SourceStat{}
|
||||
}
|
||||
s = &model.SourceStat{IP: ip}
|
||||
t.by[ip] = s
|
||||
}
|
||||
s.Conns += conns
|
||||
s.BytesIn += in
|
||||
s.BytesOut += out
|
||||
}
|
||||
|
||||
func (t *sourceTable) take() map[string]*model.SourceStat {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
by := t.by
|
||||
t.by = nil
|
||||
return by
|
||||
}
|
||||
|
||||
// clientIP reduces a "host:port" remote address to the client's IP (IPv4-mapped IPv6 as plain IPv4).
|
||||
// Anything that isn't an address gives "".
|
||||
func clientIP(remote string) string {
|
||||
host, _, err := net.SplitHostPort(remote)
|
||||
if err != nil {
|
||||
host = remote
|
||||
}
|
||||
a, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return a.WithZone("").Unmap().String()
|
||||
}
|
||||
|
||||
// record counts traffic on a port, and names the client it came from when that is known.
|
||||
func (ms *mapState) record(remote string, conns, in, out int64) {
|
||||
ms.conns.Add(conns)
|
||||
ms.bytesIn.Add(in)
|
||||
ms.bytesOut.Add(out)
|
||||
ms.sources.add(clientIP(remote), conns, in, out)
|
||||
}
|
||||
|
||||
// takeStats hands over (and resets) the traffic counted since the last call.
|
||||
func (ms *mapState) takeStats() (model.Stat, bool) {
|
||||
st := model.Stat{Conns: ms.conns.Swap(0), BytesIn: ms.bytesIn.Swap(0), BytesOut: ms.bytesOut.Swap(0)}
|
||||
st.Sources = model.TopSources(ms.sources.take(), model.MaxSources)
|
||||
return st, st.Conns|st.BytesIn|st.BytesOut != 0
|
||||
}
|
||||
|
||||
// restoreStats puts taken traffic back after a report that didn't arrive, so none of it is lost.
|
||||
func (ms *mapState) restoreStats(st model.Stat) {
|
||||
ms.conns.Add(st.Conns)
|
||||
ms.bytesIn.Add(st.BytesIn)
|
||||
ms.bytesOut.Add(st.BytesOut)
|
||||
for _, s := range st.Sources {
|
||||
ms.sources.add(s.IP, s.Conns, s.BytesIn, s.BytesOut)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user