diff --git a/cmd/ingestor/main.go b/cmd/ingestor/main.go index 6176e327..96184b6b 100644 --- a/cmd/ingestor/main.go +++ b/cmd/ingestor/main.go @@ -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) diff --git a/cmd/ingestor/main_test.go b/cmd/ingestor/main_test.go index ffb6138f..f0e60215 100644 --- a/cmd/ingestor/main_test.go +++ b/cmd/ingestor/main_test.go @@ -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").