mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-24 13:29:45 +00:00
Adds, per node, how many distinct FLOOD adverts it originated in the last 7 days. Zero-hop adverts (route_type DIRECT) are excluded, so a nearby observer hearing a node's cheap local adverts does not inflate the number - the existing advert_count mixes both kinds and cannot tell a chatty flooder (mesh-wide airtime) from the recommended 240-minute zero-hop cadence (local only). Consumers (the ArcScope repeater advisor) rate advert hygiene against the community practice of one flood advert every ~49h; with the mixed total, a correctly configured repeater looked chatty whenever an observer sat within zero-hop range. Implemented like the relay-liveness fields: a pure, unit-tested counter over (first_seen, route_type, hash) entries with the same timestamp parsing and hash dedup, fed by a from_pubkey-indexed query capped at the 2000 most recent advert rows. The flood route-type constant is named advertRouteTypeFlood so this merges independently of the open unscoped-relay PR (#1823). --------- Co-authored-by: Waydroid Builder <build@waydroid.local>
82 lines
3.3 KiB
Go
82 lines
3.3 KiB
Go
package main
|
|
|
|
import "time"
|
|
|
|
// advertRouteTypeFlood is ROUTE_TYPE_FLOOD from the MeshCore packet header.
|
|
// Named distinctly from the equivalent constant in the (still open) unscoped-
|
|
// relay PR so the two changes merge independently.
|
|
const advertRouteTypeFlood = 1
|
|
|
|
// floodAdvertEntry is one advert transmission originated by a node, reduced to
|
|
// what the windowed flood-advert count needs: first-seen timestamp, route type
|
|
// and packet hash (for dedup across re-ingests / multi-observer rows).
|
|
type floodAdvertEntry struct {
|
|
ts string
|
|
rt int
|
|
hash string
|
|
}
|
|
|
|
// countFloodAdverts counts distinct flood adverts (route_type ==
|
|
// advertRouteTypeFlood) whose first-seen lies within the past windowHours. Entries
|
|
// with unparseable timestamps are skipped, matching relay-liveness behaviour;
|
|
// entries without a hash fall back to their timestamp as the dedup key.
|
|
func countFloodAdverts(entries []floodAdvertEntry, now time.Time, windowHours float64) int {
|
|
cutoff := now.Add(-time.Duration(windowHours * float64(time.Hour)))
|
|
seen := map[string]struct{}{}
|
|
for _, e := range entries {
|
|
if e.rt != advertRouteTypeFlood {
|
|
continue
|
|
}
|
|
t, ok := parseRelayTS(e.ts)
|
|
if !ok || !t.After(cutoff) {
|
|
continue
|
|
}
|
|
key := e.hash
|
|
if key == "" {
|
|
key = e.ts
|
|
}
|
|
seen[key] = struct{}{}
|
|
}
|
|
return len(seen)
|
|
}
|
|
|
|
// CountFloodAdvertsForNode returns how many distinct FLOOD adverts pubkey
|
|
// originated in the last windowHours - the mesh-wide-airtime kind. Zero-hop
|
|
// adverts (route_type DIRECT) are excluded, so a nearby observer hearing a
|
|
// node's cheap local adverts does not inflate the number.
|
|
//
|
|
// route_type is filtered in SQL so an advert-spamming node cannot truncate
|
|
// the flood count (review feedback on the earlier LIMIT approach). The time
|
|
// floor is a DATE-ONLY string with one day of slack: a date prefix compares
|
|
// lexically the same across every first_seen format parseRelayTS accepts
|
|
// ('T' and ' ' separators alike); the exact window check stays in Go.
|
|
//
|
|
// The row cap is a pure safety valve on per-request allocation: it applies to
|
|
// flood adverts inside the floor window only, and 50000 in ~8 days is ~4 per
|
|
// minute - any node past it is unambiguously a spammer whether the count
|
|
// saturates or not. (An exact COUNT cannot move into SQL because the precise
|
|
// window check needs parseRelayTS over the mixed first_seen formats.)
|
|
// floodAdvertRowCap is the production row cap; tests pass a smaller cap
|
|
// directly, so there is no mutable package state to race on.
|
|
const floodAdvertRowCap = 50000
|
|
|
|
func (db *DB) CountFloodAdvertsForNode(pubkey string, windowHours float64, rowCap int) (int, error) {
|
|
floor := time.Now().UTC().Add(-time.Duration(windowHours*float64(time.Hour))).AddDate(0, 0, -1).Format("2006-01-02")
|
|
rows, err := db.conn.Query(
|
|
"SELECT COALESCE(first_seen, ''), COALESCE(route_type, -1), COALESCE(hash, '') FROM transmissions WHERE from_pubkey = ? AND payload_type = ? AND route_type = ? AND first_seen >= ? ORDER BY id DESC LIMIT ?",
|
|
pubkey, payloadTypeAdvert, advertRouteTypeFlood, floor, rowCap)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer rows.Close()
|
|
var entries []floodAdvertEntry
|
|
for rows.Next() {
|
|
var e floodAdvertEntry
|
|
if err := rows.Scan(&e.ts, &e.rt, &e.hash); err != nil {
|
|
return 0, err
|
|
}
|
|
entries = append(entries, e)
|
|
}
|
|
return countFloodAdverts(entries, time.Now(), windowHours), nil
|
|
}
|