From 13aebeecf96c53346cf43a710d49cd9a21fb3494 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 2 Jan 2026 17:16:24 +0100 Subject: [PATCH] Implemented network identity handling --- RNS/Discovery.py | 59 ++++++++++++++++++++++++--------------- RNS/Reticulum.py | 27 ++++++++++++++++++ RNS/Transport.py | 23 +++++++++++++-- RNS/Utilities/rnsd.py | 13 +++++++++ RNS/Utilities/rnstatus.py | 10 +++++-- 5 files changed, 104 insertions(+), 28 deletions(-) diff --git a/RNS/Discovery.py b/RNS/Discovery.py index af6f81e..9c1aeab 100644 --- a/RNS/Discovery.py +++ b/RNS/Discovery.py @@ -5,6 +5,7 @@ import threading from .vendor import umsgpack as msgpack NAME = 0xFF +TRANSPORT_ID = 0xFE INTERFACE_TYPE = 0x00 TRANSPORT = 0x01 REACHABLE_ON = 0x02 @@ -45,7 +46,10 @@ class InterfaceAnnouncer(): self.stamper = LXStamper self.stamp_cache = {} - self.discovery_destination = RNS.Destination(self.owner.identity, RNS.Destination.IN, RNS.Destination.SINGLE, + if self.owner.has_network_identity(): identity = self.owner.network_identity + else: identity = self.owner.identity + + self.discovery_destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "discovery", "interface") def start(self): @@ -85,12 +89,14 @@ class InterfaceAnnouncer(): def get_interface_announce_data(self, interface): interface_type = type(interface).__name__ - stamp_value = interface.discovery_stamp_value if interface.discovery_stamp_value else self.DEFAULT_STAMP_VALUE + stamp_value = interface.discovery_stamp_value if interface.discovery_stamp_value else self.DEFAULT_STAMP_VALUE + if not interface_type in self.DISCOVERABLE_INTERFACE_TYPES: return None else: flags = bytes([0x00]) info = {INTERFACE_TYPE: interface_type, TRANSPORT: RNS.Reticulum.transport_enabled(), + TRANSPORT_ID: RNS.Transport.identity.hash, NAME: self.sanitize(interface.discovery_name), LATITUDE: interface.discovery_latitude, LONGITUDE: interface.discovery_longitude, @@ -125,8 +131,8 @@ class InterfaceAnnouncer(): info[IFAC_NETNAME] = self.sanitize(interface.ifac_netname) info[IFAC_NETKEY] = self.sanitize(interface.ifac_netkey) - packed = msgpack.packb(info) - infohash = RNS.Identity.full_hash(packed) + packed = msgpack.packb(info) + infohash = RNS.Identity.full_hash(packed) if infohash in self.stamp_cache: return flags+packed+self.stamp_cache[infohash] else: stamp, v = self.stamper.generate_stamp(infohash, stamp_cost=stamp_value, expand_rounds=self.WORKBLOCK_EXPAND_ROUNDS) @@ -137,6 +143,9 @@ class InterfaceAnnouncer(): return flags+packed+stamp class InterfaceAnnounceHandler: + FLAG_SIGNED = 0b00000001 + FLAG_ENCRYPTED = 0b00000010 + def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None): import importlib.util if importlib.util.find_spec('LXMF') != None: from LXMF import LXStamper @@ -153,7 +162,11 @@ class InterfaceAnnounceHandler: def received_announce(self, destination_hash, announced_identity, app_data): try: if app_data and len(app_data) > self.stamper.STAMP_SIZE+1: + flags = app_data[0] app_data = app_data[1:] + signed = flags & self.FLAG_SIGNED + encrypted = flags & self.FLAG_ENCRYPTED + stamp = app_data[-self.stamper.STAMP_SIZE:] packed = app_data[:-self.stamper.STAMP_SIZE] infohash = RNS.Identity.full_hash(packed) @@ -170,18 +183,19 @@ class InterfaceAnnounceHandler: info = None unpacked = msgpack.unpackb(packed) if INTERFACE_TYPE in unpacked: - interface_type = unpacked[INTERFACE_TYPE] - info = {"type": interface_type, - "transport": unpacked[TRANSPORT], - "name": unpacked[NAME] or f"Discovered {interface_type}", - "received": time.time(), - "stamp": stamp, - "value": value, - "identity": RNS.hexrep(announced_identity.hash, delimit=False), - "hops": RNS.Transport.hops_to(destination_hash), - "latitude": unpacked[LATITUDE], - "longitude": unpacked[LONGITUDE], - "height": unpacked[HEIGHT]} + interface_type = unpacked[INTERFACE_TYPE] + info = {"type": interface_type, + "transport": unpacked[TRANSPORT], + "name": unpacked[NAME] or f"Discovered {interface_type}", + "received": time.time(), + "stamp": stamp, + "value": value, + "transport_id": RNS.hexrep(unpacked[TRANSPORT_ID], delimit=False), + "network_id": RNS.hexrep(announced_identity.hash, delimit=False), + "hops": RNS.Transport.hops_to(destination_hash), + "latitude": unpacked[LATITUDE], + "longitude": unpacked[LONGITUDE], + "height": unpacked[HEIGHT]} if IFAC_NETNAME in unpacked: info["ifac_netname"] = unpacked[IFAC_NETNAME] if IFAC_NETKEY in unpacked: info["ifac_netkey"] = unpacked[IFAC_NETKEY] @@ -195,7 +209,7 @@ class InterfaceAnnounceHandler: cfg_name = info["name"] cfg_remote = info["reachable_on"] cfg_port = info["port"] - cfg_identity = info["identity"] + cfg_identity = info["transport_id"] cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else "" @@ -207,7 +221,7 @@ class InterfaceAnnounceHandler: info["reachable_on"] = unpacked[REACHABLE_ON] cfg_name = info["name"] cfg_remote = info["reachable_on"] - cfg_identity = info["identity"] + cfg_identity = info["transport_id"] cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else "" @@ -225,7 +239,7 @@ class InterfaceAnnounceHandler: cfg_bandwidth = info["bandwidth"] cfg_sf = info["sf"] cfg_cr = info["cr"] - cfg_identity = info["identity"] + cfg_identity = info["transport_id"] cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else "" @@ -239,7 +253,7 @@ class InterfaceAnnounceHandler: info["channel"] = unpacked[CHANNEL] info["modulation"] = unpacked[MODULATION] cfg_name = info["name"] - cfg_identity = info["identity"] + cfg_identity = info["transport_id"] cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else "" @@ -255,7 +269,7 @@ class InterfaceAnnounceHandler: cfg_frequency = info["frequency"] cfg_bandwidth = info["bandwidth"] cfg_modulation = info["modulation"] - cfg_identity = info["identity"] + cfg_identity = info["transport_id"] cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else "" @@ -263,7 +277,7 @@ class InterfaceAnnounceHandler: cfg_identity_str = f"\n transport_identity = {cfg_identity}" info["config_entry"] = f"[[{cfg_name}]]\n type = KISSInterface\n enabled = yes\n port = \n # Frequency: {cfg_frequency}\n # Bandwidth: {cfg_bandwidth}\n # Modulation: {cfg_modulation}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}" - discovery_hash_material = info["identity"]+info["name"] + discovery_hash_material = info["transport_id"]+info["name"] info["discovery_hash"] = RNS.Identity.full_hash(discovery_hash_material.encode("utf-8")) RNS.log(f"Discovered interface with stamp value {value}: {info}", RNS.LOG_DEBUG) @@ -280,7 +294,6 @@ class InterfaceDiscovery(): STATUS_STALE = 0 STATUS_UNKNOWN = 100 STATUS_AVAILABLE = 1000 - STATUS_CODE_MAP = {"available": STATUS_AVAILABLE, "unknown": STATUS_UNKNOWN, "stale": STATUS_STALE} def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None, discover_interfaces=True): diff --git a/RNS/Reticulum.py b/RNS/Reticulum.py index d2763b8..12e5702 100755 --- a/RNS/Reticulum.py +++ b/RNS/Reticulum.py @@ -251,6 +251,7 @@ class Reticulum: Reticulum.blackholepath = Reticulum.configdir+"/storage/blackhole" Reticulum.interfacepath = Reticulum.configdir+"/interfaces" + Reticulum.__network_identity = None Reticulum.__transport_enabled = False Reticulum.__link_mtu_discovery = Reticulum.LINK_MTU_DISCOVERY Reticulum.__remote_management_enabled = False @@ -482,6 +483,29 @@ class Reticulum: v = self.config["reticulum"].as_bool(option) if v == True: Reticulum.__transport_enabled = True + if option == "network_identity": + if Reticulum.__network_identity == None: + path = self.config["reticulum"][option] + identitypath = os.path.expanduser(path) + try: + network_identity = None + if not os.path.isfile(identitypath): + network_identity = RNS.Identity() + network_identity.to_file(identitypath) + RNS.log(f"Network identity generated and persisted to {identitypath}", RNS.LOG_VERBOSE) + + else: + network_identity = RNS.Identity.from_file(identitypath) + RNS.log(f"Network identity loaded from {identitypath}", RNS.LOG_VERBOSE) + + if network_identity: + Reticulum.__network_identity = network_identity + RNS.Transport.set_network_identity(Reticulum.__network_identity) + + else: raise ValueError("Network identity initialisation failed") + + except Exception as e: raise ValueError(f"Could not set network identity from {path}: {e}") + if option == "link_mtu_discovery": v = self.config["reticulum"].as_bool(option) if v == True: Reticulum.__link_mtu_discovery = True @@ -669,6 +693,7 @@ class Reticulum: discovery_announce_interval = None discovery_stamp_value = None discovery_name = None + discovery_sign = False reachable_on = None publish_ifac = False latitude = None @@ -688,6 +713,7 @@ class Reticulum: if discovery_announce_interval == None: discovery_announce_interval = 6*60*60 if "discovery_stamp_value" in c: discovery_stamp_value = c.as_int("discovery_stamp_value") if "discovery_name" in c: discovery_name = c["discovery_name"] + if "discovery_sign" in c: discovery_sign = c.as_bool("discovery_sign") if "reachable_on" in c: reachable_on = c["reachable_on"] if "publish_ifac" in c: publish_ifac = c.as_bool("publish_ifac") if "latitude" in c: latitude = c.as_float("latitude") @@ -718,6 +744,7 @@ class Reticulum: interface.discovery_publish_ifac = publish_ifac interface.reachable_on = reachable_on interface.discovery_name = discovery_name + interface.discovery_sign = discovery_sign interface.discovery_stamp_value = discovery_stamp_value interface.discovery_latitude = latitude interface.discovery_longitude = longitude diff --git a/RNS/Transport.py b/RNS/Transport.py index 2dcc1d0..aa1c41b 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -173,7 +173,8 @@ class Transport: speed_tx = 0 traffic_captured = None - identity = None + identity = None + network_identity = None @staticmethod def start(reticulum_instance): @@ -231,6 +232,14 @@ class Transport: Transport.mgmt_hashes.append(Transport.blackhole_destination.hash) RNS.log(f"Enabled blackhole list publishing for transport identity {RNS.prettyhexrep(Transport.identity.hash)}", RNS.LOG_NOTICE) + if Transport.network_identity and not Transport.owner.is_connected_to_shared_instance: + Transport.instance_destination = RNS.Destination(Transport.network_identity, RNS.Destination.IN, RNS.Destination.SINGLE, Transport.APP_NAME, "network", "instance", RNS.hexrep(Transport.network_identity.hash, delimit=False)) + Transport.network_destination = RNS.Destination(Transport.network_identity, RNS.Destination.IN, RNS.Destination.SINGLE, Transport.APP_NAME, "network") + Transport.mgmt_destinations.append(Transport.instance_destination) + Transport.mgmt_destinations.append(Transport.network_destination) + Transport.mgmt_hashes.append(Transport.instance_destination) + Transport.mgmt_hashes.append(Transport.network_destination) + # Defer cleaning packet cache for 60 seconds Transport.cache_last_cleaned = time.time() + 60 @@ -374,6 +383,16 @@ class Transport: gc.collect() + @staticmethod + def set_network_identity(identity): + if not Transport.network_identity: + Transport.network_identity = identity + + @staticmethod + def has_network_identity(): + if Transport.network_identity: return True + else: return False + @staticmethod def prioritize_interfaces(): try: Transport.interfaces.sort(key=lambda interface: interface.bitrate, reverse=True) @@ -3172,7 +3191,7 @@ class Transport: if len(filename) != dest_len: raise ValueError(f"Identity hash length for blackhole source {filename} is invalid") source_identity_hash = bytes.fromhex(filename) if not source_identity_hash in RNS.Reticulum.blackhole_sources(): - RNS.log(f"Skipping disabled blackhole source {RNS.prettyhexrep(source_identity_hash)}", RNS.LOG_INFO) + RNS.log(f"Skipping disabled blackhole source {RNS.prettyhexrep(source_identity_hash)}", RNS.LOG_VERBOSE) continue sourcepath = os.path.join(RNS.Reticulum.blackholepath, filename) diff --git a/RNS/Utilities/rnsd.py b/RNS/Utilities/rnsd.py index 6d56569..807cabf 100755 --- a/RNS/Utilities/rnsd.py +++ b/RNS/Utilities/rnsd.py @@ -176,6 +176,19 @@ instance_name = default # 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 +# a set of instances. If sending interface discovery +# announces, these will all be signed by the specified +# network identity, and other nodes discovering your +# interfaces will be able to identify that they belong +# to the same network, even though they exist on different +# transport nodes. + +# network_identity = ~/.reticulum/storage/identity/network + + # 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 449f1ad..6528680 100644 --- a/RNS/Utilities/rnstatus.py +++ b/RNS/Utilities/rnstatus.py @@ -194,19 +194,19 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= name = i["name"] if_type = i["type"] status = i["status"] - + if status == "available": status_display = "Available" elif status == "unknown": status_display = "Unknown" elif status == "stale": status_display = "Stale" else: status_display = status - + now = time.time() dago = now-i["discovered"] hago = now-i["last_heard"] discovered_display = f"{RNS.prettytime(dago, compact=True)} ago" last_heard_display = f"{RNS.prettytime(hago, compact=True)} ago" transport_str = "Enabled" if i["transport"] else "Disabled" - + if i["latitude"] is not None and i["longitude"] is not None: lat = round(i["latitude"], 4) lon = round(i["longitude"], 4) @@ -215,8 +215,12 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= location = f"{lat}, {lon}{height}" else: location = "Unknown" + network = None + 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}")