From 38a73f9508bb9d3aa43da00306868fce6cd9e382 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sun, 16 Aug 2026 00:29:14 +0200 Subject: [PATCH] Added ability to include operator LXMF address in interface discovery info --- RNS/Discovery.py | 32 +++++++++++++++++++++++++++++--- RNS/Reticulum.py | 7 +++++++ RNS/Utilities/rnstatus.py | 10 ++++++---- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/RNS/Discovery.py b/RNS/Discovery.py index cdcb5345..c30fc6c7 100644 --- a/RNS/Discovery.py +++ b/RNS/Discovery.py @@ -27,6 +27,7 @@ SPREADINGFACTOR = 0x0B CODINGRATE = 0x0C MODULATION = 0x0D CHANNEL = 0x0E +OP_ADDR = 0xF0 APP_NAME = "rnstransport" @@ -123,7 +124,7 @@ class InterfaceAnnouncer(): interface.discovery_height = dhgt except Exception as e: - RNS.log(f"Error while getting reachable_on from executable at {interface.reachable_on}: {e}", RNS.LOG_ERROR) + RNS.log(f"Error while getting discovery location from executable at {interface.reachable_on}: {e}", RNS.LOG_ERROR) RNS.log(f"Aborting discovery announce", RNS.LOG_ERROR) return None @@ -136,11 +137,14 @@ class InterfaceAnnouncer(): LONGITUDE: interface.discovery_longitude, HEIGHT: interface.discovery_height} + if interface.discovery_lxmf_address: info[OP_ADDR] = interface.discovery_lxmf_address + if interface_type == "TCPClientInterface" and not interface.kiss_framing: RNS.log(f"Invalid interface discovery configuration for {interface}, aborting discovery announce", RNS.LOG_ERROR) return None if interface_type in ["BackboneInterface", "TCPServerInterface"]: + if not interface.reachable_on: return None reachable_on = self.sanitize(interface.reachable_on) if not RNS.vendor.platformutils.is_windows(): @@ -410,6 +414,11 @@ class InterfaceAnnounceHandler: discovery_hash_material = info["transport_id"]+info["name"] info["discovery_hash"] = RNS.Identity.full_hash(discovery_hash_material.encode("utf-8")) + if info and OP_ADDR in unpacked: + if type(unpacked[OP_ADDR]) not in [type(None), bytes]: raise ValueError("Invalid data in operator LXMF address field of announce") + if unpacked[OP_ADDR] and len(unpacked[OP_ADDR]) == RNS.Identity.TRUNCATED_HASHLENGTH//8: + info["operator_lxmf_address"] = RNS.hexrep(unpacked[OP_ADDR], delimit=False) + if self.callback and callable(self.callback): self.callback(info) except Exception as e: @@ -713,14 +722,22 @@ class InterfaceDiscovery(): return if interface_type == "I2PInterface": - RNS.log(f"Auto-connecting discovered I2P interfaces is not yet implemented, aborting auto-connect", RNS.LOG_WARNING) - RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_WARNING) + RNS.log(f"Auto-connecting discovered I2P interfaces is not yet implemented, aborting auto-connect", RNS.LOG_DEBUG) + RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_DEBUG) return if is_ygg_ipv6(info["reachable_on"]): # TODO: Somehow detect if yggdrasil is enabled on the system return + if is_onion_address(info["reachable_on"]): + # TODO: Somehow detect if TOR is enabled on the system + return + + if is_ip_address(info["reachable_on"]) and is_invalid_ip_address(info["reachable_on"]): + RNS.log(f"Not auto-connecting discovered interface with invalid IP address: {info["reachable_on"]}", RNS.LOG_DEBUG) + return + interface_name = info["name"] config_entry = info["config_entry"] interface_config = {} @@ -851,6 +868,15 @@ def is_ygg_ipv6(address_string): try: return ipaddress.ip_address(address_string) in ipaddress.IPv6Network("200::/7") except: return False +def is_onion_address(address_string): + try: return address_string.lower().endswith(".onion") + except: return False + +INVALID_IP_ADDRESSES = ["127.0.0.1", "0.0.0.0"] +def is_invalid_ip_address(address_string): + if not address_string in INVALID_IP_ADDRESSES: return False + else: return True + def is_hostname(hostname): if hostname[-1] == ".": hostname = hostname[:-1] if len(hostname) > 253: return False diff --git a/RNS/Reticulum.py b/RNS/Reticulum.py index 8ed957b5..b81bdb9e 100755 --- a/RNS/Reticulum.py +++ b/RNS/Reticulum.py @@ -860,6 +860,7 @@ class Reticulum: discovery_announce_interval = None discovery_stamp_value = None discovery_name = None + discovery_lxmf_address = None discovery_encrypt = False reachable_on = None publish_ifac = False @@ -891,6 +892,11 @@ class Reticulum: if "discovery_frequency" in c: discovery_frequency = c.as_int("discovery_frequency") if "discovery_bandwidth" in c: discovery_bandwidth = c.as_int("discovery_bandwidth") if "discovery_modulation" in c: discovery_modulation = c.as_int("discovery_modulation") + if "discovery_lxmf_address" in c: + if len(c["discovery_lxmf_address"]) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2: + try: discovery_lxmf_address = bytes.fromhex(c["discovery_lxmf_address"]) + except: RNS.log(f"Invalid interface discovery LXMF address: {c['discovery_lxmf_address']}", RNS.LOG_ERROR) + else: RNS.log(f"Invalid length for interface discovery LXMF address: {c['discovery_lxmf_address']}", RNS.LOG_ERROR) if not interface_mode in [Interface.Interface.MODE_GATEWAY, Interface.Interface.MODE_ACCESS_POINT, Interface.Interface.MODE_INTERNAL]: if not ignore_config_warnings: @@ -922,6 +928,7 @@ class Reticulum: interface.discovery_publish_ifac = publish_ifac interface.reachable_on = reachable_on interface.discovery_name = discovery_name + interface.discovery_lxmf_address = discovery_lxmf_address interface.discovery_encrypt = discovery_encrypt interface.discovery_stamp_value = discovery_stamp_value interface.discovery_location = discovery_location diff --git a/RNS/Utilities/rnstatus.py b/RNS/Utilities/rnstatus.py index 6802e61e..fc66b514 100644 --- a/RNS/Utilities/rnstatus.py +++ b/RNS/Utilities/rnstatus.py @@ -225,15 +225,18 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= location = f"{lat}, {lon}{height}" else: location = "Unknown" + lxmf_addr = None transport_id = None - network = None + network = None + if "operator_lxmf_address" in i: lxmf_addr = i['operator_lxmf_address'] 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 idx > 0: print("\n"+"="*47+"\n") if network: print(f"Network ID : {network}") if transport_id: print(f"Transport ID : {transport_id}") + if lxmf_addr: print(f"LXMF address : {lxmf_addr}") print(f"Name : {name}") print(f"Type : {if_type}") @@ -258,8 +261,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= config_lines = i["config_entry"].split('\n') for line in config_lines: print(f" {line}") - except Exception as e: - pass + except Exception as e: pass else: print(f"{'Name':<25} {'Type':<12} {'Status':<12} {'Last Heard':<12} {'Value':<8} {'Location':<15}")