mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-10-22 14:01:27 +00:00
Harden MeshCore packet decoding: extract routeType, validate paths, guard compat rewrite
- Extract and persist routeType from wire header byte (was always undefined) - Log warnings on truncated path data and reserved hash-size modes - Guard buildCompatRawHex against pathByteLength > 63 overflow - Extract transport codes (4 bytes) when present in FLOOD/DIRECT+codes packets - Validate path hash hex lengths against wire-format hash size in resolver - Add routeType to LivePacket type for downstream consumers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f0c2248c18
commit
954b3ce853
5 files changed
+62
-7
No files matched your search
@@ -311,6 +311,7 @@ async function handleMessage(topic: string, rawPayload: Buffer): Promise<void> {
|
||||
let decodedHash: string | undefined;
|
||||
let decodedHops: number | undefined;
|
||||
let decodedPathHashSizeBytes: number | undefined;
|
||||
let decodedRouteType: number | undefined;
|
||||
let summary: string | undefined;
|
||||
let srcNodeId: string | undefined;
|
||||
let advertCount: number | undefined;
|
||||
@@ -319,13 +320,14 @@ async function handleMessage(topic: string, rawPayload: Buffer): Promise<void> {
|
||||
|
||||
if (rawHex) {
|
||||
try {
|
||||
const { decoded, pathHashes, pathHashCount, pathHashSize } = decodePacketCompat(rawHex, keyStore);
|
||||
const { decoded, pathHashes, pathHashCount, pathHashSize, routeType: decodedRT } = decodePacketCompat(rawHex, keyStore);
|
||||
|
||||
if (decoded) {
|
||||
resolvedPacketType = decoded.payloadType ?? resolvedPacketType;
|
||||
decodedHash = decoded.messageHash;
|
||||
decodedHops = pathHashCount ?? decoded.pathLength;
|
||||
decodedPathHashSizeBytes = pathHashSize;
|
||||
decodedRouteType = decodedRT;
|
||||
if (pathHashes && pathHashes.length > 0) {
|
||||
path = pathHashes;
|
||||
}
|
||||
@@ -443,6 +445,7 @@ async function handleMessage(topic: string, rawPayload: Buffer): Promise<void> {
|
||||
topic,
|
||||
network,
|
||||
packetType: resolvedPacketType,
|
||||
routeType: decodedRouteType,
|
||||
hopCount: decodedHops,
|
||||
pathHashSizeBytes: decodedPathHashSizeBytes,
|
||||
direction,
|
||||
@@ -462,7 +465,7 @@ async function handleMessage(topic: string, rawPayload: Buffer): Promise<void> {
|
||||
srcNodeId,
|
||||
topic,
|
||||
packetType: resolvedPacketType,
|
||||
routeType: undefined,
|
||||
routeType: decodedRouteType,
|
||||
hopCount: decodedHops,
|
||||
pathHashSizeBytes: decodedPathHashSizeBytes,
|
||||
rssi,
|
||||
|
||||
@@ -3,6 +3,8 @@ import { MeshCoreDecoder } from '@michaelhart/meshcore-decoder';
|
||||
type KeyStore = ReturnType<typeof MeshCoreDecoder.createKeyStore>;
|
||||
|
||||
type PathMeta = {
|
||||
routeType: number;
|
||||
transportCodes: string | undefined;
|
||||
encodedLengthByte: number;
|
||||
pathLengthOffset: number;
|
||||
pathHashCount: number;
|
||||
@@ -16,6 +18,8 @@ export type CompatDecodedPacket = {
|
||||
pathHashes?: string[];
|
||||
pathHashCount?: number;
|
||||
pathHashSize?: number;
|
||||
routeType?: number;
|
||||
transportCodes?: string;
|
||||
};
|
||||
|
||||
function bytesToHex(bytes: Uint8Array): string {
|
||||
@@ -34,18 +38,38 @@ function parsePathMeta(rawHex: string): PathMeta | null {
|
||||
|
||||
const routeType = bytes[0]! & 0x03;
|
||||
let offset = 1;
|
||||
if (routeType === 0 || routeType === 3) offset += 4;
|
||||
|
||||
// Extract transport codes when present (routeType 0 = FLOOD, 3 = DIRECT with codes)
|
||||
let transportCodes: string | undefined;
|
||||
if (routeType === 0 || routeType === 3) {
|
||||
if (bytes.length < offset + 4) return null;
|
||||
transportCodes = bytesToHex(bytes.subarray(offset, offset + 4));
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
if (bytes.length <= offset) return null;
|
||||
|
||||
const encodedLengthByte = bytes[offset]!;
|
||||
const pathHashCount = encodedLengthByte & 0x3f;
|
||||
const pathHashSize = (encodedLengthByte >> 6) + 1;
|
||||
if (pathHashSize > 3) return null;
|
||||
|
||||
// Reserved hash-size mode (0b11 → 4 bytes) — reject fully (#3)
|
||||
if (pathHashSize > 3) {
|
||||
console.warn(`[decode] reserved path hash size mode ${pathHashSize} in packet ${rawHex.slice(0, 16)}…`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathByteLength = pathHashCount * pathHashSize;
|
||||
const pathStart = offset + 1;
|
||||
const pathEnd = pathStart + pathByteLength;
|
||||
if (bytes.length < pathEnd) return null;
|
||||
|
||||
// Warn on truncated path data (#2)
|
||||
if (bytes.length < pathEnd) {
|
||||
console.warn(
|
||||
`[decode] truncated path: need ${pathByteLength} path bytes but only ${bytes.length - pathStart} available in packet ${rawHex.slice(0, 16)}…`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathHashes: string[] = [];
|
||||
for (let i = 0; i < pathHashCount; i++) {
|
||||
@@ -55,6 +79,8 @@ function parsePathMeta(rawHex: string): PathMeta | null {
|
||||
}
|
||||
|
||||
return {
|
||||
routeType,
|
||||
transportCodes,
|
||||
encodedLengthByte,
|
||||
pathLengthOffset: offset,
|
||||
pathHashCount,
|
||||
@@ -66,6 +92,15 @@ function parsePathMeta(rawHex: string): PathMeta | null {
|
||||
|
||||
function buildCompatRawHex(rawHex: string, meta: PathMeta): string {
|
||||
if (meta.encodedLengthByte < 64) return rawHex;
|
||||
|
||||
// Guard: pathByteLength > 63 would bleed into upper 2 bits, corrupting the compat byte (#6)
|
||||
if (meta.pathByteLength > 63) {
|
||||
console.warn(
|
||||
`[decode] pathByteLength ${meta.pathByteLength} exceeds 63, skipping compat rewrite for packet ${rawHex.slice(0, 16)}…`,
|
||||
);
|
||||
return rawHex;
|
||||
}
|
||||
|
||||
const bytes = hexToBytes(rawHex);
|
||||
if (!bytes) return rawHex;
|
||||
const compat = new Uint8Array(bytes);
|
||||
@@ -82,5 +117,7 @@ export function decodePacketCompat(rawHex: string, keyStore: KeyStore): CompatDe
|
||||
pathHashes: meta?.pathHashes ?? (decoded.path as string[] | null) ?? undefined,
|
||||
pathHashCount: meta?.pathHashCount ?? decoded.pathLength ?? undefined,
|
||||
pathHashSize: meta?.pathHashSize ?? 1,
|
||||
routeType: meta?.routeType ?? decoded.routeType ?? undefined,
|
||||
transportCodes: meta?.transportCodes,
|
||||
};
|
||||
}
|
||||
@@ -1038,7 +1038,7 @@ export type BetaResolvedPayload = {
|
||||
export async function resolveBetaPathForPacketHash(packetHash: string, network: string, observer?: string): Promise<BetaResolvedPayload | null> {
|
||||
const [packetResult, observerHopResult] = await Promise.all([
|
||||
query<PathPacket>(
|
||||
`SELECT packet_hash, rx_node_id, src_node_id, packet_type, hop_count, path_hashes
|
||||
`SELECT packet_hash, rx_node_id, src_node_id, packet_type, hop_count, path_hashes, path_hash_size_bytes
|
||||
FROM packets
|
||||
WHERE packet_hash = $1
|
||||
AND ($2 = 'all' OR network = $2)
|
||||
@@ -1095,7 +1095,20 @@ export async function resolveBetaPathForPacketHash(packetHash: string, network:
|
||||
|
||||
const src = packet.src_node_id ? (context.nodesById.get(packet.src_node_id) ?? null) : null;
|
||||
const hashes = packet.path_hashes ?? [];
|
||||
const hops = packet.hop_count != null ? hashes.slice(0, Math.max(0, packet.hop_count)) : hashes;
|
||||
|
||||
// Validate path hash lengths against wire-format hash size when available (#4)
|
||||
const expectedHexLen = packet.path_hash_size_bytes != null ? packet.path_hash_size_bytes * 2 : null;
|
||||
const validatedHashes = expectedHexLen != null
|
||||
? hashes.filter((h) => {
|
||||
if (h.length !== expectedHexLen) {
|
||||
console.warn(`${logPrefix} hash length mismatch: expected ${expectedHexLen} hex chars, got ${h.length} ("${h}")`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
: hashes;
|
||||
|
||||
const hops = packet.hop_count != null ? validatedHashes.slice(0, Math.max(0, packet.hop_count)) : validatedHashes;
|
||||
const forceIncludeSource = packet.packet_type === 4;
|
||||
const currentHopCount = Number(packet.hop_count ?? 0);
|
||||
const observerHopHints: ObserverHopHint[] = currentHopCount > 0 && packet.rx_node_id
|
||||
|
||||
@@ -37,6 +37,7 @@ export type PathPacket = {
|
||||
packet_type: number | null;
|
||||
hop_count: number | null;
|
||||
path_hashes: string[] | null;
|
||||
path_hash_size_bytes: number | null;
|
||||
};
|
||||
|
||||
export type ObserverHopHint = {
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface LivePacket {
|
||||
topic: string;
|
||||
network?: string;
|
||||
packetType?: number;
|
||||
routeType?: number; // 0=FLOOD, 1=DIRECT, 2=FLOOD+codes, 3=DIRECT+codes
|
||||
hopCount?: number;
|
||||
pathHashSizeBytes?: number;
|
||||
direction?: string; // 'rx' | 'tx' from mctomqtt
|
||||
|
||||
Reference in new issue
Block a user