mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-06 20:57:20 +00:00
fix: matchScope reports unknown instead of guessing on HMAC collision
transport_code_1 is only 16 bits, so with enough configured hashRegions
a different, unrelated region's HMAC can coincidentally also match a
packet's real code1 (expected rate ~= len(regionKeys)/65534 per
packet). matchScope used to return the first match found while
iterating the region-key map, whose order Go randomizes per process —
so an ambiguous packet's assigned scope was effectively a coin flip
that could even change across ingestor restarts.
Confirmed on stg.meshview.dk (1098 configured regions): a live GRP_TXT
message's code1=C417 matched both "#dk" (the sender's true region) and
"#dk1906" (an unrelated collider), and was observed resolving to
either name across sends.
matchScope now scans every configured region and only returns a name
when exactly one matches; two or more matches are reported as
unknown-scoped ("") rather than guessed, with a log line for operator
visibility. Doesn't fix the root collision-probability cause (that
needs fewer/coarser configured regions) but stops confidently
displaying a wrong single answer.
This commit is contained in:
+21
-2
@@ -1454,10 +1454,24 @@ func loadRegionKeys(cfg *Config) map[string][]byte {
|
||||
|
||||
// matchScope performs one HMAC-SHA256 per configured region. Expected
|
||||
// len(regionKeys) ≤ 50; beyond that, consider a pre-indexed lookup table.
|
||||
//
|
||||
// code1 is only 16 bits (65534 usable values after the 0x0000/0xFFFF
|
||||
// remap), so with enough configured regions a *different*, unrelated
|
||||
// region's HMAC can coincidentally also produce the packet's real code1
|
||||
// (birthday-paradox collision — expected rate ≈ len(regionKeys)/65534
|
||||
// per packet). Silently returning the first map-iteration match would
|
||||
// make the guess a coin flip between the true region and the collider,
|
||||
// and Go's randomized map order means the same ambiguous packet could
|
||||
// even resolve differently across ingestor restarts. Instead we scan
|
||||
// every region and only return a name when exactly one matches; two or
|
||||
// more matches means the code1 doesn't uniquely identify a region for
|
||||
// this payload, so we report unknown-scoped ("") rather than guess.
|
||||
func matchScope(regionKeys map[string][]byte, payloadType byte, payloadRaw []byte, code1 string) string {
|
||||
if code1 == "0000" || len(regionKeys) == 0 || len(payloadRaw) == 0 {
|
||||
return ""
|
||||
}
|
||||
var match string
|
||||
matchCount := 0
|
||||
for name, key := range regionKeys {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write([]byte{payloadType})
|
||||
@@ -1471,10 +1485,15 @@ func matchScope(regionKeys map[string][]byte, payloadType byte, payloadRaw []byt
|
||||
}
|
||||
codeBytes := [2]byte{byte(code & 0xFF), byte(code >> 8)}
|
||||
if strings.ToUpper(hex.EncodeToString(codeBytes[:])) == code1 {
|
||||
return name
|
||||
match = name
|
||||
matchCount++
|
||||
}
|
||||
}
|
||||
return ""
|
||||
if matchCount > 1 {
|
||||
log.Printf("[regions] ambiguous scope match for code1=%s: %d regions collide (including %q) — reporting unknown-scoped instead of guessing", code1, matchCount, match)
|
||||
return ""
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// Version info (set via ldflags)
|
||||
|
||||
@@ -880,6 +880,43 @@ func TestMatchScope(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestMatchScope_AmbiguousCollisionReturnsUnknown is a real-world regression
|
||||
// vector captured on stg.meshview.dk (1098 configured hashRegions): a single
|
||||
// GRP_TXT packet (payloadType=5, code1=C417) whose HMAC coincidentally
|
||||
// matched BOTH "#dk" (the sender's true region) and "#dk1906" (an unrelated
|
||||
// region that happened to collide in the 16-bit code1 space). Before this
|
||||
// fix, matchScope returned whichever name Go's randomized map iteration
|
||||
// visited first — the same ambiguous packet could resolve to either name
|
||||
// across restarts. It must now report unknown-scoped ("") instead of
|
||||
// guessing when more than one region matches.
|
||||
func TestMatchScope_AmbiguousCollisionReturnsUnknown(t *testing.T) {
|
||||
dkKey, _ := hex.DecodeString("4a447e7539b0418bd14cf28aa8241529")
|
||||
dk1906Key, _ := hex.DecodeString("dd24ec5e2aa5221030147ebec77ebd7c")
|
||||
regionKeys := map[string][]byte{"#dk": dkKey, "#dk1906": dk1906Key}
|
||||
|
||||
payloadRaw, err := hex.DecodeString("D9A740B10D5BA91A9E5D5156DB534888AD454D")
|
||||
if err != nil {
|
||||
t.Fatalf("decode payloadRaw: %v", err)
|
||||
}
|
||||
|
||||
// Sanity: each key matches C417 individually (proves the vector is a
|
||||
// real collision, not a bug in the test itself).
|
||||
if got := matchScope(map[string][]byte{"#dk": dkKey}, 5, payloadRaw, "C417"); got != "#dk" {
|
||||
t.Fatalf("precondition: #dk alone should match C417, got %q", got)
|
||||
}
|
||||
if got := matchScope(map[string][]byte{"#dk1906": dk1906Key}, 5, payloadRaw, "C417"); got != "#dk1906" {
|
||||
t.Fatalf("precondition: #dk1906 alone should match C417, got %q", got)
|
||||
}
|
||||
|
||||
// With both configured, the collision must resolve to unknown ("")
|
||||
// rather than nondeterministically picking one.
|
||||
for i := 0; i < 20; i++ {
|
||||
if got := matchScope(regionKeys, 5, payloadRaw, "C417"); got != "" {
|
||||
t.Errorf("ambiguous match: matchScope = %q, want empty (unknown-scoped)", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPacketDataScopeMatching(t *testing.T) {
|
||||
// Fixed known-answer packet: TRANSPORT_FLOOD, payloadType=5, payload="hello",
|
||||
// Code1=2AB5 (pre-computed for region "#test").
|
||||
|
||||
Reference in New Issue
Block a user