diff --git a/RNS/Reticulum.py b/RNS/Reticulum.py index 6453d3a..2a335e7 100755 --- a/RNS/Reticulum.py +++ b/RNS/Reticulum.py @@ -216,6 +216,7 @@ class Reticulum: Reticulum.identitypath = Reticulum.configdir+"/storage/identities" Reticulum.__transport_enabled = False + Reticulum.__remote_management_enabled = False Reticulum.__use_implicit_proof = True Reticulum.__allow_probes = False @@ -346,6 +347,7 @@ class Reticulum: self.is_standalone_instance = False self.is_connected_to_shared_instance = True Reticulum.__transport_enabled = False + Reticulum.__remote_management_enabled = False Reticulum.__allow_probes = False RNS.log("Connected to locally available Reticulum instance via: "+str(interface), RNS.LOG_DEBUG) except Exception as e: @@ -396,6 +398,23 @@ class Reticulum: v = self.config["reticulum"].as_bool(option) if v == True: Reticulum.__transport_enabled = True + if option == "enable_remote_management": + v = self.config["reticulum"].as_bool(option) + if v == True: + Reticulum.__remote_management_enabled = True + if option == "remote_management_allowed": + 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("Identity hash length for remote management ACL "+str(hexhash)+" is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)) + try: + allowed_hash = bytes.fromhex(hexhash) + except Exception as e: + raise ValueError("Invalid identity hash for remote management ACL: "+str(hexhash)) + + if not allowed_hash in RNS.Transport.remote_management_allowed: + RNS.Transport.remote_management_allowed.append(allowed_hash) if option == "respond_to_probes": v = self.config["reticulum"].as_bool(option) if v == True: @@ -1516,6 +1535,19 @@ class Reticulum: """ return Reticulum.__transport_enabled + @staticmethod + def remote_management_enabled(): + """ + Returns whether remote management is enabled for the + running instance. + + When remote management is enabled, authenticated peers + can remotely query and manage this instance. + + :returns: True if remote management is enabled, False if not. + """ + return Reticulum.__remote_management_enabled + @staticmethod def probe_destination_enabled(): return Reticulum.__allow_probes diff --git a/RNS/Transport.py b/RNS/Transport.py index 889ad67..ff0d56a 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -35,109 +35,110 @@ class Transport: Transport system of Reticulum. """ # Constants - BROADCAST = 0x00; - TRANSPORT = 0x01; - RELAY = 0x02; - TUNNEL = 0x03; - types = [BROADCAST, TRANSPORT, RELAY, TUNNEL] + BROADCAST = 0x00; + TRANSPORT = 0x01; + RELAY = 0x02; + TUNNEL = 0x03; + types = [BROADCAST, TRANSPORT, RELAY, TUNNEL] - REACHABILITY_UNREACHABLE = 0x00 - REACHABILITY_DIRECT = 0x01 - REACHABILITY_TRANSPORT = 0x02 + REACHABILITY_UNREACHABLE = 0x00 + REACHABILITY_DIRECT = 0x01 + REACHABILITY_TRANSPORT = 0x02 APP_NAME = "rnstransport" - PATHFINDER_M = 128 # Max hops + PATHFINDER_M = 128 # Max hops """ Maximum amount of hops that Reticulum will transport a packet. """ - PATHFINDER_R = 1 # Retransmit retries - PATHFINDER_G = 5 # Retry grace period - PATHFINDER_RW = 0.5 # Random window for announce rebroadcast - PATHFINDER_E = 60*60*24*7 # Path expiration of one week - AP_PATH_TIME = 60*60*24 # Path expiration of one day for Access Point paths - ROAMING_PATH_TIME = 60*60*6 # Path expiration of 6 hours for Roaming paths + PATHFINDER_R = 1 # Retransmit retries + PATHFINDER_G = 5 # Retry grace period + PATHFINDER_RW = 0.5 # Random window for announce rebroadcast + PATHFINDER_E = 60*60*24*7 # Path expiration of one week + AP_PATH_TIME = 60*60*24 # Path expiration of one day for Access Point paths + ROAMING_PATH_TIME = 60*60*6 # Path expiration of 6 hours for Roaming paths # TODO: Calculate an optimal number for this in # various situations - LOCAL_REBROADCASTS_MAX = 2 # How many local rebroadcasts of an announce is allowed + LOCAL_REBROADCASTS_MAX = 2 # How many local rebroadcasts of an announce is allowed - PATH_REQUEST_TIMEOUT = 15 # Default timuout for client path requests in seconds - PATH_REQUEST_GRACE = 0.4 # Grace time before a path announcement is made, allows directly reachable peers to respond first - PATH_REQUEST_RG = 0.6 # Extra grace time for roaming-mode interfaces to allow more suitable peers to respond first - PATH_REQUEST_MI = 20 # Minimum interval in seconds for automated path requests + PATH_REQUEST_TIMEOUT = 15 # Default timuout for client path requests in seconds + PATH_REQUEST_GRACE = 0.4 # Grace time before a path announcement is made, allows directly reachable peers to respond first + PATH_REQUEST_RG = 0.6 # Extra grace time for roaming-mode interfaces to allow more suitable peers to respond first + PATH_REQUEST_MI = 20 # Minimum interval in seconds for automated path requests - STATE_UNKNOWN = 0x00 - STATE_UNRESPONSIVE = 0x01 - STATE_RESPONSIVE = 0x02 + STATE_UNKNOWN = 0x00 + STATE_UNRESPONSIVE = 0x01 + STATE_RESPONSIVE = 0x02 - LINK_TIMEOUT = RNS.Link.STALE_TIME * 1.25 - REVERSE_TIMEOUT = 30*60 # Reverse table entries are removed after 30 minutes - DESTINATION_TIMEOUT = 60*60*24*7 # Destination table entries are removed if unused for one week - MAX_RECEIPTS = 1024 # Maximum number of receipts to keep track of - MAX_RATE_TIMESTAMPS = 16 # Maximum number of announce timestamps to keep per destination - PERSIST_RANDOM_BLOBS = 32 # Maximum number of random blobs per destination to persist to disk - MAX_RANDOM_BLOBS = 64 # Maximum number of random blobs per destination to keep in memory + LINK_TIMEOUT = RNS.Link.STALE_TIME * 1.25 + REVERSE_TIMEOUT = 30*60 # Reverse table entries are removed after 30 minutes + DESTINATION_TIMEOUT = 60*60*24*7 # Destination table entries are removed if unused for one week + MAX_RECEIPTS = 1024 # Maximum number of receipts to keep track of + MAX_RATE_TIMESTAMPS = 16 # Maximum number of announce timestamps to keep per destination + PERSIST_RANDOM_BLOBS = 32 # Maximum number of random blobs per destination to persist to disk + MAX_RANDOM_BLOBS = 64 # Maximum number of random blobs per destination to keep in memory - interfaces = [] # All active interfaces - destinations = [] # All active destinations - pending_links = [] # Links that are being established - active_links = [] # Links that are active - packet_hashlist = [] # A list of packet hashes for duplicate detection - receipts = [] # Receipts of all outgoing packets for proof processing + interfaces = [] # All active interfaces + destinations = [] # All active destinations + pending_links = [] # Links that are being established + active_links = [] # Links that are active + packet_hashlist = [] # A list of packet hashes for duplicate detection + receipts = [] # Receipts of all outgoing packets for proof processing # TODO: "destination_table" should really be renamed to "path_table" # Notes on memory usage: 1 megabyte of memory can store approximately # 55.100 path table entries or approximately 22.300 link table entries. - announce_table = {} # A table for storing announces currently waiting to be retransmitted - destination_table = {} # A lookup table containing the next hop to a given destination - reverse_table = {} # A lookup table for storing packet hashes used to return proofs and replies - link_table = {} # A lookup table containing hops for links - held_announces = {} # A table containing temporarily held announce-table entries - announce_handlers = [] # A table storing externally registered announce handlers - tunnels = {} # A table storing tunnels to other transport instances - announce_rate_table = {} # A table for keeping track of announce rates - path_requests = {} # A table for storing path request timestamps - path_states = {} # A table for keeping track of path states + announce_table = {} # A table for storing announces currently waiting to be retransmitted + destination_table = {} # A lookup table containing the next hop to a given destination + reverse_table = {} # A lookup table for storing packet hashes used to return proofs and replies + link_table = {} # A lookup table containing hops for links + held_announces = {} # A table containing temporarily held announce-table entries + announce_handlers = [] # A table storing externally registered announce handlers + tunnels = {} # A table storing tunnels to other transport instances + announce_rate_table = {} # A table for keeping track of announce rates + path_requests = {} # A table for storing path request timestamps + path_states = {} # A table for keeping track of path states - discovery_path_requests = {} # A table for keeping track of path requests on behalf of other nodes - discovery_pr_tags = [] # A table for keeping track of tagged path requests - max_pr_tags = 32000 # Maximum amount of unique path request tags to remember + discovery_path_requests = {} # A table for keeping track of path requests on behalf of other nodes + discovery_pr_tags = [] # A table for keeping track of tagged path requests + max_pr_tags = 32000 # Maximum amount of unique path request tags to remember # Transport control destinations are used # for control purposes like path requests - control_destinations = [] - control_hashes = [] + control_destinations = [] + control_hashes = [] + remote_management_allowed = [] # Interfaces for communicating with # local clients connected to a shared # Reticulum instance - local_client_interfaces = [] + local_client_interfaces = [] - local_client_rssi_cache = [] - local_client_snr_cache = [] - local_client_q_cache = [] - LOCAL_CLIENT_CACHE_MAXSIZE = 512 + local_client_rssi_cache = [] + local_client_snr_cache = [] + local_client_q_cache = [] + LOCAL_CLIENT_CACHE_MAXSIZE = 512 pending_local_path_requests = {} - start_time = None - jobs_locked = False - jobs_running = False - job_interval = 0.250 - links_last_checked = 0.0 - links_check_interval = 1.0 - receipts_last_checked = 0.0 - receipts_check_interval = 1.0 - announces_last_checked = 0.0 - announces_check_interval = 1.0 - hashlist_maxsize = 1000000 - tables_last_culled = 0.0 - tables_cull_interval = 5.0 - interface_last_jobs = 0.0 - interface_jobs_interval = 5.0 + start_time = None + jobs_locked = False + jobs_running = False + job_interval = 0.250 + links_last_checked = 0.0 + links_check_interval = 1.0 + receipts_last_checked = 0.0 + receipts_check_interval = 1.0 + announces_last_checked = 0.0 + announces_check_interval = 1.0 + hashlist_maxsize = 1000000 + tables_last_culled = 0.0 + tables_cull_interval = 5.0 + interface_last_jobs = 0.0 + interface_jobs_interval = 5.0 identity = None @@ -179,6 +180,13 @@ class Transport: Transport.control_destinations.append(Transport.tunnel_synthesize_handler) Transport.control_hashes.append(Transport.tunnel_synthesize_destination.hash) + if RNS.Reticulum.remote_management_enabled() and not Transport.owner.is_connected_to_shared_instance: + Transport.remote_management_destination = RNS.Destination(Transport.identity, RNS.Destination.IN, RNS.Destination.SINGLE, Transport.APP_NAME, "remote", "management") + Transport.remote_management_destination.register_request_handler("/status", response_generator = Transport.remote_status_handler, allow = RNS.Destination.ALLOW_LIST, allowed_list=Transport.remote_management_allowed) + Transport.control_destinations.append(Transport.remote_management_destination) + Transport.control_hashes.append(Transport.remote_management_destination.hash) + RNS.log("Enabled remote management on "+str(Transport.remote_management_destination), RNS.LOG_NOTICE) + Transport.jobs_running = False thread = threading.Thread(target=Transport.jobloop, daemon=True) thread.start() @@ -2247,6 +2255,25 @@ class Transport: packet.send() Transport.path_requests[destination_hash] = time.time() + @staticmethod + def remote_status_handler(path, data, request_id, link_id, remote_identity, requested_at): + if remote_identity != None: + response = None + try: + if isinstance(data, list) and len(data) > 0: + response = [] + response.append(Transport.owner.get_interface_stats()) + if data[0] == True: + response.append(Transport.owner.get_link_count()) + + return response + + except Exception as e: + RNS.log("An error occurred while processing remote status request from "+RNS.prettyhexrep(remote_identity), RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) + + return None + @staticmethod def path_request_handler(data, packet): try: diff --git a/RNS/Utilities/rnstatus.py b/RNS/Utilities/rnstatus.py index d20a908..2f058d1 100644 --- a/RNS/Utilities/rnstatus.py +++ b/RNS/Utilities/rnstatus.py @@ -23,6 +23,9 @@ # SOFTWARE. import RNS +import os +import sys +import time import argparse from RNS._version import __version__ @@ -46,22 +49,135 @@ def size_str(num, suffix='B'): return "%.2f%s%s" % (num, last_unit, suffix) -def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, lstats=False, sorting=None, sort_reverse=False): +request_result = None +request_concluded = False +def get_remote_status(destination_hash, include_lstats, identity, no_output=False): + global request_result, request_concluded + link_count = None + + if not RNS.Transport.has_path(destination_hash): + if not no_output: + print("Path to "+RNS.prettyhexrep(destination_hash)+" requested", end=" ") + sys.stdout.flush() + RNS.Transport.request_path(destination_hash) + pr_time = time.time() + pr_timeout = 10 + while not RNS.Transport.has_path(destination_hash): + time.sleep(0.1) + if time.time() - pr_time > pr_timeout: + if not no_output: + print("\r \r", end="") + print("Path request timed out") + exit(12) + + remote_identity = RNS.Identity.recall(destination_hash) + + def remote_link_closed(link): + if link.teardown_reason == RNS.Link.TIMEOUT: + if not no_output: + print("\r \r", end="") + print("The link timed out, exiting now") + elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: + if not no_output: + print("\r \r", end="") + print("The link was closed by the server, exiting now") + else: + if not no_output: + print("\r \r", end="") + print("Link closed unexpectedly, exiting now") + exit(10) + + def request_failed(request_receipt): + global request_result, request_concluded + if not no_output: + print("\r \r", end="") + print("The remote status request failed. Likely authentication failure.") + request_concluded = True + + def got_response(request_receipt): + global request_result, request_concluded + response = request_receipt.response + if isinstance(response, list): + status = response[0] + if len(response) > 1: + link_count = response[1] + else: + link_count = None + + request_result = (status, link_count) + + request_concluded = True + + def remote_link_established(link): + if not no_output: + print("\r \r", end="") + print("Sending request...", end=" ") + sys.stdout.flush() + link.identify(identity) + link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed) + + if not no_output: + print("\r \r", end="") + print("Establishing link with remote transport instance...", end=" ") + sys.stdout.flush() + + remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management") + link = RNS.Link(remote_destination) + link.set_link_established_callback(remote_link_established) + link.set_link_closed_callback(remote_link_closed) + + while not request_concluded: + time.sleep(0.1) + + if request_result != None: + print("\r \r", end="") + + return request_result + +def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, + lstats=False, sorting=None, sort_reverse=False, remote=None, management_identity=None): reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity) link_count = None - if lstats: + stats = None + if remote: try: - link_count = reticulum.get_link_count() + dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 + if len(remote) != dest_len: + raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)) + try: + identity_hash = bytes.fromhex(remote) + destination_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash) + except Exception as e: + raise ValueError("Invalid destination entered. Check your input.") + + identity = RNS.Identity.from_file(os.path.expanduser(management_identity)) + if identity == None: + raise ValueError("Could not load management identity from "+str(management_identity)) + + try: + remote_status = get_remote_status(destination_hash, lstats, identity, no_output=json) + if remote_status != None: + stats, link_count = remote_status + except Exception as e: + raise e + + except Exception as e: + print(str(e)) + exit(20) + + else: + if lstats: + try: + link_count = reticulum.get_link_count() + except Exception as e: + pass + + try: + stats = reticulum.get_interface_stats() except Exception as e: pass - stats = None - try: - stats = reticulum.get_interface_stats() - except Exception as e: - pass - if stats != None: if json: import json @@ -227,7 +343,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= print("\n Transport Instance "+RNS.prettyhexrep(stats["transport_id"])+" running") if "probe_responder" in stats and stats["probe_responder"] != None: print(" Probe responder at "+RNS.prettyhexrep(stats["probe_responder"])+ " active") - print(" Uptime is "+RNS.prettytime(stats["transport_uptime"])+lstr) + if "transport_uptime" in stats and stats["transport_uptime"] != None: + print(" Uptime is "+RNS.prettytime(stats["transport_uptime"])+lstr) else: if lstr != "": print(f"\n{lstr}") @@ -235,7 +352,10 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json= print("") else: - print("Could not get RNS status") + if not remote: + print("Could not get RNS status") + else: + print("Could not get RNS status from remote transport instance "+RNS.prettyhexrep(identity_hash)) def main(): try: @@ -292,6 +412,26 @@ def main(): default=False ) + parser.add_argument( + "-R", + "--remote", + action="store", + metavar="hash", + help="transport identity hash of remote instance to get status from", + default=None, + type=str + ) + + parser.add_argument( + "-i", + "--identity", + action="store", + metavar="path", + help="path to identity used for remote management", + default=None, + type=str + ) + parser.add_argument('-v', '--verbose', action='count', default=0) parser.add_argument("filter", nargs="?", default=None, help="only display interfaces with names including filter", type=str) @@ -313,6 +453,8 @@ def main(): lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, + remote=args.remote, + management_identity=args.identity, ) except KeyboardInterrupt: