feat: enhance flood loop detection with hash size validation and update tests

This commit is contained in:
Rightup
2026-07-13 21:28:21 +01:00
parent 5d75cb9ad4
commit 6f61365d88
2 changed files with 58 additions and 8 deletions
+14 -8
View File
@@ -34,13 +34,12 @@ LOOP_DETECT_MINIMAL = "minimal"
LOOP_DETECT_MODERATE = "moderate"
LOOP_DETECT_STRICT = "strict"
# Thresholds for flood loop detection (hash-size-aware: 1, 2, or 3 bytes per hop).
# Count how many times our own hash already exists in the incoming FLOOD path.
# If occurrences >= threshold, treat as loop and drop.
# Thresholds for flood loop detection, keyed by loop mode and path-hash width.
# MeshCore treats 4-byte hashes as reserved, so only 1-3 byte widths are valid here.
LOOP_DETECT_MAX_COUNTERS = {
LOOP_DETECT_MINIMAL: 4,
LOOP_DETECT_MODERATE: 2,
LOOP_DETECT_STRICT: 1,
LOOP_DETECT_MINIMAL: {1: 4, 2: 2, 3: 1},
LOOP_DETECT_MODERATE: {1: 2, 2: 1, 3: 1},
LOOP_DETECT_STRICT: {1: 1, 2: 1, 3: 1},
}
@@ -778,6 +777,9 @@ class RepeaterHandler(BaseHandler):
if not packet or not packet.payload:
return False, "Empty payload"
if packet.get_path_hash_size() > 3:
return False, "Reserved path hash size is invalid"
if len(packet.path or []) >= MAX_PATH_SIZE:
return (
False,
@@ -806,11 +808,15 @@ class RepeaterHandler(BaseHandler):
if mode == LOOP_DETECT_OFF:
return False
max_counter = LOOP_DETECT_MAX_COUNTERS.get(mode)
if max_counter is None:
max_counters = LOOP_DETECT_MAX_COUNTERS.get(mode)
if max_counters is None:
return False
hash_size = packet.get_path_hash_size()
if hash_size not in max_counters:
return False
max_counter = max_counters[hash_size]
hop_count = packet.get_path_hash_count()
path = packet.path or bytearray()
local_hash = self.local_hash_bytes[:hash_size]
+44
View File
@@ -22,6 +22,7 @@ from openhop_core.protocol.constants import (
ROUTE_TYPE_TRANSPORT_DIRECT,
ROUTE_TYPE_TRANSPORT_FLOOD,
)
from openhop_core.protocol.packet_utils import PathUtils
# ---------------------------------------------------------------------------
# Helpers — build minimal config / mocks needed by RepeaterHandler.__init__
@@ -500,6 +501,13 @@ class TestValidatePacket:
valid, reason = handler.validate_packet(pkt)
assert valid is True
def test_reserved_four_byte_path_width_fails(self, handler):
pkt = _make_flood_packet(path=bytes([LOCAL_HASH] * 4))
pkt.path_len = (3 << 6) | 1
valid, reason = handler.validate_packet(pkt)
assert valid is False
assert "reserved" in reason.lower()
def test_none_packet(self, handler):
valid, reason = handler.validate_packet(None)
assert valid is False
@@ -750,6 +758,42 @@ class TestFloodLoopDetection:
result = handler.flood_forward(pkt)
assert result is None
@pytest.mark.parametrize(
"loop_detect, hash_size, threshold",
[
("minimal", 1, 4),
("minimal", 2, 2),
("minimal", 3, 1),
("moderate", 1, 2),
("moderate", 2, 1),
("moderate", 3, 1),
("strict", 1, 1),
("strict", 2, 1),
("strict", 3, 1),
],
)
def test_loop_threshold_depends_on_hash_width(self, handler, loop_detect, hash_size, threshold):
handler.config["mesh"]["loop_detect"] = loop_detect
handler.reload_runtime_config()
handler.local_hash_bytes = bytes([LOCAL_HASH, 0xCD, 0xEF])
local_hash = handler.local_hash_bytes[:hash_size]
below_payload = bytes([hash_size, threshold, 0x01])
below_path = local_hash * max(threshold - 1, 0)
below_pkt = _make_flood_packet(path=below_path, payload=below_payload)
below_pkt.path_len = PathUtils.encode_path_len(hash_size, max(threshold - 1, 0))
below_result = handler.flood_forward(below_pkt)
assert below_result is not None
at_payload = bytes([hash_size, threshold, 0x02])
at_path = local_hash * threshold
at_pkt = _make_flood_packet(path=at_path, payload=at_payload)
at_pkt.path_len = PathUtils.encode_path_len(hash_size, threshold)
at_result = handler.flood_forward(at_pkt)
assert at_result is None
assert "loop" in (at_pkt.drop_reason or "").lower()
# ===================================================================
# 10. Airtime / duty-cycle integration