90 lines
2.3 KiB
Go
90 lines
2.3 KiB
Go
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)
|
|
}
|
|
}
|