mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 09:13:04 +02:00
Use more faithful packet frame parsing
This commit is contained in:
@@ -5,44 +5,7 @@ import type { RawPacket, Channel } from '../types';
|
||||
import { api } from '../api';
|
||||
import { toast } from './ui/sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* Extract the payload from a raw packet hex string, skipping header and path.
|
||||
* Returns the payload as a hex string, or null if malformed.
|
||||
*/
|
||||
function extractPayload(packetHex: string): string | null {
|
||||
if (packetHex.length < 4) return null; // Need at least 2 bytes
|
||||
|
||||
try {
|
||||
const header = parseInt(packetHex.slice(0, 2), 16);
|
||||
const routeType = header & 0x03;
|
||||
let offset = 2; // 1 byte = 2 hex chars
|
||||
|
||||
// Skip transport codes if present (TRANSPORT_FLOOD=0, TRANSPORT_DIRECT=3)
|
||||
if (routeType === 0x00 || routeType === 0x03) {
|
||||
if (packetHex.length < offset + 8) return null; // Need 4 more bytes
|
||||
offset += 8; // 4 bytes = 8 hex chars
|
||||
}
|
||||
|
||||
// Get path byte (packed as [hash_mode:2][hop_count:6])
|
||||
if (packetHex.length < offset + 2) return null;
|
||||
const pathByte = parseInt(packetHex.slice(offset, offset + 2), 16);
|
||||
offset += 2;
|
||||
const hashMode = (pathByte >> 6) & 0x03;
|
||||
const hopCount = pathByte & 0x3f;
|
||||
const hashSize = hashMode < 3 ? hashMode + 1 : 1;
|
||||
const pathHexChars = hopCount * hashSize * 2;
|
||||
|
||||
// Skip path data
|
||||
if (packetHex.length < offset + pathHexChars) return null;
|
||||
offset += pathHexChars;
|
||||
|
||||
// Rest is payload
|
||||
return packetHex.slice(offset);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
import { extractPacketPayloadHex } from '../utils/pathUtils';
|
||||
|
||||
interface CrackedRoom {
|
||||
roomName: string;
|
||||
@@ -180,7 +143,7 @@ export function CrackerPanel({
|
||||
for (const packet of undecryptedGroupText) {
|
||||
if (!newQueue.has(packet.id)) {
|
||||
// Extract payload and check for duplicates
|
||||
const payload = extractPayload(packet.data);
|
||||
const payload = extractPacketPayloadHex(packet.data);
|
||||
if (payload && seenPayloadsRef.current.has(payload)) {
|
||||
// Skip - we already have a packet with this payload queued
|
||||
newSkipped++;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
parsePathHops,
|
||||
extractPacketPayloadHex,
|
||||
findContactsByPrefix,
|
||||
calculateDistance,
|
||||
resolvePath,
|
||||
@@ -107,6 +108,28 @@ describe('parsePathHops', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractPacketPayloadHex', () => {
|
||||
it('extracts payload from legacy 1-byte-hop packet', () => {
|
||||
expect(extractPacketPayloadHex('0902AABB48656C6C6F')).toBe('48656C6C6F');
|
||||
});
|
||||
|
||||
it('extracts payload from 2-byte-hop packet', () => {
|
||||
expect(extractPacketPayloadHex('0942AABBCCDD48656C6C6F')).toBe('48656C6C6F');
|
||||
});
|
||||
|
||||
it('rejects reserved mode 3', () => {
|
||||
expect(extractPacketPayloadHex('09C1AABBCCDDEEFF')).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects oversized path encoding', () => {
|
||||
expect(extractPacketPayloadHex(`09BF${'AA'.repeat(189)}4869`)).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects packets with no payload after path', () => {
|
||||
expect(extractPacketPayloadHex('0902AABB')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findContactsByPrefix', () => {
|
||||
const contacts: Contact[] = [
|
||||
createContact({
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Contact, RadioConfig, MessagePath } from '../types';
|
||||
import { CONTACT_TYPE_REPEATER } from '../types';
|
||||
|
||||
const MAX_PATH_BYTES = 64;
|
||||
|
||||
export interface PathHop {
|
||||
prefix: string; // Hex hop identifier (e.g., "1A" for 1-byte, "1A2B" for 2-byte)
|
||||
matches: Contact[]; // Matched repeaters (empty=unknown, multiple=ambiguous)
|
||||
@@ -64,6 +66,61 @@ export function parsePathHops(path: string | null | undefined, hopCount?: number
|
||||
return hops;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the payload portion from a raw packet hex string using firmware-equivalent
|
||||
* path-byte validation. Returns null for malformed or payload-less packets.
|
||||
*/
|
||||
export function extractPacketPayloadHex(packetHex: string): string | null {
|
||||
if (packetHex.length < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const normalized = packetHex.toUpperCase();
|
||||
const header = parseInt(normalized.slice(0, 2), 16);
|
||||
const routeType = header & 0x03;
|
||||
let offset = 2;
|
||||
|
||||
if (routeType === 0x00 || routeType === 0x03) {
|
||||
if (normalized.length < offset + 8) {
|
||||
return null;
|
||||
}
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
if (normalized.length < offset + 2) {
|
||||
return null;
|
||||
}
|
||||
const pathByte = parseInt(normalized.slice(offset, offset + 2), 16);
|
||||
offset += 2;
|
||||
|
||||
const hashMode = (pathByte >> 6) & 0x03;
|
||||
if (hashMode === 0x03) {
|
||||
return null;
|
||||
}
|
||||
const hopCount = pathByte & 0x3f;
|
||||
const hashSize = hashMode + 1;
|
||||
const pathByteLen = hopCount * hashSize;
|
||||
if (pathByteLen > MAX_PATH_BYTES) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathHexChars = pathByteLen * 2;
|
||||
if (normalized.length < offset + pathHexChars) {
|
||||
return null;
|
||||
}
|
||||
offset += pathHexChars;
|
||||
|
||||
if (offset >= normalized.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized.slice(offset);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find contacts matching first 2 chars of public key (repeaters only for intermediate hops)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user