From 0b508a04b8745d06b2018a9b34801d63afa7327b Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 2 Jan 2026 21:03:18 +0100 Subject: [PATCH] Added interface discovery source filtering by network identity --- RNS/Discovery.py | 15 ++++++++++- RNS/Reticulum.py | 20 +++++++++++++++ RNS/Utilities/rnsd.py | 52 +++++++++++++++++++++++++++------------ RNS/Utilities/rnstatus.py | 38 +++++++++++++++------------- 4 files changed, 91 insertions(+), 34 deletions(-) diff --git a/RNS/Discovery.py b/RNS/Discovery.py index 9c1aeab..2ecbebe 100644 --- a/RNS/Discovery.py +++ b/RNS/Discovery.py @@ -161,6 +161,11 @@ class InterfaceAnnounceHandler: def received_announce(self, destination_hash, announced_identity, app_data): try: + discovery_sources = RNS.Reticulum.interface_discovery_sources() + if discovery_sources and not announced_identity.hash in discovery_sources: + RNS.log(f"Interface discovered from non-authorized network identity {RNS.prettyhexrep(announced_identity.hash)}, ignoring", RNS.LOG_DEBUG) + return + if app_data and len(app_data) > self.stamper.STAMP_SIZE+1: flags = app_data[0] app_data = app_data[1:] @@ -314,12 +319,19 @@ class InterfaceDiscovery(): def list_discovered_interfaces(self): now = time.time() discovered_interfaces = [] + discovery_sources = RNS.Reticulum.interface_discovery_sources() for filename in os.listdir(self.storagepath): try: filepath = os.path.join(self.storagepath, filename) with open(filepath, "rb") as f: info = msgpack.unpackb(f.read()) + should_remove = False heard_delta = now-info["last_heard"] - if heard_delta > self.THRESHOLD_REMOVE: + + if heard_delta > self.THRESHOLD_REMOVE: should_remove = True + elif discovery_sources and not "network_id" in info: should_remove = True + elif discovery_sources and not bytes.fromhex(info["network_id"]) in discovery_sources: should_remove = True + + if should_remove: os.unlink(filepath) continue @@ -333,6 +345,7 @@ class InterfaceDiscovery(): except Exception as e: RNS.log(f"Error while loading discovered interface data: {e}", RNS.LOG_ERROR) + RNS.log(f"The interface data file {os.path.join(self.storagepath, filename)} may be corrupt", RNS.LOG_ERROR) RNS.trace_exception(e) discovered_interfaces.sort(key=lambda info: (info["status_code"], info["value"], info["last_heard"]), reverse=True) diff --git a/RNS/Reticulum.py b/RNS/Reticulum.py index 12e5702..c79d324 100755 --- a/RNS/Reticulum.py +++ b/RNS/Reticulum.py @@ -262,6 +262,7 @@ class Reticulum: Reticulum.__required_discovery_value = None Reticulum.__publish_blackhole = False Reticulum.__blackhole_sources = [] + Reticulum.__interface_sources = [] Reticulum.panic_on_interface_error = False @@ -565,6 +566,15 @@ class Reticulum: try: source_identity_hash = bytes.fromhex(hexhash) except Exception as e: raise ValueError(f"Invalid identity hash for remote blackhole source: {hexhash}") if not source_identity_hash in Reticulum.__blackhole_sources: Reticulum.__blackhole_sources.append(source_identity_hash) + + if option == "interface_discovery_sources": + v = self.config["reticulum"].as_list(option) + for hexhash in v: + dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 + if len(hexhash) != dest_len: raise ValueError(f"Identity hash length for interface discovery source {hexhash} is invalid, must be {dest_len} hexadecimal characters ({dest_len//2} bytes).") + try: source_identity_hash = bytes.fromhex(hexhash) + except Exception as e: raise ValueError(f"Invalid identity hash for interface discovery source: {hexhash}") + if not source_identity_hash in Reticulum.__interface_sources: Reticulum.__interface_sources.append(source_identity_hash) if RNS.compiled: RNS.log("Reticulum running in compiled mode", RNS.LOG_DEBUG) else: RNS.log("Reticulum running in interpreted mode", RNS.LOG_DEBUG) @@ -1524,6 +1534,16 @@ class Reticulum: """ return Reticulum.__blackhole_sources + @staticmethod + def interface_discovery_sources(): + """ + Returns the list of network identity hashes from which + interfaces are discovered. + + :returns: A list of identity hashes. + """ + return Reticulum.__interface_sources + # Default configuration file: __default_rns_config__ = '''# This is the default Reticulum config file. # You should probably edit it to include any additional, diff --git a/RNS/Utilities/rnsd.py b/RNS/Utilities/rnsd.py index 807cabf..b365ecc 100755 --- a/RNS/Utilities/rnsd.py +++ b/RNS/Utilities/rnsd.py @@ -160,22 +160,6 @@ instance_name = default # remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc -# You can configure whether Reticulum should discover -# available interfaces from other Transport Instances over -# the network. If this option is enabled, Reticulum will -# collect interface information discovered from the network. - -# discover_interfaces = No - - -# To prevent interface discovery spamming, a valid crypto- -# graphic stamp is required per announced interface. You -# can configure the minimum required value to accept as -# valid for discovered interfaces. - -# required_discovery_value = 14 - - # For easier management, discovery and configuration of # networks with many individual transport instances, # you can specify a network identity to be used across @@ -189,6 +173,42 @@ instance_name = default # network_identity = ~/.reticulum/storage/identity/network +# You can configure whether Reticulum should discover +# available interfaces from other Transport Instances over +# the network. If this option is enabled, Reticulum will +# collect interface information discovered from the network. + +# discover_interfaces = No + + +# If you only want to discover interfaces from specific +# networks, you can provide a list of network identities +# from which to discover interfaces. If this option is not +# provided, interfaces will be discovered from all transport +# instances on all connected networks. + +# interface_discovery_sources = 78616ff7c4b8d3886d67d494b440f333, cb127015e13aa6ea1e0a606cdc9123d0 + + +# It is possible to automatically bring up and connect new +# interfaces discovered over the network. This option is +# disabled by default, but allows you to specify a maximum +# number of discovered interfaces to automatically connect. +# Additionally, if this option is enabled, Reticulum will +# also try to autoconnect available auto-discovered inter- +# faces on startup, up to the maximum number specified. + +# autoconnect_discovered_interfaces = 0 + + +# To prevent interface discovery spamming, a valid crypto- +# graphic stamp is required per announced interface. You +# can configure the minimum required value to accept as +# valid for discovered interfaces. + +# required_discovery_value = 14 + + # You can configure Reticulum to panic and forcibly close # if an unrecoverable interface error occurs, such as the # hardware device for an interface disappearing. This is diff --git a/RNS/Utilities/rnstatus.py b/RNS/Utilities/rnstatus.py index 6528680..bcee142 100644 --- a/RNS/Utilities/rnstatus.py +++ b/RNS/Utilities/rnstatus.py @@ -215,29 +215,33 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= location = f"{lat}, {lon}{height}" else: location = "Unknown" + transport_id = None network = None + if "transport_id" in i: transport_id = i["transport_id"] if "transport_id" in i and "network_id" in i and i["transport_id"] != i["network_id"]: network = i["network_id"] - if idx > 0: print("\n"+"="*32+"\n") - if network: print(f"Network ID : {network}") - print(f"Name : {name}") - print(f"Type : {if_type}") - print(f"Status : {status_display}") - print(f"Transport : {transport_str}") - print(f"Distance : {i['hops']} hop{'' if i['hops'] == 1 else 's'}") - print(f"Discovered : {discovered_display}") - print(f"Last Heard : {last_heard_display}") - print(f"Location : {location}") + if idx > 0: print("\n"+"="*32+"\n") + if network: print(f"Network ID : {network}") + if transport_id: print(f"Transport ID : {transport_id}") - if "frequency" in i: print(f"Frequency : {i['frequency']:,} Hz") - if "bandwidth" in i: print(f"Bandwidth : {i['bandwidth']:,} Hz") - if "sf" in i: print(f"Sprd.Factor : {i['sf']}") - if "cr" in i: print(f"Coding Rate : {i['cr']}") - if "modulation" in i: print(f"Modulation : {i['modulation']}") - if "reachable_on" in i: print(f"Address : {i['reachable_on']}:{i['port']}") + print(f"Name : {name}") + print(f"Type : {if_type}") + print(f"Status : {status_display}") + print(f"Transport : {transport_str}") + print(f"Distance : {i['hops']} hop{'' if i['hops'] == 1 else 's'}") + print(f"Discovered : {discovered_display}") + print(f"Last Heard : {last_heard_display}") + print(f"Location : {location}") - print(f"Stamp Value : {i['value']}") + if "frequency" in i: print(f"Frequency : {i['frequency']:,} Hz") + if "bandwidth" in i: print(f"Bandwidth : {i['bandwidth']:,} Hz") + if "sf" in i: print(f"Sprd. Factor : {i['sf']}") + if "cr" in i: print(f"Coding Rate : {i['cr']}") + if "modulation" in i: print(f"Modulation : {i['modulation']}") + if "reachable_on" in i: print(f"Address : {i['reachable_on']}:{i['port']}") + + print(f"Stamp Value : {i['value']}") print(f"\nConfiguration Entry:") config_lines = i["config_entry"].split('\n')