mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-11 12:05:40 +00:00
Rebase of #1881 by @SaarMesh-Bot onto current master. Their three commits are preserved, two of them cherry-picked with authorship intact; the sweep itself had to be regenerated. Opened as a new PR rather than force-pushing their branch. Closes #1881 once merged. Addresses parts 1 and 3 of #1859; part 2 landed as #1937. ## Why regenerated rather than merged The sweep in #1881 was cut on 2026-09-02 07:13 and roughly forty PRs landed after it, so it went `CONFLICTING/DIRTY`. Re-running `gofmt` on current master is cheaper and less error-prone than resolving 72 conflicts that are all whitespace. The drift it fixes also grew in the meantime: 66 files now, against 72 then, but spread differently. ## The three commits 1. **`style(#1859)`** — `gofmt -w` across the 14 modules. 66 files. 2. **`test(#1859)`** — @SaarMesh-Bot's fix for the one `go vet` copylocks finding, `cmd/ingestor/coverage_boost_test.go`: the range variable copied a `Config` embedding `sync.Once`. Cherry-picked unchanged. 3. **`ci(#1859)`** — @SaarMesh-Bot's CI step that fails on gofmt drift or vet findings, plus `.git-blame-ignore-revs`. Cherry-picked with one change, noted in the commit message: the ignore file pointed at `04bc80ee`, the sweep commit on their branch, which does not exist on this base and would make `git blame --ignore-revs-file` error. Repointed at `d3a02599`, the sweep here. ## Verification The claim "formatting only" is checked twice rather than asserted: - Every changed file is byte-identical to `gofmt(previous content)`. 0 of 66 deviate. - With line comments and all whitespace stripped, 0 of 66 files differ, so no code outside comments changed. 14 of the 66 also show doc-comment reflow. Since Go 1.19 `gofmt` re-indents indented comment blocks to tabs and inserts a blank comment line before them; the behavior matrix above `resolveHopWithContext` in `cmd/ingestor/path_resolver.go` is a clear example. That is gofmt's own output, not an edit, but it is worth naming because it makes the diff look larger than "whitespace" suggests. The gate was run locally exactly as the workflow runs it: `gofmt` clean, and `go vet` clean in all 14 modules, including `cmd/ingestor` which is what commit 2 fixes. Suites: `cmd/server` ok (80.7s), `internal/packetpath` ok (2.3s), `cmd/ingestor` passes except `TestWriteStatsAtomic_SymlinkAtDestIsReplaced`, which fails identically on bare master with "A required privilege is not held by the client" (Windows symlink privilege on my host, not code). ## Sequencing This should go last in the queue. The sweep touches 66 files, so merging it before the remaining open Go PRs gives each of them a conflict about nothing but formatting. After it lands the gate is active, and any PR with drift fails CI until it runs `gofmt -w`. Excluded from the sweep: the misnamed `Dockerfile.go`, which is a Dockerfile that gofmt cannot parse (the workflow excludes it too), and `docs/DEPLOYMENT.md`, which a case-insensitive filesystem surfaces as a spurious modification against `docs/deployment.md` and is unrelated. --------- Co-authored-by: SaarMesh-Bot <300107934+SaarMesh-Bot@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
576 lines
19 KiB
Go
576 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"testing"
|
|
)
|
|
|
|
func TestDecodeHeader_TransportFlood(t *testing.T) {
|
|
// Route type 0 = TRANSPORT_FLOOD, payload type 5 = GRP_TXT, version 0
|
|
// Header byte: (0 << 6) | (5 << 2) | 0 = 0x14
|
|
h := decodeHeader(0x14)
|
|
if h.RouteType != RouteTransportFlood {
|
|
t.Errorf("expected RouteTransportFlood (0), got %d", h.RouteType)
|
|
}
|
|
if h.RouteTypeName != "TRANSPORT_FLOOD" {
|
|
t.Errorf("expected TRANSPORT_FLOOD, got %s", h.RouteTypeName)
|
|
}
|
|
if h.PayloadType != PayloadGRP_TXT {
|
|
t.Errorf("expected PayloadGRP_TXT (5), got %d", h.PayloadType)
|
|
}
|
|
}
|
|
|
|
func TestDecodeHeader_TransportDirect(t *testing.T) {
|
|
// Route type 3 = TRANSPORT_DIRECT, payload type 2 = TXT_MSG, version 0
|
|
// Header byte: (0 << 6) | (2 << 2) | 3 = 0x0B
|
|
h := decodeHeader(0x0B)
|
|
if h.RouteType != RouteTransportDirect {
|
|
t.Errorf("expected RouteTransportDirect (3), got %d", h.RouteType)
|
|
}
|
|
if h.RouteTypeName != "TRANSPORT_DIRECT" {
|
|
t.Errorf("expected TRANSPORT_DIRECT, got %s", h.RouteTypeName)
|
|
}
|
|
}
|
|
|
|
func TestDecodeHeader_Flood(t *testing.T) {
|
|
// Route type 1 = FLOOD, payload type 4 = ADVERT
|
|
// Header byte: (0 << 6) | (4 << 2) | 1 = 0x11
|
|
h := decodeHeader(0x11)
|
|
if h.RouteType != RouteFlood {
|
|
t.Errorf("expected RouteFlood (1), got %d", h.RouteType)
|
|
}
|
|
if h.RouteTypeName != "FLOOD" {
|
|
t.Errorf("expected FLOOD, got %s", h.RouteTypeName)
|
|
}
|
|
}
|
|
|
|
func TestIsTransportRoute(t *testing.T) {
|
|
if !isTransportRoute(RouteTransportFlood) {
|
|
t.Error("expected RouteTransportFlood to be transport")
|
|
}
|
|
if !isTransportRoute(RouteTransportDirect) {
|
|
t.Error("expected RouteTransportDirect to be transport")
|
|
}
|
|
if isTransportRoute(RouteFlood) {
|
|
t.Error("expected RouteFlood to NOT be transport")
|
|
}
|
|
if isTransportRoute(RouteDirect) {
|
|
t.Error("expected RouteDirect to NOT be transport")
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_TransportFloodHasCodes(t *testing.T) {
|
|
// Build a minimal TRANSPORT_FLOOD packet:
|
|
// Header 0x14 (route=0/T_FLOOD, payload=5/GRP_TXT)
|
|
// Transport codes: AABB CCDD (4 bytes)
|
|
// Path byte: 0x00 (hashSize=1, hashCount=0)
|
|
// Payload: at least some bytes for GRP_TXT
|
|
hex := "14AABBCCDD00112233445566778899"
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if pkt.TransportCodes == nil {
|
|
t.Fatal("expected transport codes to be present")
|
|
}
|
|
if pkt.TransportCodes.Code1 != "AABB" {
|
|
t.Errorf("expected Code1=AABB, got %s", pkt.TransportCodes.Code1)
|
|
}
|
|
if pkt.TransportCodes.Code2 != "CCDD" {
|
|
t.Errorf("expected Code2=CCDD, got %s", pkt.TransportCodes.Code2)
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_FloodHasNoCodes(t *testing.T) {
|
|
// Header 0x11 (route=1/FLOOD, payload=4/ADVERT)
|
|
// Path byte: 0x00 (no hops)
|
|
// Some payload bytes
|
|
hex := "110011223344556677889900AABBCCDD"
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if pkt.TransportCodes != nil {
|
|
t.Error("expected no transport codes for FLOOD route")
|
|
}
|
|
}
|
|
|
|
func TestZeroHopDirectHashSize(t *testing.T) {
|
|
// DIRECT (RouteType=2) + REQ (PayloadType=0) → header byte = 0x02
|
|
// pathByte=0x00 → hash_count=0, hash_size bits=0 → should get HashSize=0
|
|
// Need at least a few payload bytes after pathByte.
|
|
hex := "02" + "00" + repeatHex("AA", 20)
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket failed: %v", err)
|
|
}
|
|
if pkt.Path.HashSize != 0 {
|
|
t.Errorf("DIRECT zero-hop: want HashSize=0, got %d", pkt.Path.HashSize)
|
|
}
|
|
}
|
|
|
|
func TestZeroHopDirectHashSizeWithNonZeroUpperBits(t *testing.T) {
|
|
// DIRECT (RouteType=2) + REQ (PayloadType=0) → header byte = 0x02
|
|
// pathByte=0x40 → hash_count=0, hash_size bits=01 → should still get HashSize=0
|
|
// because hash_count is zero (lower 6 bits are 0).
|
|
hex := "02" + "40" + repeatHex("AA", 20)
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket failed: %v", err)
|
|
}
|
|
if pkt.Path.HashSize != 0 {
|
|
t.Errorf("DIRECT zero-hop with hash_size bits set: want HashSize=0, got %d", pkt.Path.HashSize)
|
|
}
|
|
}
|
|
|
|
func TestZeroHopTransportDirectHashSize(t *testing.T) {
|
|
// TRANSPORT_DIRECT (RouteType=3) + REQ (PayloadType=0) → header byte = 0x03
|
|
// 4 bytes transport codes + pathByte=0x00 → hash_count=0 → should get HashSize=0
|
|
hex := "03" + "11223344" + "00" + repeatHex("AA", 20)
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket failed: %v", err)
|
|
}
|
|
if pkt.Path.HashSize != 0 {
|
|
t.Errorf("TRANSPORT_DIRECT zero-hop: want HashSize=0, got %d", pkt.Path.HashSize)
|
|
}
|
|
}
|
|
|
|
func TestZeroHopTransportDirectHashSizeWithNonZeroUpperBits(t *testing.T) {
|
|
// pathByte=0xC0 → hash_size bits=11 (4, reserved per firmware Packet.cpp:13-18).
|
|
// Firmware Packet::isValidPathLen rejects this regardless of hash_count,
|
|
// because hash_size==4 is reserved. Go decoder must mirror that — even
|
|
// when hash_count==0, an attacker-emitted 0xC0 byte should not be
|
|
// silently accepted; firmware never emits hash_size==4.
|
|
hex := "03" + "11223344" + "C0" + repeatHex("AA", 20)
|
|
_, err := DecodePacket(hex, false)
|
|
if err == nil {
|
|
t.Fatalf("DecodePacket(pathByte=0xC0) succeeded; want error mirroring firmware Packet.cpp:13-18 (hash_size==4 reserved)")
|
|
}
|
|
}
|
|
|
|
func TestNonDirectZeroPathByteKeepsHashSize(t *testing.T) {
|
|
// FLOOD (RouteType=1) + REQ (PayloadType=0) → header byte = 0x01
|
|
// pathByte=0x00 → even though hash_count=0, non-DIRECT should keep HashSize=1
|
|
hex := "01" + "00" + repeatHex("AA", 20)
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket failed: %v", err)
|
|
}
|
|
if pkt.Path.HashSize != 1 {
|
|
t.Errorf("FLOOD zero pathByte: want HashSize=1 (unchanged), got %d", pkt.Path.HashSize)
|
|
}
|
|
}
|
|
|
|
func TestDirectNonZeroHopKeepsHashSize(t *testing.T) {
|
|
// DIRECT (RouteType=2) + REQ (PayloadType=0) → header byte = 0x02
|
|
// pathByte=0x01 → hash_count=1, hash_size=1 → should keep HashSize=1
|
|
// Need 1 hop hash byte after pathByte.
|
|
hex := "02" + "01" + repeatHex("BB", 21)
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket failed: %v", err)
|
|
}
|
|
if pkt.Path.HashSize != 1 {
|
|
t.Errorf("DIRECT with 1 hop: want HashSize=1, got %d", pkt.Path.HashSize)
|
|
}
|
|
}
|
|
|
|
func repeatHex(byteHex string, n int) string {
|
|
s := ""
|
|
for i := 0; i < n; i++ {
|
|
s += byteHex
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestDecodePacket_TraceHopsCompleted(t *testing.T) {
|
|
// Build a TRACE packet:
|
|
// header: route=FLOOD(1), payload=TRACE(9), version=0 → (0<<6)|(9<<2)|1 = 0x25
|
|
// path_length: hash_size bits=0b00 (1-byte), hash_count=2 (2 SNR bytes) → 0x02
|
|
// path: 2 SNR bytes: 0xAA, 0xBB
|
|
// payload: tag(4 LE) + authCode(4 LE) + flags(1) + 4 hop hashes (1 byte each)
|
|
hex := "2502AABB" + // header + path_length + 2 SNR bytes
|
|
"01000000" + // tag = 1
|
|
"02000000" + // authCode = 2
|
|
"00" + // flags = 0
|
|
"DEADBEEF" // 4 hops (1-byte hash each)
|
|
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket error: %v", err)
|
|
}
|
|
if pkt.Payload.Type != "TRACE" {
|
|
t.Fatalf("expected TRACE, got %s", pkt.Payload.Type)
|
|
}
|
|
// Full intended route = 4 hops from payload
|
|
if len(pkt.Path.Hops) != 4 {
|
|
t.Errorf("expected 4 hops, got %d: %v", len(pkt.Path.Hops), pkt.Path.Hops)
|
|
}
|
|
// HopsCompleted = 2 (from header path SNR count)
|
|
if pkt.Path.HopsCompleted == nil {
|
|
t.Fatal("expected HopsCompleted to be set")
|
|
}
|
|
if *pkt.Path.HopsCompleted != 2 {
|
|
t.Errorf("expected HopsCompleted=2, got %d", *pkt.Path.HopsCompleted)
|
|
}
|
|
// FLOOD routing for TRACE is anomalous
|
|
if pkt.Anomaly == "" {
|
|
t.Error("expected anomaly flag for FLOOD-routed TRACE")
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_TraceNoSNR(t *testing.T) {
|
|
// TRACE with 0 SNR bytes (trace hasn't been forwarded yet)
|
|
// path_length: hash_size=0b00 (1-byte), hash_count=0 → 0x00
|
|
hex := "2500" + // header + path_length (0 hops in header)
|
|
"01000000" + // tag
|
|
"02000000" + // authCode
|
|
"00" + // flags
|
|
"AABBCC" // 3 hops intended
|
|
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket error: %v", err)
|
|
}
|
|
if pkt.Path.HopsCompleted == nil {
|
|
t.Fatal("expected HopsCompleted to be set")
|
|
}
|
|
if *pkt.Path.HopsCompleted != 0 {
|
|
t.Errorf("expected HopsCompleted=0, got %d", *pkt.Path.HopsCompleted)
|
|
}
|
|
if len(pkt.Path.Hops) != 3 {
|
|
t.Errorf("expected 3 hops, got %d", len(pkt.Path.Hops))
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_TraceFullyCompleted(t *testing.T) {
|
|
// TRACE where all hops completed (SNR count = hop count)
|
|
// path_length: hash_size=0b00 (1-byte), hash_count=3 → 0x03
|
|
hex := "2503AABBCC" + // header + path_length + 3 SNR bytes
|
|
"01000000" + // tag
|
|
"02000000" + // authCode
|
|
"00" + // flags
|
|
"DDEEFF" // 3 hops intended
|
|
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket error: %v", err)
|
|
}
|
|
if pkt.Path.HopsCompleted == nil {
|
|
t.Fatal("expected HopsCompleted to be set")
|
|
}
|
|
if *pkt.Path.HopsCompleted != 3 {
|
|
t.Errorf("expected HopsCompleted=3, got %d", *pkt.Path.HopsCompleted)
|
|
}
|
|
if len(pkt.Path.Hops) != 3 {
|
|
t.Errorf("expected 3 hops, got %d", len(pkt.Path.Hops))
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_TraceFlags1_TwoBytePathSz(t *testing.T) {
|
|
// TRACE with flags=1 → path_sz = 1 << (1 & 0x03) = 2-byte hashes
|
|
// Firmware always sends TRACE as DIRECT (route_type=2), so header byte =
|
|
// (0<<6)|(9<<2)|2 = 0x26. path_length 0x00 = 0 SNR bytes.
|
|
hex := "2600" + // header (DIRECT+TRACE) + path_length (0 SNR)
|
|
"01000000" + // tag
|
|
"02000000" + // authCode
|
|
"01" + // flags = 1 → path_sz = 2
|
|
"AABBCCDD" // 4 bytes = 2 hops of 2-byte each
|
|
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket error: %v", err)
|
|
}
|
|
if len(pkt.Path.Hops) != 2 {
|
|
t.Errorf("expected 2 hops (2-byte path_sz), got %d: %v", len(pkt.Path.Hops), pkt.Path.Hops)
|
|
}
|
|
if pkt.Path.HashSize != 2 {
|
|
t.Errorf("expected HashSize=2, got %d", pkt.Path.HashSize)
|
|
}
|
|
if pkt.Anomaly != "" {
|
|
t.Errorf("expected no anomaly for DIRECT TRACE, got %q", pkt.Anomaly)
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_TraceFlags2_FourBytePathSz(t *testing.T) {
|
|
// TRACE with flags=2 → path_sz = 1 << (2 & 0x03) = 4-byte hashes
|
|
// DIRECT route_type (0x26)
|
|
hex := "2600" + // header (DIRECT+TRACE) + path_length (0 SNR)
|
|
"01000000" + // tag
|
|
"02000000" + // authCode
|
|
"02" + // flags = 2 → path_sz = 4
|
|
"AABBCCDD11223344" // 8 bytes = 2 hops of 4-byte each
|
|
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket error: %v", err)
|
|
}
|
|
if len(pkt.Path.Hops) != 2 {
|
|
t.Errorf("expected 2 hops (4-byte path_sz), got %d: %v", len(pkt.Path.Hops), pkt.Path.Hops)
|
|
}
|
|
if pkt.Path.HashSize != 4 {
|
|
t.Errorf("expected HashSize=4, got %d", pkt.Path.HashSize)
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_TracePathSzUnevenPayload(t *testing.T) {
|
|
// TRACE with flags=1 → path_sz=2, but 5 bytes of path data (not evenly divisible)
|
|
// Should produce 2 hops (4 bytes) and ignore the trailing byte
|
|
hex := "2600" + // header (DIRECT+TRACE) + path_length (0 SNR)
|
|
"01000000" + // tag
|
|
"02000000" + // authCode
|
|
"01" + // flags = 1 → path_sz = 2
|
|
"AABBCCDDEE" // 5 bytes → 2 hops, 1 byte remainder ignored
|
|
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket error: %v", err)
|
|
}
|
|
if len(pkt.Path.Hops) != 2 {
|
|
t.Errorf("expected 2 hops (trailing byte ignored), got %d: %v", len(pkt.Path.Hops), pkt.Path.Hops)
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_TraceTransportDirect(t *testing.T) {
|
|
// TRACE via TRANSPORT_DIRECT (route_type=3) — includes 4 transport code bytes
|
|
// header: (0<<6)|(9<<2)|3 = 0x27
|
|
hex := "27" + // header (TRANSPORT_DIRECT+TRACE)
|
|
"AABB" + "CCDD" + // transport codes (2+2 bytes)
|
|
"02" + // path_length: hash_count=2 SNR bytes
|
|
"EEFF" + // 2 SNR bytes
|
|
"01000000" + // tag
|
|
"02000000" + // authCode
|
|
"00" + // flags = 0 → path_sz = 1
|
|
"112233" // 3 hops (1-byte each)
|
|
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket error: %v", err)
|
|
}
|
|
if pkt.TransportCodes == nil {
|
|
t.Fatal("expected transport codes for TRANSPORT_DIRECT")
|
|
}
|
|
if pkt.TransportCodes.Code1 != "AABB" {
|
|
t.Errorf("expected Code1=AABB, got %s", pkt.TransportCodes.Code1)
|
|
}
|
|
if len(pkt.Path.Hops) != 3 {
|
|
t.Errorf("expected 3 hops, got %d: %v", len(pkt.Path.Hops), pkt.Path.Hops)
|
|
}
|
|
if pkt.Path.HopsCompleted == nil || *pkt.Path.HopsCompleted != 2 {
|
|
t.Errorf("expected HopsCompleted=2, got %v", pkt.Path.HopsCompleted)
|
|
}
|
|
if pkt.Anomaly != "" {
|
|
t.Errorf("expected no anomaly for TRANSPORT_DIRECT TRACE, got %q", pkt.Anomaly)
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_TraceFloodRouteAnomaly(t *testing.T) {
|
|
// TRACE via FLOOD (route_type=1) — anomalous per firmware (firmware only
|
|
// sends TRACE as DIRECT). Should still parse but flag the anomaly.
|
|
hex := "2500" + // header (FLOOD+TRACE) + path_length (0 SNR)
|
|
"01000000" + // tag
|
|
"02000000" + // authCode
|
|
"01" + // flags = 1 → path_sz = 2
|
|
"AABBCCDD" // 4 bytes = 2 hops of 2-byte each
|
|
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("should not crash on anomalous FLOOD+TRACE: %v", err)
|
|
}
|
|
if len(pkt.Path.Hops) != 2 {
|
|
t.Errorf("expected 2 hops even for anomalous FLOOD route, got %d", len(pkt.Path.Hops))
|
|
}
|
|
if pkt.Anomaly == "" {
|
|
t.Error("expected anomaly flag for FLOOD-routed TRACE, got empty string")
|
|
}
|
|
}
|
|
|
|
func TestDecodeAdvertSignatureValidation(t *testing.T) {
|
|
pub, priv, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var timestamp uint32 = 1234567890
|
|
appdata := []byte{0x02} // flags: repeater, no extras
|
|
|
|
// Build signed message: pubKey(32) + timestamp(4 LE) + appdata
|
|
msg := make([]byte, 32+4+len(appdata))
|
|
copy(msg[0:32], pub)
|
|
binary.LittleEndian.PutUint32(msg[32:36], timestamp)
|
|
copy(msg[36:], appdata)
|
|
sig := ed25519.Sign(priv, msg)
|
|
|
|
// Build a raw advert buffer: pubKey(32) + timestamp(4) + signature(64) + appdata
|
|
buf := make([]byte, 100+len(appdata))
|
|
copy(buf[0:32], pub)
|
|
binary.LittleEndian.PutUint32(buf[32:36], timestamp)
|
|
copy(buf[36:100], sig)
|
|
copy(buf[100:], appdata)
|
|
|
|
// With validation enabled
|
|
p := decodeAdvert(buf, true)
|
|
if p.SignatureValid == nil {
|
|
t.Fatal("expected SignatureValid to be set")
|
|
}
|
|
if !*p.SignatureValid {
|
|
t.Error("expected valid signature")
|
|
}
|
|
if p.PubKey != hex.EncodeToString(pub) {
|
|
t.Errorf("pubkey mismatch: got %s", p.PubKey)
|
|
}
|
|
|
|
// Tamper with signature → invalid
|
|
buf[40] ^= 0xFF
|
|
p = decodeAdvert(buf, true)
|
|
if p.SignatureValid == nil {
|
|
t.Fatal("expected SignatureValid to be set")
|
|
}
|
|
if *p.SignatureValid {
|
|
t.Error("expected invalid signature after tampering")
|
|
}
|
|
|
|
// Without validation → SignatureValid should be nil
|
|
p = decodeAdvert(buf, false)
|
|
if p.SignatureValid != nil {
|
|
t.Error("expected SignatureValid to be nil when validation disabled")
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_TraceSNRValues(t *testing.T) {
|
|
// TRACE packet with 3 SNR bytes in header path:
|
|
// SNR byte 0: 0x14 = int8(20) → 20/4.0 = 5.0 dB
|
|
// SNR byte 1: 0xF4 = int8(-12) → -12/4.0 = -3.0 dB
|
|
// SNR byte 2: 0x08 = int8(8) → 8/4.0 = 2.0 dB
|
|
// header: DIRECT+TRACE = (0<<6)|(9<<2)|2 = 0x26
|
|
// path_length: hash_size=0b00 (1-byte), hash_count=3 → 0x03
|
|
hex := "2603" + "14F408" + // header + path_byte + 3 SNR bytes
|
|
"01000000" + // tag
|
|
"02000000" + // authCode
|
|
"00" + // flags=0 → path_sz=1
|
|
"AABBCCDD" // 4 route hops (1-byte each)
|
|
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket error: %v", err)
|
|
}
|
|
if pkt.Payload.SNRValues == nil {
|
|
t.Fatal("expected SNRValues to be populated")
|
|
}
|
|
if len(pkt.Payload.SNRValues) != 3 {
|
|
t.Fatalf("expected 3 SNR values, got %d", len(pkt.Payload.SNRValues))
|
|
}
|
|
expected := []float64{5.0, -3.0, 2.0}
|
|
for i, want := range expected {
|
|
if pkt.Payload.SNRValues[i] != want {
|
|
t.Errorf("SNRValues[%d] = %v, want %v", i, pkt.Payload.SNRValues[i], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecodePacket_TraceNoSNRValues(t *testing.T) {
|
|
// TRACE with 0 SNR bytes → SNRValues should be nil/empty
|
|
hex := "2600" + // header + path_byte (0 hops)
|
|
"01000000" + // tag
|
|
"02000000" + // authCode
|
|
"00" + // flags
|
|
"AABB" // 2 route hops
|
|
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket error: %v", err)
|
|
}
|
|
if len(pkt.Payload.SNRValues) != 0 {
|
|
t.Errorf("expected empty SNRValues, got %v", pkt.Payload.SNRValues)
|
|
}
|
|
}
|
|
|
|
// TestDecodePacketBoundsFromWire_Issue1211 — mirror of ingestor test.
|
|
// Malformed pathByte=0xF6 inside a 15-byte buffer triggered
|
|
// `slice bounds out of range [218:15]`.
|
|
func TestDecodePacketBoundsFromWire_Issue1211(t *testing.T) {
|
|
raw := "12F6"
|
|
for i := 0; i < 13; i++ {
|
|
raw += "AA"
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("DecodePacket panicked on malformed input: %v", r)
|
|
}
|
|
}()
|
|
pkt, err := DecodePacket(raw, false)
|
|
if err == nil {
|
|
t.Fatalf("expected error for malformed packet, got nil; pkt=%+v", pkt)
|
|
}
|
|
}
|
|
|
|
// Adv M2: see cmd/ingestor/decoder_test.go — sweep gated on !testing.Short();
|
|
// FuzzDecodePacketTruncated below is the real fuzzing target.
|
|
func TestDecodePacketFuzzTruncated_Issue1211(t *testing.T) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("DecodePacket panicked during fuzz: %v", r)
|
|
}
|
|
}()
|
|
if testing.Short() {
|
|
t.Skip("skipping exhaustive sweep in -short mode; use FuzzDecodePacketTruncated")
|
|
}
|
|
for hdr := 0; hdr < 256; hdr++ {
|
|
for pb := 0; pb < 256; pb++ {
|
|
for tail := 0; tail < 20; tail++ {
|
|
raw := hex.EncodeToString([]byte{byte(hdr), byte(pb)})
|
|
for i := 0; i < tail; i++ {
|
|
raw += "00"
|
|
}
|
|
_, _ = DecodePacket(raw, false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// FuzzDecodePacketTruncated — native go fuzz target. Zero panics required.
|
|
// Run with: go test -fuzz=FuzzDecodePacketTruncated -fuzztime=30s ./cmd/server
|
|
func FuzzDecodePacketTruncated(f *testing.F) {
|
|
seeds := [][]byte{
|
|
{0x12, 0xF6, 0xAA, 0xAA, 0xAA},
|
|
{0x12, 0x00},
|
|
{0x03, 0x11, 0x22, 0x33, 0x44, 0xC0, 0xAA, 0xAA, 0xAA},
|
|
}
|
|
for _, s := range seeds {
|
|
f.Add(s)
|
|
}
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("DecodePacket panicked on input %x: %v", data, r)
|
|
}
|
|
}()
|
|
_, _ = DecodePacket(hex.EncodeToString(data), false)
|
|
})
|
|
}
|
|
|
|
// TestDecodeAdvertOversizedNameTruncated asserts decodeAdvert truncates the
|
|
// advert name to firmware's MAX_ADVERT_DATA_SIZE=32 (firmware/src/MeshCore.h:11).
|
|
// Firmware writes the node name into a 32-byte buffer, so any on-wire advert
|
|
// carrying >32 bytes of name data is adversarial — the Go decoder must not
|
|
// surface attacker-controlled bytes beyond what firmware would ever emit.
|
|
func TestDecodeAdvertOversizedNameTruncated(t *testing.T) {
|
|
pubkey := repeatHex("AA", 32)
|
|
timestamp := "78563412"
|
|
signature := repeatHex("BB", 64)
|
|
flags := "81" // chat(1) | hasName(0x80), no location, no feat1/2
|
|
// 64-byte ASCII 'X' name (firmware buffer is only 32 bytes).
|
|
name := repeatHex("58", 64)
|
|
hex := "1200" + pubkey + timestamp + signature + flags + name
|
|
pkt, err := DecodePacket(hex, false)
|
|
if err != nil {
|
|
t.Fatalf("DecodePacket: %v", err)
|
|
}
|
|
if got := len(pkt.Payload.Name); got > 32 {
|
|
t.Errorf("name length=%d, want <=32 (MAX_ADVERT_DATA_SIZE firmware/src/MeshCore.h:11)", got)
|
|
}
|
|
}
|