mirror of
https://github.com/rightup/pyMC_Repeater.git
synced 2026-04-30 18:52:14 +02:00
Merge pull request #170 from zindello/feat/fixRegions
Feat/fix regions: rename global_flood_allow to unscoped_flood_allow, fix region handling to correctly separate unscoped traffic from scoped regions, maintain backward compat with existing config files.
This commit is contained in:
@@ -121,10 +121,9 @@ repeater:
|
||||
|
||||
# Mesh Network Configuration
|
||||
mesh:
|
||||
# Global flood policy - controls whether the repeater allows or denies flooding by default
|
||||
# true = allow flooding globally, false = deny flooding globally
|
||||
# Individual transport keys can override this setting
|
||||
global_flood_allow: true
|
||||
# Unscoped flood policy - controls whether the repeater allows or denies unscoped flooding
|
||||
# true = allow unscoped flooding, false = deny flooding globally
|
||||
unscoped_flood_allow: true
|
||||
|
||||
# Path hash mode for flood packets (0-hop): per-hop hash size in path encoding
|
||||
# 0 = 1-byte hashes (legacy), 1 = 2-byte, 2 = 3-byte. Must match mesh convention.
|
||||
|
||||
@@ -152,12 +152,12 @@ def save_config(config_data: Dict[str, Any], config_path: Optional[str] = None)
|
||||
return False
|
||||
|
||||
|
||||
def update_global_flood_policy(allow: bool, config_path: Optional[str] = None) -> bool:
|
||||
def update_unscoped_flood_policy(allow: bool, config_path: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Update the global flood policy in the configuration.
|
||||
Update the unscoped flood policy in the configuration.
|
||||
|
||||
Args:
|
||||
allow: True to allow flooding globally, False to deny
|
||||
allow: True to allow unscoped flooding, False to deny
|
||||
config_path: Path to config file (uses default if None)
|
||||
|
||||
Returns:
|
||||
@@ -173,12 +173,13 @@ def update_global_flood_policy(allow: bool, config_path: Optional[str] = None) -
|
||||
|
||||
# Set global flood policy
|
||||
config["mesh"]["global_flood_allow"] = allow
|
||||
config["mesh"]["unscoped_flood_allow"] = allow
|
||||
|
||||
# Save updated config
|
||||
return save_config(config, config_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update global flood policy: {e}")
|
||||
logger.error(f"Failed to update unscoped flood policy: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -623,10 +623,10 @@ class RepeaterHandler(BaseHandler):
|
||||
route_type = packet.header & PH_ROUTE_MASK
|
||||
|
||||
if route_type == ROUTE_TYPE_FLOOD:
|
||||
# Check if global flood policy blocked it
|
||||
global_flood_allow = self.config.get("mesh", {}).get("global_flood_allow", True)
|
||||
if not global_flood_allow:
|
||||
return "Global flood policy disabled"
|
||||
# Check if unscoped flood policy blocked it
|
||||
unscoped_flood_allow = self.config.get("mesh", {}).get("unscoped_flood_allow", self.config.get("mesh", {}).get("global_flood_allow", True))
|
||||
if not unscoped_flood_allow:
|
||||
return "Unscoped flood policy disabled"
|
||||
|
||||
if route_type == ROUTE_TYPE_DIRECT:
|
||||
hash_size = packet.get_path_hash_size()
|
||||
@@ -800,19 +800,20 @@ class RepeaterHandler(BaseHandler):
|
||||
if not packet.drop_reason:
|
||||
packet.drop_reason = "Marked do not retransmit"
|
||||
return None
|
||||
|
||||
# Check unscoped flood policy
|
||||
unscoped_flood_allow = self.config.get("mesh", {}).get("unscoped_flood_allow", self.config.get("mesh", {}).get("global_flood_allow", True))
|
||||
route_type = packet.header & PH_ROUTE_MASK
|
||||
if route_type == ROUTE_TYPE_FLOOD:
|
||||
if not unscoped_flood_allow:
|
||||
packet.drop_reason = "Unscoped flood policy disabled"
|
||||
return None
|
||||
|
||||
# Check global flood policy
|
||||
global_flood_allow = self.config.get("mesh", {}).get("global_flood_allow", True)
|
||||
if not global_flood_allow:
|
||||
route_type = packet.header & PH_ROUTE_MASK
|
||||
if route_type == ROUTE_TYPE_FLOOD or route_type == ROUTE_TYPE_TRANSPORT_FLOOD:
|
||||
|
||||
allowed, check_reason = self._check_transport_codes(packet)
|
||||
if not allowed:
|
||||
packet.drop_reason = check_reason
|
||||
return None
|
||||
else:
|
||||
packet.drop_reason = "Global flood policy disabled"
|
||||
#Check transport scopes flood policy
|
||||
if route_type == ROUTE_TYPE_TRANSPORT_FLOOD:
|
||||
allowed, check_reason = self._check_transport_codes(packet)
|
||||
if not allowed:
|
||||
packet.drop_reason = "Transport code not allowed to flood"
|
||||
return None
|
||||
|
||||
mode = self._get_loop_detect_mode()
|
||||
@@ -1134,7 +1135,7 @@ class RepeaterHandler(BaseHandler):
|
||||
"web": self.config.get("web", {}), # Include web configuration
|
||||
"mesh": {
|
||||
"loop_detect": self.config.get("mesh", {}).get("loop_detect", "off"),
|
||||
"global_flood_allow": self.config.get("mesh", {}).get("global_flood_allow", True),
|
||||
"unscoped_flood_allow": self.config.get("mesh", {}).get("unscoped_flood_allow", self.config.get("mesh", {}).get("global_flood_allow", True)),
|
||||
"path_hash_mode": self.config.get("mesh", {}).get("path_hash_mode", 0),
|
||||
},
|
||||
"letsmesh": self.config.get("letsmesh", {}),
|
||||
|
||||
@@ -15,7 +15,7 @@ from repeater.companion.identity_resolve import (
|
||||
find_companion_index,
|
||||
heal_companion_empty_names,
|
||||
)
|
||||
from repeater.config import update_global_flood_policy
|
||||
from repeater.config import update_unscoped_flood_policy
|
||||
|
||||
from .auth.middleware import require_auth
|
||||
from .auth_endpoints import AuthAPIEndpoints
|
||||
@@ -93,8 +93,8 @@ logger = logging.getLogger("HTTPServer")
|
||||
# DELETE /api/transport_key?key_id=X - Delete transport key
|
||||
|
||||
# Network Policy
|
||||
# GET /api/global_flood_policy - Get global flood policy
|
||||
# POST /api/global_flood_policy - Update global flood policy
|
||||
# GET /api/unscoped_flood_policy - Get unscoped flood policy
|
||||
# POST /api/unscoped_flood_policy - Update unscoped flood policy
|
||||
# POST /api/ping_neighbor - Ping a neighbor node
|
||||
|
||||
# Identity Management
|
||||
@@ -2153,57 +2153,57 @@ class APIEndpoints:
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@cherrypy.tools.json_in()
|
||||
def global_flood_policy(self):
|
||||
def unscoped_flood_policy(self):
|
||||
"""
|
||||
Update global flood policy configuration
|
||||
Update unscoped flood policy configuration
|
||||
|
||||
POST /global_flood_policy
|
||||
Body: {"global_flood_allow": true/false}
|
||||
POST /unscoped_flood_policy
|
||||
Body: {"unscoped_flood_allow": true/false}
|
||||
"""
|
||||
if cherrypy.request.method == "POST":
|
||||
try:
|
||||
data = cherrypy.request.json or {}
|
||||
global_flood_allow = data.get("global_flood_allow")
|
||||
unscoped_flood_allow = data.get("unscoped_flood_allow")
|
||||
|
||||
if global_flood_allow is None:
|
||||
return self._error("Missing required field: global_flood_allow")
|
||||
if unscoped_flood_allow is None:
|
||||
return self._error("Missing required field: unscoped_flood_allow")
|
||||
|
||||
if not isinstance(global_flood_allow, bool):
|
||||
return self._error("global_flood_allow must be a boolean value")
|
||||
if not isinstance(unscoped_flood_allow, bool):
|
||||
return self._error("unscoped_flood_allow must be a boolean value")
|
||||
|
||||
# Update the running configuration first (like CAD settings)
|
||||
if "mesh" not in self.config:
|
||||
self.config["mesh"] = {}
|
||||
self.config["mesh"]["global_flood_allow"] = global_flood_allow
|
||||
self.config["mesh"]["unscoped_flood_allow"] = unscoped_flood_allow
|
||||
|
||||
# Get the actual config path from daemon instance (same as CAD settings)
|
||||
config_path = getattr(self, "_config_path", "/etc/pymc_repeater/config.yaml")
|
||||
if self.daemon_instance and hasattr(self.daemon_instance, "config_path"):
|
||||
config_path = self.daemon_instance.config_path
|
||||
|
||||
logger.info(f"Using config path for global flood policy: {config_path}")
|
||||
logger.info(f"Using config path for unscoped flood policy: {config_path}")
|
||||
|
||||
# Update the configuration file using ConfigManager
|
||||
try:
|
||||
saved = self.config_manager.save_to_file()
|
||||
if saved:
|
||||
logger.info(
|
||||
f"Updated running config and saved global flood policy to file: {'allow' if global_flood_allow else 'deny'}"
|
||||
f"Updated running config and saved unscoped flood policy to file: {'allow' if unscoped_flood_allow else 'deny'}"
|
||||
)
|
||||
else:
|
||||
logger.error("Failed to save global flood policy to file")
|
||||
logger.error("Failed to save unscoped flood policy to file")
|
||||
return self._error("Failed to save configuration to file")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save global flood policy to file: {e}")
|
||||
logger.error(f"Failed to save unscoped flood policy to file: {e}")
|
||||
return self._error(f"Failed to save configuration to file: {e}")
|
||||
|
||||
return self._success(
|
||||
{"global_flood_allow": global_flood_allow},
|
||||
message=f"Global flood policy updated to {'allow' if global_flood_allow else 'deny'} (live and saved)",
|
||||
{"unscoped_flood_allow": unscoped_flood_allow},
|
||||
message=f"Unscoped flood policy updated to {'allow' if unscoped_flood_allow else 'deny'} (live and saved)",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating global flood policy: {e}")
|
||||
logger.error(f"Error updating unscoped flood policy: {e}")
|
||||
return self._error(e)
|
||||
else:
|
||||
return self._error("Method not supported")
|
||||
|
||||
@@ -1235,10 +1235,10 @@ paths:
|
||||
# ============================================================================
|
||||
# Network Policy
|
||||
# ============================================================================
|
||||
/global_flood_policy:
|
||||
/unscoped_flood_policy:
|
||||
get:
|
||||
tags: [Network Policy]
|
||||
summary: Get global flood policy
|
||||
summary: Get unscoped flood policy
|
||||
description: Retrieve current network flood policy configuration
|
||||
security:
|
||||
- BearerAuth: []
|
||||
@@ -1252,7 +1252,7 @@ paths:
|
||||
type: object
|
||||
post:
|
||||
tags: [Network Policy]
|
||||
summary: Update global flood policy
|
||||
summary: Update unscoped flood policy
|
||||
description: Modify network flood policy settings
|
||||
security:
|
||||
- BearerAuth: []
|
||||
|
||||
@@ -46,7 +46,7 @@ def _make_config(**overrides) -> dict:
|
||||
"node_name": "test-node",
|
||||
},
|
||||
"mesh": {
|
||||
"global_flood_allow": True,
|
||||
"unscoped_flood_allow": True,
|
||||
"loop_detect": "off",
|
||||
},
|
||||
"delays": {
|
||||
@@ -228,11 +228,10 @@ class TestFloodForward:
|
||||
assert result is None
|
||||
assert "do not retransmit" in pkt.drop_reason.lower()
|
||||
|
||||
def test_global_flood_deny_plain_flood(self, handler):
|
||||
handler.config["mesh"]["global_flood_allow"] = False
|
||||
def test_unscoped_flood_deny_plain_flood(self, handler):
|
||||
handler.config["mesh"]["unscoped_flood_allow"] = False
|
||||
pkt = _make_flood_packet()
|
||||
# When global_flood_allow=False, flood_forward calls _check_transport_codes
|
||||
# which will fail because there are no transport codes on a plain flood
|
||||
# When unscoped_flood_allow=False, flood_forward should fail on a packet type without a transport code defined
|
||||
result = handler.flood_forward(pkt)
|
||||
assert result is None
|
||||
|
||||
@@ -656,26 +655,26 @@ class TestHashStabilityThroughForwarding:
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# 9. Global flood policy
|
||||
# 9. unscoped flood policy
|
||||
# ===================================================================
|
||||
|
||||
class TestGlobalFloodPolicy:
|
||||
"""global_flood_allow=False blocks plain flood, transport checked."""
|
||||
class TestUnscopedFloodPolicy:
|
||||
"""unscoped_flood_allow=False blocks plain flood, transport checked."""
|
||||
|
||||
def test_flood_blocked_by_policy(self, handler):
|
||||
handler.config["mesh"]["global_flood_allow"] = False
|
||||
handler.config["mesh"]["unscoped_flood_allow"] = False
|
||||
pkt = _make_flood_packet()
|
||||
result = handler.flood_forward(pkt)
|
||||
assert result is None
|
||||
|
||||
def test_direct_unaffected_by_flood_policy(self, handler):
|
||||
handler.config["mesh"]["global_flood_allow"] = False
|
||||
handler.config["mesh"]["unscoped_flood_allow"] = False
|
||||
pkt = _make_direct_packet()
|
||||
result = handler.direct_forward(pkt)
|
||||
assert result is not None # direct is not blocked by flood policy
|
||||
|
||||
def test_transport_flood_checked_when_policy_off(self, handler):
|
||||
handler.config["mesh"]["global_flood_allow"] = False
|
||||
handler.config["mesh"]["unscoped_flood_allow"] = False
|
||||
pkt = _make_transport_flood_packet()
|
||||
# Will call _check_transport_codes which will fail (no storage keys)
|
||||
result = handler.flood_forward(pkt)
|
||||
@@ -812,7 +811,7 @@ class TestGetDropReason:
|
||||
assert "Path too long" in reason
|
||||
|
||||
def test_flood_policy_reason(self, handler):
|
||||
handler.config["mesh"]["global_flood_allow"] = False
|
||||
handler.config["mesh"]["unscoped_flood_allow"] = False
|
||||
pkt = _make_flood_packet()
|
||||
reason = handler._get_drop_reason(pkt)
|
||||
assert "flood" in reason.lower()
|
||||
@@ -1202,7 +1201,7 @@ BAD_PACKETS = [
|
||||
"no path"),
|
||||
|
||||
("bad_flood_policy_off",
|
||||
"Plain flood when global_flood_allow=False (needs config override)",
|
||||
"Plain flood when unscoped_flood_allow=False (needs config override)",
|
||||
lambda: _make_flood_packet(payload=b"\x01\x02"),
|
||||
"transport codes"),
|
||||
|
||||
@@ -1323,9 +1322,9 @@ class TestBadPacketArray:
|
||||
BAD_PACKETS, ids=_bad_ids,
|
||||
)
|
||||
def test_process_packet_drops(self, handler, name, desc, builder, expected_reason):
|
||||
# Two entries need global_flood_allow=False
|
||||
# Two entries need unscoped_flood_allow=False
|
||||
if "policy_off" in name:
|
||||
handler.config["mesh"]["global_flood_allow"] = False
|
||||
handler.config["mesh"]["unscoped_flood_allow"] = False
|
||||
|
||||
pkt = builder()
|
||||
result = handler.process_packet(pkt, snr=5.0)
|
||||
@@ -1337,7 +1336,7 @@ class TestBadPacketArray:
|
||||
)
|
||||
def test_drop_reason_set(self, handler, name, desc, builder, expected_reason):
|
||||
if "policy_off" in name:
|
||||
handler.config["mesh"]["global_flood_allow"] = False
|
||||
handler.config["mesh"]["unscoped_flood_allow"] = False
|
||||
|
||||
pkt = builder()
|
||||
handler.process_packet(pkt, snr=5.0)
|
||||
@@ -1353,7 +1352,7 @@ class TestBadPacketArray:
|
||||
def test_bad_packet_not_marked_seen(self, handler, name, desc, builder, expected_reason):
|
||||
"""Dropped packets must NOT pollute the seen cache."""
|
||||
if "policy_off" in name:
|
||||
handler.config["mesh"]["global_flood_allow"] = False
|
||||
handler.config["mesh"]["unscoped_flood_allow"] = False
|
||||
|
||||
pkt = builder()
|
||||
handler.process_packet(pkt, snr=5.0)
|
||||
|
||||
@@ -7,7 +7,7 @@ objects to verify:
|
||||
- Loop detection modes (off, minimal, moderate, strict) with real path bytes
|
||||
- Flood re-forwarding prevention (own hash already in path)
|
||||
- Multi-byte hash mode interaction with loop/dedup
|
||||
- Global flood policy enforcement
|
||||
- Unscoped flood policy enforcement
|
||||
- mark_seen / is_duplicate cache behaviour
|
||||
- do_not_retransmit flag handling
|
||||
"""
|
||||
@@ -50,7 +50,7 @@ def _make_handler(
|
||||
loop_detect="off",
|
||||
path_hash_mode=0,
|
||||
local_hash_bytes=None,
|
||||
global_flood_allow=True,
|
||||
unscoped_flood_allow=True,
|
||||
):
|
||||
"""Create a RepeaterHandler with real engine logic, mocking only hardware."""
|
||||
lhb = local_hash_bytes or LOCAL_HASH_BYTES
|
||||
@@ -64,7 +64,7 @@ def _make_handler(
|
||||
"node_name": "test-node",
|
||||
},
|
||||
"mesh": {
|
||||
"global_flood_allow": global_flood_allow,
|
||||
"unscoped_flood_allow": unscoped_flood_allow,
|
||||
"loop_detect": loop_detect,
|
||||
"path_hash_mode": path_hash_mode,
|
||||
},
|
||||
@@ -398,29 +398,29 @@ class TestLoopDetectionMultiByte:
|
||||
|
||||
|
||||
# ===================================================================
|
||||
# 5. Global flood policy
|
||||
# 5. Unscoped flood policy
|
||||
# ===================================================================
|
||||
|
||||
|
||||
class TestGlobalFloodPolicy:
|
||||
"""Test global_flood_allow=False blocks flood packets."""
|
||||
class TestUnscopedFloodPolicy:
|
||||
"""Test unscoped=False blocks flood packets."""
|
||||
|
||||
def test_global_flood_disabled_drops_flood(self):
|
||||
h = _make_handler(global_flood_allow=False)
|
||||
def test_unscoped_flood_disabled_drops_flood(self):
|
||||
h = _make_handler(unscoped_flood_allow=False)
|
||||
pkt = _make_flood_packet(payload=b"\x01\x02")
|
||||
result = h.flood_forward(pkt)
|
||||
assert result is None
|
||||
assert pkt.drop_reason is not None
|
||||
|
||||
def test_global_flood_enabled_allows_flood(self):
|
||||
h = _make_handler(global_flood_allow=True)
|
||||
def test_unscoped_flood_enabled_allows_flood(self):
|
||||
h = _make_handler(unscoped_flood_allow=True)
|
||||
pkt = _make_flood_packet(payload=b"\x01\x02")
|
||||
result = h.flood_forward(pkt)
|
||||
assert result is not None
|
||||
|
||||
def test_transport_flood_without_codes_drops(self):
|
||||
"""ROUTE_TYPE_TRANSPORT_FLOOD with global_flood_allow=False and no valid codes."""
|
||||
h = _make_handler(global_flood_allow=False)
|
||||
"""ROUTE_TYPE_TRANSPORT_FLOOD with unscoped_flood_allow=False and no valid codes."""
|
||||
h = _make_handler(unscoped_flood_allow=False)
|
||||
# Nullify the storage to ensure transport code check fails
|
||||
h.storage = None
|
||||
pkt = Packet()
|
||||
|
||||
@@ -74,7 +74,7 @@ def _make_handler(path_hash_mode=0, local_hash_bytes=None):
|
||||
"node_name": "test-node",
|
||||
},
|
||||
"mesh": {
|
||||
"global_flood_allow": True,
|
||||
"unscoped_flood_allow": True,
|
||||
"loop_detect": "off",
|
||||
"path_hash_mode": path_hash_mode,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user