From b29f7ae24ded187c8552614bca2e1028a8fe657e Mon Sep 17 00:00:00 2001 From: Avza <69256629+devavza@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:01:36 +0300 Subject: [PATCH] Reject malformed tunnel packets --- meshtastic/tests/test_tunnel.py | 27 +++++++++++++++++++++++++++ meshtastic/tunnel.py | 17 +++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/meshtastic/tests/test_tunnel.py b/meshtastic/tests/test_tunnel.py index e2e917e5b..071514f4b 100644 --- a/meshtastic/tests/test_tunnel.py +++ b/meshtastic/tests/test_tunnel.py @@ -123,6 +123,33 @@ def test_shouldFilterPacket_random(mock_platform_system, caplog, iface_with_node assert not ignore +@pytest.mark.unit +@patch("platform.system") +@pytest.mark.parametrize( + "packet", + [ + b"", + b"\x00" * 19, + "not-a-packet", + *[b"\x00" * 9 + bytes([protocol]) + b"\x00" * 10 for protocol in (1, 6, 17)], + ], +) +def test_shouldFilterPacket_rejects_malformed_packets( + mock_platform_system, caplog, iface_with_nodes, packet +): + """Malformed mesh payloads must not crash the tunnel reader.""" + iface = iface_with_nodes + iface.noProto = True + mock_platform_system.return_value = "Linux" + + with caplog.at_level(logging.WARNING): + with patch("socket.socket"): + tun = Tunnel(iface) + assert tun._shouldFilterPacket(packet) + + assert re.search(r"Ignoring (malformed|truncated)", caplog.text) + + @pytest.mark.unitslow @patch("platform.system") def test_shouldFilterPacket_in_blacklist( diff --git a/meshtastic/tunnel.py b/meshtastic/tunnel.py index 46e3a2e0d..7f4b746c1 100644 --- a/meshtastic/tunnel.py +++ b/meshtastic/tunnel.py @@ -146,6 +146,14 @@ def onReceive(self, packet): def _shouldFilterPacket(self, p): """Given a packet, decode it and return true if it should be ignored""" + # Packets received from the mesh are untrusted. IPv4 requires a + # 20-byte header, and the protocol-specific fields below require the + # first four bytes of the transport header. Without these checks a + # truncated payload raises IndexError in the tunnel reader thread. + if not isinstance(p, (bytes, bytearray)) or len(p) < 20: + logger.warning("Ignoring malformed IP tunnel packet") + return True + protocol = p[8 + 1] srcaddr = p[12:16] destAddr = p[16:20] @@ -157,6 +165,9 @@ def _shouldFilterPacket(self, p): self.LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}" ) elif protocol == 0x01: # ICMP + if len(p) < subheader + 4: + logger.warning("Ignoring truncated ICMP tunnel packet") + return True icmpType = p[20] icmpCode = p[21] checksum = p[22:24] @@ -168,6 +179,9 @@ def _shouldFilterPacket(self, p): # pingback = p[:12]+p[16:20]+p[12:16]+p[20:] # tap.write(pingback) elif protocol == 0x11: # UDP + if len(p) < subheader + 4: + logger.warning("Ignoring truncated UDP tunnel packet") + return True srcport = readnet_u16(p, subheader) destport = readnet_u16(p, subheader + 2) if destport in self.udpBlacklist: @@ -176,6 +190,9 @@ def _shouldFilterPacket(self, p): else: logger.debug(f"forwarding udp srcport={srcport}, destport={destport}") elif protocol == 0x06: # TCP + if len(p) < subheader + 4: + logger.warning("Ignoring truncated TCP tunnel packet") + return True srcport = readnet_u16(p, subheader) destport = readnet_u16(p, subheader + 2) if destport in self.tcpBlacklist: