From ab0ee0a3575a03489e6a863c09eb2e0dc4b0bfe9 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:53:48 -0500 Subject: [PATCH 1/5] Update meshcop TLVs --- python_otbr_api/tlv_parser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python_otbr_api/tlv_parser.py b/python_otbr_api/tlv_parser.py index c8d4cd4..d970513 100644 --- a/python_otbr_api/tlv_parser.py +++ b/python_otbr_api/tlv_parser.py @@ -36,6 +36,7 @@ class MeshcopTLVType(IntEnum): JOINER_IID = 19 JOINER_RLOC = 20 JOINER_ROUTER_KEK = 21 + DURATION = 23 PROVISIONING_URL = 32 VENDOR_NAME_TLV = 33 VENDOR_MODEL_TLV = 34 @@ -51,8 +52,8 @@ class MeshcopTLVType(IntEnum): PERIOD = 55 SCAN_DURATION = 56 ENERGY_LIST = 57 - # Seen in a dataset imported through iOS companion app - APPLE_TAG_UNKNOWN = 74 + THREAD_DOMAIN_NAME = 59 + WAKEUP_CHANNEL = 74 DISCOVERYREQUEST = 128 DISCOVERYRESPONSE = 129 JOINERADVERTISEMENT = 241 From 54cd5d368e72e9f2c906fc90b8346ab54a75c7e0 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:12:42 -0500 Subject: [PATCH 2/5] Allow parsing to skip unknown TLVs --- python_otbr_api/tlv_parser.py | 44 +++++++++++++++++++++++++++-------- tests/test_tlv_parser.py | 15 ++++++++---- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/python_otbr_api/tlv_parser.py b/python_otbr_api/tlv_parser.py index d970513..443afa4 100644 --- a/python_otbr_api/tlv_parser.py +++ b/python_otbr_api/tlv_parser.py @@ -5,6 +5,9 @@ from dataclasses import dataclass, field from enum import IntEnum import struct +import logging + +_LOGGER = logging.getLogger(__name__) class TLVError(Exception): @@ -58,6 +61,9 @@ class MeshcopTLVType(IntEnum): DISCOVERYRESPONSE = 129 JOINERADVERTISEMENT = 241 + # Alias for backwards compatibility + APPLE_TAG_UNKNOWN = WAKEUP_CHANNEL + @dataclass class MeshcopTLVItem: @@ -125,7 +131,7 @@ def _encode_item(item: MeshcopTLVItem) -> bytes: return struct.pack(f"!BB{data_len}s", item.tag, data_len, item.data) -def encode_tlv(items: dict[MeshcopTLVType, MeshcopTLVItem]) -> str: +def encode_tlv(items: dict[MeshcopTLVType | int, MeshcopTLVItem]) -> str: """Encode a TLV encoded dataset to a hex string. Raises if the TLV is invalid. @@ -138,7 +144,7 @@ def encode_tlv(items: dict[MeshcopTLVType, MeshcopTLVItem]) -> str: return result.hex() -def _parse_item(tag: MeshcopTLVType, data: bytes) -> MeshcopTLVItem: +def _parse_item(tag: MeshcopTLVType | int, data: bytes) -> MeshcopTLVItem: """Parse a TLV encoded dataset item.""" if tag == MeshcopTLVType.ACTIVETIMESTAMP: return Timestamp(tag, data) @@ -150,7 +156,7 @@ def _parse_item(tag: MeshcopTLVType, data: bytes) -> MeshcopTLVItem: return MeshcopTLVItem(tag, data) -def parse_tlv(data: str) -> dict[MeshcopTLVType, MeshcopTLVItem]: +def parse_tlv(data: str) -> dict[MeshcopTLVType | int, MeshcopTLVItem]: """Parse a TLV encoded dataset. Raises if the TLV is invalid. @@ -161,19 +167,37 @@ def parse_tlv(data: str) -> dict[MeshcopTLVType, MeshcopTLVItem]: raise TLVError("invalid tlvs") from err result = {} pos = 0 - while pos < len(data_bytes): + length = len(data_bytes) + + while pos < length: + if pos + 2 > length: + raise TLVError("truncated tlv header") + + raw_tag = data_bytes[pos] + pos += 1 + + # Unknown tags should still be passed through + tag: MeshcopTLVType | int + try: - tag = MeshcopTLVType(data_bytes[pos]) + tag = MeshcopTLVType(raw_tag) except ValueError as err: - raise TLVError(f"unknown type {data_bytes[pos]}") from err - pos += 1 + tag = raw_tag + _len = data_bytes[pos] pos += 1 + + if pos + _len > length: + raise TLVError(f"expected {_len} bytes for tag {tag!r}, got {length - pos}") + val = data_bytes[pos : pos + _len] - if len(val) < _len: - raise TLVError(f"expected {_len} bytes for {tag.name}, got {len(val)}") pos += _len + + # Once we have the value, we can log a warning about the unknown TLV + if tag not in MeshcopTLVType: + _LOGGER.warning("unknown TLV type %d=%r", raw_tag, val) + if tag in result: - raise TLVError(f"duplicated tag {tag.name}") + raise TLVError(f"duplicated tag {tag!r}") result[tag] = _parse_item(tag, val) return result diff --git a/tests/test_tlv_parser.py b/tests/test_tlv_parser.py index ea698ec..3a8a43e 100644 --- a/tests/test_tlv_parser.py +++ b/tests/test_tlv_parser.py @@ -47,6 +47,7 @@ def test_encode_tlv() -> None: MeshcopTLVType.SECURITYPOLICY: MeshcopTLVItem( MeshcopTLVType.SECURITYPOLICY, bytes.fromhex("02a0f7f8") ), + 189: MeshcopTLVItem(189, bytes.fromhex("abcdef")), } dataset_tlv = encode_tlv(dataset) assert ( @@ -54,7 +55,7 @@ def test_encode_tlv() -> None: == ( "0E080000000000010000000300000F35060004001FFFE0020811111111222222220708FDAD" "70BFE5AA15DD051000112233445566778899AABBCCDDEEFF030E4F70656E54687265616444" - "656D6F010212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8" + "656D6F010212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8BD03ABCDEF" ).lower() ) @@ -64,7 +65,7 @@ def test_parse_tlv() -> None: dataset_tlv = ( "0E080000000000010000000300000F35060004001FFFE0020811111111222222220708FDAD70BF" "E5AA15DD051000112233445566778899AABBCCDDEEFF030E4F70656E54687265616444656D6F01" - "0212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8" + "0212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8BD03ABCDEF" ) dataset = parse_tlv(dataset_tlv) assert dataset == { @@ -98,6 +99,7 @@ def test_parse_tlv() -> None: MeshcopTLVType.CHANNELMASK: MeshcopTLVItem( MeshcopTLVType.CHANNELMASK, bytes.fromhex("0004001fffe0") ), + 189: MeshcopTLVItem(189, bytes.fromhex("abcdef")), } @@ -138,12 +140,17 @@ def test_parse_tlv_apple() -> None: ( "FF", TLVError, - "unknown type 255", + "truncated tlv header", + ), + ( + "FF01", + TLVError, + "expected 1 bytes for tag 255, got 0", ), ( "030E4F70656E54687265616444656D", TLVError, - "expected 14 bytes for NETWORKNAME, got 13", + "expected 14 bytes for tag , got 13", ), ( "030E4F70656E54687265616444656DFF", From e09f05df2dd7e74a617fe136f7eb9dd71c07821d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:15:56 -0500 Subject: [PATCH 3/5] Fix flake8 error --- python_otbr_api/tlv_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_otbr_api/tlv_parser.py b/python_otbr_api/tlv_parser.py index 443afa4..05adc4b 100644 --- a/python_otbr_api/tlv_parser.py +++ b/python_otbr_api/tlv_parser.py @@ -181,7 +181,7 @@ def parse_tlv(data: str) -> dict[MeshcopTLVType | int, MeshcopTLVItem]: try: tag = MeshcopTLVType(raw_tag) - except ValueError as err: + except ValueError: tag = raw_tag _len = data_bytes[pos] From 4155fd3660266a70607fab26a3363360c6aa015a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:19:58 -0500 Subject: [PATCH 4/5] Switch to a syntax compatible with more Python versions --- python_otbr_api/tlv_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_otbr_api/tlv_parser.py b/python_otbr_api/tlv_parser.py index 05adc4b..e38150b 100644 --- a/python_otbr_api/tlv_parser.py +++ b/python_otbr_api/tlv_parser.py @@ -194,7 +194,7 @@ def parse_tlv(data: str) -> dict[MeshcopTLVType | int, MeshcopTLVItem]: pos += _len # Once we have the value, we can log a warning about the unknown TLV - if tag not in MeshcopTLVType: + if not isinstance(tag, MeshcopTLVType): _LOGGER.warning("unknown TLV type %d=%r", raw_tag, val) if tag in result: From ff7a6da042a5353e2e51f0481a2f7b9fd88db4d5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:21:11 -0500 Subject: [PATCH 5/5] Revert tag changes --- python_otbr_api/tlv_parser.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/python_otbr_api/tlv_parser.py b/python_otbr_api/tlv_parser.py index e38150b..d89b05a 100644 --- a/python_otbr_api/tlv_parser.py +++ b/python_otbr_api/tlv_parser.py @@ -39,7 +39,6 @@ class MeshcopTLVType(IntEnum): JOINER_IID = 19 JOINER_RLOC = 20 JOINER_ROUTER_KEK = 21 - DURATION = 23 PROVISIONING_URL = 32 VENDOR_NAME_TLV = 33 VENDOR_MODEL_TLV = 34 @@ -55,15 +54,12 @@ class MeshcopTLVType(IntEnum): PERIOD = 55 SCAN_DURATION = 56 ENERGY_LIST = 57 - THREAD_DOMAIN_NAME = 59 - WAKEUP_CHANNEL = 74 + # Seen in a dataset imported through iOS companion app + APPLE_TAG_UNKNOWN = 74 DISCOVERYREQUEST = 128 DISCOVERYRESPONSE = 129 JOINERADVERTISEMENT = 241 - # Alias for backwards compatibility - APPLE_TAG_UNKNOWN = WAKEUP_CHANNEL - @dataclass class MeshcopTLVItem: