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:
+14
-71
@@ -107,87 +107,30 @@ def extract_payload(raw_packet: bytes) -> bytes | None:
|
||||
|
||||
Returns the payload bytes, or None if packet is malformed.
|
||||
"""
|
||||
from app.path_utils import decode_path_byte, path_wire_len
|
||||
from app.path_utils import parse_packet_envelope
|
||||
|
||||
if len(raw_packet) < 2:
|
||||
return None
|
||||
|
||||
try:
|
||||
header = raw_packet[0]
|
||||
route_type = header & 0x03
|
||||
offset = 1
|
||||
|
||||
# Skip transport codes if present (TRANSPORT_FLOOD=0, TRANSPORT_DIRECT=3)
|
||||
if route_type in (0x00, 0x03):
|
||||
if len(raw_packet) < offset + 4:
|
||||
return None
|
||||
offset += 4
|
||||
|
||||
# Decode packed path byte
|
||||
if len(raw_packet) < offset + 1:
|
||||
return None
|
||||
hop_count, hash_size = decode_path_byte(raw_packet[offset])
|
||||
offset += 1
|
||||
|
||||
# Skip path data
|
||||
path_bytes = path_wire_len(hop_count, hash_size)
|
||||
if len(raw_packet) < offset + path_bytes:
|
||||
return None
|
||||
offset += path_bytes
|
||||
|
||||
# Rest is payload
|
||||
return raw_packet[offset:]
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
envelope = parse_packet_envelope(raw_packet)
|
||||
return envelope.payload if envelope is not None else None
|
||||
|
||||
|
||||
def parse_packet(raw_packet: bytes) -> PacketInfo | None:
|
||||
"""Parse a raw packet and extract basic info."""
|
||||
from app.path_utils import decode_path_byte, path_wire_len
|
||||
from app.path_utils import parse_packet_envelope
|
||||
|
||||
if len(raw_packet) < 2:
|
||||
envelope = parse_packet_envelope(raw_packet)
|
||||
if envelope is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
header = raw_packet[0]
|
||||
route_type = RouteType(header & 0x03)
|
||||
payload_type = PayloadType((header >> 2) & 0x0F)
|
||||
payload_version = (header >> 6) & 0x03
|
||||
|
||||
offset = 1
|
||||
|
||||
# Skip transport codes if present
|
||||
if route_type in (RouteType.TRANSPORT_FLOOD, RouteType.TRANSPORT_DIRECT):
|
||||
if len(raw_packet) < offset + 4:
|
||||
return None
|
||||
offset += 4
|
||||
|
||||
# Decode packed path byte
|
||||
if len(raw_packet) < offset + 1:
|
||||
return None
|
||||
hop_count, hash_size = decode_path_byte(raw_packet[offset])
|
||||
offset += 1
|
||||
|
||||
# Extract path data
|
||||
path_byte_len = path_wire_len(hop_count, hash_size)
|
||||
if len(raw_packet) < offset + path_byte_len:
|
||||
return None
|
||||
path = raw_packet[offset : offset + path_byte_len]
|
||||
offset += path_byte_len
|
||||
|
||||
# Rest is payload
|
||||
payload = raw_packet[offset:]
|
||||
|
||||
return PacketInfo(
|
||||
route_type=route_type,
|
||||
payload_type=payload_type,
|
||||
payload_version=payload_version,
|
||||
path_length=hop_count,
|
||||
path_hash_size=hash_size,
|
||||
path=path,
|
||||
payload=payload,
|
||||
route_type=RouteType(envelope.route_type),
|
||||
payload_type=PayloadType(envelope.payload_type),
|
||||
payload_version=envelope.payload_version,
|
||||
path_length=envelope.hop_count,
|
||||
path_hash_size=envelope.hash_size,
|
||||
path=envelope.path,
|
||||
payload=envelope.payload,
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import aiomqtt
|
||||
import nacl.bindings
|
||||
|
||||
from app.fanout.mqtt_base import BaseMqttPublisher
|
||||
from app.path_utils import split_path_hex
|
||||
from app.path_utils import parse_packet_envelope, split_path_hex
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -143,41 +143,17 @@ def _calculate_packet_hash(raw_bytes: bytes) -> str:
|
||||
return "0" * 16
|
||||
|
||||
try:
|
||||
header = raw_bytes[0]
|
||||
payload_type = (header >> 2) & 0x0F
|
||||
route_type = header & 0x03
|
||||
|
||||
# Transport codes present for TRANSPORT_FLOOD (0) and TRANSPORT_DIRECT (3)
|
||||
has_transport = route_type in (0x00, 0x03)
|
||||
|
||||
offset = 1 # Past header
|
||||
if has_transport:
|
||||
offset += 4 # Skip 4 bytes of transport codes
|
||||
|
||||
# Read path byte (packed as [hash_mode:2][hop_count:6]).
|
||||
# Invalid/truncated packets map to zero hash.
|
||||
if offset >= len(raw_bytes):
|
||||
envelope = parse_packet_envelope(raw_bytes)
|
||||
if envelope is None:
|
||||
return "0" * 16
|
||||
path_byte = raw_bytes[offset]
|
||||
offset += 1
|
||||
hash_mode = (path_byte >> 6) & 0x03
|
||||
hop_count = path_byte & 0x3F
|
||||
hash_size = (hash_mode + 1) if hash_mode < 3 else 1
|
||||
path_wire_len = hop_count * hash_size
|
||||
|
||||
# Skip past path to get to payload. Invalid/truncated packets map to zero hash.
|
||||
if len(raw_bytes) < offset + path_wire_len:
|
||||
return "0" * 16
|
||||
payload_start = offset + path_wire_len
|
||||
payload_data = raw_bytes[payload_start:]
|
||||
|
||||
# Hash: payload_type(1 byte) [+ path_byte as uint16_t LE for TRACE] + payload_data
|
||||
# IMPORTANT: TRACE hash uses the raw wire byte (not decoded hop count) to match firmware.
|
||||
hash_obj = hashlib.sha256()
|
||||
hash_obj.update(bytes([payload_type]))
|
||||
if payload_type == 9: # PAYLOAD_TYPE_TRACE
|
||||
hash_obj.update(path_byte.to_bytes(2, byteorder="little"))
|
||||
hash_obj.update(payload_data)
|
||||
hash_obj.update(bytes([envelope.payload_type]))
|
||||
if envelope.payload_type == 9: # PAYLOAD_TYPE_TRACE
|
||||
hash_obj.update(envelope.path_byte.to_bytes(2, byteorder="little"))
|
||||
hash_obj.update(envelope.payload)
|
||||
|
||||
return hash_obj.hexdigest()[:16].upper()
|
||||
except Exception:
|
||||
@@ -198,42 +174,15 @@ def _decode_packet_fields(raw_bytes: bytes) -> tuple[str, str, str, list[str], i
|
||||
payload_type: int | None = None
|
||||
|
||||
try:
|
||||
if len(raw_bytes) < 2:
|
||||
envelope = parse_packet_envelope(raw_bytes)
|
||||
if envelope is None or envelope.payload_version != 0:
|
||||
return route, packet_type, payload_len, path_values, payload_type
|
||||
|
||||
header = raw_bytes[0]
|
||||
payload_version = (header >> 6) & 0x03
|
||||
if payload_version != 0:
|
||||
return route, packet_type, payload_len, path_values, payload_type
|
||||
|
||||
route_type = header & 0x03
|
||||
has_transport = route_type in (0x00, 0x03)
|
||||
|
||||
offset = 1
|
||||
if has_transport:
|
||||
offset += 4
|
||||
|
||||
if len(raw_bytes) <= offset:
|
||||
return route, packet_type, payload_len, path_values, payload_type
|
||||
|
||||
path_byte = raw_bytes[offset]
|
||||
offset += 1
|
||||
hash_mode = (path_byte >> 6) & 0x03
|
||||
hop_count = path_byte & 0x3F
|
||||
hash_size = (hash_mode + 1) if hash_mode < 3 else 1
|
||||
path_wire_len = hop_count * hash_size
|
||||
|
||||
if len(raw_bytes) < offset + path_wire_len:
|
||||
return route, packet_type, payload_len, path_values, payload_type
|
||||
|
||||
path_bytes = raw_bytes[offset : offset + path_wire_len]
|
||||
offset += path_wire_len
|
||||
|
||||
payload_type = (header >> 2) & 0x0F
|
||||
route = _ROUTE_MAP.get(route_type, "U")
|
||||
payload_type = envelope.payload_type
|
||||
route = _ROUTE_MAP.get(envelope.route_type, "U")
|
||||
packet_type = str(payload_type)
|
||||
payload_len = str(max(0, len(raw_bytes) - offset))
|
||||
path_values = split_path_hex(path_bytes.hex(), hop_count)
|
||||
payload_len = str(len(envelope.payload))
|
||||
path_values = split_path_hex(envelope.path.hex(), envelope.hop_count)
|
||||
|
||||
return route, packet_type, payload_len, path_values, payload_type
|
||||
except Exception:
|
||||
|
||||
+8
-65
@@ -452,43 +452,14 @@ async def _migrate_004_add_payload_hash_column(conn: aiosqlite.Connection) -> No
|
||||
|
||||
def _extract_payload_for_hash(raw_packet: bytes) -> bytes | None:
|
||||
"""
|
||||
Extract payload from a raw packet for hashing (migration-local copy of decoder logic).
|
||||
Extract payload from a raw packet for hashing using canonical framing validation.
|
||||
|
||||
Returns the payload bytes, or None if packet is malformed.
|
||||
"""
|
||||
if len(raw_packet) < 2:
|
||||
return None
|
||||
from app.path_utils import parse_packet_envelope
|
||||
|
||||
try:
|
||||
header = raw_packet[0]
|
||||
route_type = header & 0x03
|
||||
offset = 1
|
||||
|
||||
# Skip transport codes if present (TRANSPORT_FLOOD=0, TRANSPORT_DIRECT=3)
|
||||
if route_type in (0x00, 0x03):
|
||||
if len(raw_packet) < offset + 4:
|
||||
return None
|
||||
offset += 4
|
||||
|
||||
# Get path byte (packed as [hash_mode:2][hop_count:6])
|
||||
if len(raw_packet) < offset + 1:
|
||||
return None
|
||||
path_byte = raw_packet[offset]
|
||||
offset += 1
|
||||
hash_mode = (path_byte >> 6) & 0x03
|
||||
hop_count = path_byte & 0x3F
|
||||
hash_size = (hash_mode + 1) if hash_mode < 3 else 1
|
||||
path_wire_len = hop_count * hash_size
|
||||
|
||||
# Skip path bytes
|
||||
if len(raw_packet) < offset + path_wire_len:
|
||||
return None
|
||||
offset += path_wire_len
|
||||
|
||||
# Rest is payload (may be empty, matching decoder.py behavior)
|
||||
return raw_packet[offset:]
|
||||
except (IndexError, ValueError):
|
||||
return None
|
||||
envelope = parse_packet_envelope(raw_packet)
|
||||
return envelope.payload if envelope is not None else None
|
||||
|
||||
|
||||
async def _migrate_005_backfill_payload_hashes(conn: aiosqlite.Connection) -> None:
|
||||
@@ -638,42 +609,14 @@ async def _migrate_006_replace_path_len_with_path(conn: aiosqlite.Connection) ->
|
||||
|
||||
def _extract_path_from_packet(raw_packet: bytes) -> str | None:
|
||||
"""
|
||||
Extract path hex string from a raw packet (migration-local copy of decoder logic).
|
||||
Extract path hex string from a raw packet using canonical framing validation.
|
||||
|
||||
Returns the path as a hex string, or None if packet is malformed.
|
||||
"""
|
||||
if len(raw_packet) < 2:
|
||||
return None
|
||||
from app.path_utils import parse_packet_envelope
|
||||
|
||||
try:
|
||||
header = raw_packet[0]
|
||||
route_type = header & 0x03
|
||||
offset = 1
|
||||
|
||||
# Skip transport codes if present (TRANSPORT_FLOOD=0, TRANSPORT_DIRECT=3)
|
||||
if route_type in (0x00, 0x03):
|
||||
if len(raw_packet) < offset + 4:
|
||||
return None
|
||||
offset += 4
|
||||
|
||||
# Get path byte (packed as [hash_mode:2][hop_count:6])
|
||||
if len(raw_packet) < offset + 1:
|
||||
return None
|
||||
path_byte = raw_packet[offset]
|
||||
offset += 1
|
||||
hash_mode = (path_byte >> 6) & 0x03
|
||||
hop_count = path_byte & 0x3F
|
||||
hash_size = (hash_mode + 1) if hash_mode < 3 else 1
|
||||
path_wire_len = hop_count * hash_size
|
||||
|
||||
# Extract path bytes
|
||||
if len(raw_packet) < offset + path_wire_len:
|
||||
return None
|
||||
path_bytes = raw_packet[offset : offset + path_wire_len]
|
||||
|
||||
return path_bytes.hex()
|
||||
except (IndexError, ValueError):
|
||||
return None
|
||||
envelope = parse_packet_envelope(raw_packet)
|
||||
return envelope.path.hex() if envelope is not None else None
|
||||
|
||||
|
||||
async def _migrate_007_backfill_message_paths(conn: aiosqlite.Connection) -> None:
|
||||
|
||||
@@ -9,6 +9,27 @@ The path_len wire byte is packed as [hash_mode:2][hop_count:6]:
|
||||
Mode 3 (hash_size=4) is reserved and rejected.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
MAX_PATH_SIZE = 64
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParsedPacketEnvelope:
|
||||
"""Canonical packet framing parse matching MeshCore Packet::readFrom()."""
|
||||
|
||||
header: int
|
||||
route_type: int
|
||||
payload_type: int
|
||||
payload_version: int
|
||||
path_byte: int
|
||||
hop_count: int
|
||||
hash_size: int
|
||||
path_byte_len: int
|
||||
path: bytes
|
||||
payload: bytes
|
||||
payload_offset: int
|
||||
|
||||
|
||||
def decode_path_byte(path_byte: int) -> tuple[int, int]:
|
||||
"""Decode a packed path byte into (hop_count, hash_size).
|
||||
@@ -32,6 +53,79 @@ def path_wire_len(hop_count: int, hash_size: int) -> int:
|
||||
return hop_count * hash_size
|
||||
|
||||
|
||||
def validate_path_byte(path_byte: int) -> tuple[int, int, int]:
|
||||
"""Validate a packed path byte using firmware-equivalent rules.
|
||||
|
||||
Returns:
|
||||
(hop_count, hash_size, byte_len)
|
||||
|
||||
Raises:
|
||||
ValueError: If the encoding uses reserved mode 3 or exceeds MAX_PATH_SIZE.
|
||||
"""
|
||||
hop_count, hash_size = decode_path_byte(path_byte)
|
||||
byte_len = path_wire_len(hop_count, hash_size)
|
||||
if byte_len > MAX_PATH_SIZE:
|
||||
raise ValueError(
|
||||
f"Invalid path length {byte_len} bytes exceeds MAX_PATH_SIZE={MAX_PATH_SIZE}"
|
||||
)
|
||||
return hop_count, hash_size, byte_len
|
||||
|
||||
|
||||
def parse_packet_envelope(raw_packet: bytes) -> ParsedPacketEnvelope | None:
|
||||
"""Parse packet framing using firmware Packet::readFrom() semantics.
|
||||
|
||||
Validation matches the firmware's path checks:
|
||||
- reserved mode 3 is invalid
|
||||
- hop_count * hash_size must not exceed MAX_PATH_SIZE
|
||||
- at least one payload byte must remain after the path
|
||||
"""
|
||||
if len(raw_packet) < 2:
|
||||
return None
|
||||
|
||||
try:
|
||||
header = raw_packet[0]
|
||||
route_type = header & 0x03
|
||||
payload_type = (header >> 2) & 0x0F
|
||||
payload_version = (header >> 6) & 0x03
|
||||
|
||||
offset = 1
|
||||
if route_type in (0x00, 0x03):
|
||||
if len(raw_packet) < offset + 4:
|
||||
return None
|
||||
offset += 4
|
||||
|
||||
if len(raw_packet) < offset + 1:
|
||||
return None
|
||||
path_byte = raw_packet[offset]
|
||||
offset += 1
|
||||
|
||||
hop_count, hash_size, path_byte_len = validate_path_byte(path_byte)
|
||||
if len(raw_packet) < offset + path_byte_len:
|
||||
return None
|
||||
|
||||
path = raw_packet[offset : offset + path_byte_len]
|
||||
offset += path_byte_len
|
||||
|
||||
if offset >= len(raw_packet):
|
||||
return None
|
||||
|
||||
return ParsedPacketEnvelope(
|
||||
header=header,
|
||||
route_type=route_type,
|
||||
payload_type=payload_type,
|
||||
payload_version=payload_version,
|
||||
path_byte=path_byte,
|
||||
hop_count=hop_count,
|
||||
hash_size=hash_size,
|
||||
path_byte_len=path_byte_len,
|
||||
path=path,
|
||||
payload=raw_packet[offset:],
|
||||
payload_offset=offset,
|
||||
)
|
||||
except (IndexError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def split_path_hex(path_hex: str, hop_count: int) -> list[str]:
|
||||
"""Split a hex path string into per-hop chunks using the known hop count.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user