diff --git a/config.yaml.example b/config.yaml.example index 46355e7..a8292b8 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -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. diff --git a/repeater/config.py b/repeater/config.py index 6ba716b..604a85f 100644 --- a/repeater/config.py +++ b/repeater/config.py @@ -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 diff --git a/repeater/engine.py b/repeater/engine.py index febcbaa..533c63f 100644 --- a/repeater/engine.py +++ b/repeater/engine.py @@ -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", {}), diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index 6a58c31..750109e 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -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") diff --git a/repeater/web/openapi.yaml b/repeater/web/openapi.yaml index e6dce1b..a353b0f 100644 --- a/repeater/web/openapi.yaml +++ b/repeater/web/openapi.yaml @@ -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: [] diff --git a/tests/test_engine.py b/tests/test_engine.py index 6b309c5..060b88a 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -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) diff --git a/tests/test_flood_loop_dedup.py b/tests/test_flood_loop_dedup.py index c595410..cb185aa 100644 --- a/tests/test_flood_loop_dedup.py +++ b/tests/test_flood_loop_dedup.py @@ -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() diff --git a/tests/test_path_hash_protocol.py b/tests/test_path_hash_protocol.py index d650ba0..2e7ead3 100644 --- a/tests/test_path_hash_protocol.py +++ b/tests/test_path_hash_protocol.py @@ -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, },