mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-07 17:33:16 +02:00
feat: enhance flood loop detection with hash size validation and update tests
This commit is contained in:
+14
-8
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user