# MIT License # # Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .vendor.platformutils import get_platform if get_platform() == "android": from .Interfaces import Interface from .Interfaces import LocalInterface from .Interfaces import AutoInterface from .Interfaces import TCPInterface from .Interfaces import UDPInterface from .Interfaces import I2PInterface from .Interfaces import RNodeMultiInterface from .Interfaces.Android import RNodeInterface from .Interfaces.Android import SerialInterface from .Interfaces.Android import KISSInterface else: from .Interfaces import * from .vendor.configobj import ConfigObj import configparser import multiprocessing.connection import signal import threading import atexit import struct import array import time import os import RNS class Reticulum: """ This class is used to initialise access to Reticulum within a program. You must create exactly one instance of this class before carrying out any other RNS operations, such as creating destinations or sending traffic. Every independently executed program must create their own instance of the Reticulum class, but Reticulum will automatically handle inter-program communication on the same system, and expose all connected programs to external interfaces as well. As soon as an instance of this class is created, Reticulum will start opening and configuring any hardware devices specified in the supplied configuration. Currently the first running instance must be kept running while other local instances are connected, as the first created instance will act as a master instance that directly communicates with external hardware such as modems, TNCs and radios. If a master instance is asked to exit, it will not exit until all client processes have terminated (unless killed forcibly). If you are running Reticulum on a system with several different programs that use RNS starting and terminating at different times, it will be advantageous to run a master RNS instance as a daemon for other programs to use on demand. """ # Future minimum will probably be locked in at 251 bytes to support # networks with segments of different MTUs. Absolute minimum is 219. MTU = 500 """ The MTU that Reticulum adheres to, and will expect other peers to adhere to. By default, the MTU is 500 bytes. In custom RNS network implementations, it is possible to change this value, but doing so will completely break compatibility with all other RNS networks. An identical MTU is a prerequisite for peers to communicate in the same network. Unless you really know what you are doing, the MTU should be left at the default value. """ MAX_QUEUED_ANNOUNCES = 16384 QUEUED_ANNOUNCE_LIFE = 60*60*24 ANNOUNCE_CAP = 2 """ The maximum percentage of interface bandwidth that, at any given time, may be used to propagate announces. If an announce was scheduled for broadcasting on an interface, but doing so would exceed the allowed bandwidth allocation, the announce will be queued for transmission when there is bandwidth available. Reticulum will always prioritise propagating announces with fewer hops, ensuring that distant, large networks with many peers on fast links don't overwhelm the capacity of smaller networks on slower mediums. If an announce remains queued for an extended amount of time, it will eventually be dropped. This value will be applied by default to all created interfaces, but it can be configured individually on a per-interface basis. In general, the global default setting should not be changed, and any alterations should be made on a per-interface basis instead. """ MINIMUM_BITRATE = 5 """ Minimum bitrate required across a medium for Reticulum to be able to successfully establish links. Currently 5 bits per second. """ # TODO: Let Reticulum somehow continously build a map of per-hop # latencies and use this map for global timeout calculation. DEFAULT_PER_HOP_TIMEOUT = 6 # Length of truncated hashes in bits. TRUNCATED_HASHLENGTH = 128 HEADER_MINSIZE = 2+1+(TRUNCATED_HASHLENGTH//8)*1 HEADER_MAXSIZE = 2+1+(TRUNCATED_HASHLENGTH//8)*2 IFAC_MIN_SIZE = 1 IFAC_SALT = bytes.fromhex("adf54d882c9a9b80771eb4995d702d4a3e733391b2a0f53f416d9f907e55cff8") MDU = MTU - HEADER_MAXSIZE - IFAC_MIN_SIZE RESOURCE_CACHE = 24*60*60 JOB_INTERVAL = 5*60 CLEAN_INTERVAL = 15*60 PERSIST_INTERVAL = 60*60*12 GRACIOUS_PERSIST_INTERVAL = 60*5 router = None config = None # The default configuration path will be expanded to a directory # named ".reticulum" inside the current users home directory userdir = os.path.expanduser("~") configdir = None configpath = "" storagepath = "" cachepath = "" interfacepath = "" __instance = None @staticmethod def exit_handler(): # This exit handler is called whenever Reticulum is asked to # shut down, and will in turn call exit handlers in other # classes, saving necessary information to disk and carrying # out cleanup operations. RNS.Transport.exit_handler() RNS.Identity.exit_handler() if RNS.profiler_ran: RNS.profiler_results() @staticmethod def sigint_handler(signal, frame): RNS.Transport.detach_interfaces() RNS.exit() @staticmethod def sigterm_handler(signal, frame): RNS.Transport.detach_interfaces() RNS.exit() @staticmethod def get_instance(): """ Return the currently running Reticulum instance """ return Reticulum.__instance def __init__(self,configdir=None, loglevel=None, logdest=None, verbosity=None, require_shared_instance=False): """ Initialises and starts a Reticulum instance. This must be done before any other operations, and Reticulum will not pass any traffic before being instantiated. :param configdir: Full path to a Reticulum configuration directory. """ if Reticulum.__instance != None: raise OSError("Attempt to reinitialise Reticulum, when it was already running") else: Reticulum.__instance = self RNS.vendor.platformutils.platform_checks() if configdir != None: Reticulum.configdir = configdir else: if os.path.isdir("/etc/reticulum") and os.path.isfile("/etc/reticulum/config"): Reticulum.configdir = "/etc/reticulum" elif os.path.isdir(Reticulum.userdir+"/.config/reticulum") and os.path.isfile(Reticulum.userdir+"/.config/reticulum/config"): Reticulum.configdir = Reticulum.userdir+"/.config/reticulum" else: Reticulum.configdir = Reticulum.userdir+"/.reticulum" if logdest == RNS.LOG_FILE: RNS.logdest = RNS.LOG_FILE RNS.logfile = Reticulum.configdir+"/logfile" Reticulum.configpath = Reticulum.configdir+"/config" Reticulum.storagepath = Reticulum.configdir+"/storage" Reticulum.cachepath = Reticulum.configdir+"/storage/cache" Reticulum.resourcepath = Reticulum.configdir+"/storage/resources" Reticulum.identitypath = Reticulum.configdir+"/storage/identities" Reticulum.interfacepath = Reticulum.configdir+"/interfaces" Reticulum.__transport_enabled = False Reticulum.__remote_management_enabled = False Reticulum.__use_implicit_proof = True Reticulum.__allow_probes = False Reticulum.panic_on_interface_error = False self.local_interface_port = 37428 self.local_control_port = 37429 self.share_instance = True self.rpc_listener = None self.rpc_key = None self.ifac_salt = Reticulum.IFAC_SALT self.requested_loglevel = loglevel self.requested_verbosity = verbosity if self.requested_loglevel != None: if self.requested_loglevel > RNS.LOG_EXTREME: self.requested_loglevel = RNS.LOG_EXTREME if self.requested_loglevel < RNS.LOG_CRITICAL: self.requested_loglevel = RNS.LOG_CRITICAL RNS.loglevel = self.requested_loglevel self.is_shared_instance = False self.require_shared = require_shared_instance self.is_connected_to_shared_instance = False self.is_standalone_instance = False self.jobs_thread = None self.last_data_persist = time.time() self.last_cache_clean = 0 if not os.path.isdir(Reticulum.storagepath): os.makedirs(Reticulum.storagepath) if not os.path.isdir(Reticulum.cachepath): os.makedirs(Reticulum.cachepath) if not os.path.isdir(Reticulum.resourcepath): os.makedirs(Reticulum.resourcepath) if not os.path.isdir(Reticulum.identitypath): os.makedirs(Reticulum.identitypath) if not os.path.isdir(Reticulum.interfacepath): os.makedirs(Reticulum.interfacepath) if os.path.isfile(self.configpath): try: self.config = ConfigObj(self.configpath) except Exception as e: RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR) RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR) RNS.panic() else: RNS.log("Could not load config file, creating default configuration file...") self.__create_default_config() RNS.log("Default config file created. Make any necessary changes in "+Reticulum.configdir+"/config and restart Reticulum if needed.") time.sleep(1.5) self.__apply_config() RNS.log("Configuration loaded from "+self.configpath, RNS.LOG_VERBOSE) RNS.Identity.load_known_destinations() RNS.Transport.start(self) self.rpc_addr = ("127.0.0.1", self.local_control_port) if self.rpc_key == None: self.rpc_key = RNS.Identity.full_hash(RNS.Transport.identity.get_private_key()) if self.is_shared_instance: self.rpc_listener = multiprocessing.connection.Listener(self.rpc_addr, authkey=self.rpc_key) thread = threading.Thread(target=self.rpc_loop) thread.daemon = True thread.start() atexit.register(Reticulum.exit_handler) signal.signal(signal.SIGINT, Reticulum.sigint_handler) signal.signal(signal.SIGTERM, Reticulum.sigterm_handler) def __start_jobs(self): if self.jobs_thread == None: RNS.Identity._clean_ratchets() self.jobs_thread = threading.Thread(target=self.__jobs) self.jobs_thread.daemon = True self.jobs_thread.start() def __jobs(self): while True: now = time.time() if now > self.last_cache_clean+Reticulum.CLEAN_INTERVAL: self.__clean_caches() self.last_cache_clean = time.time() if now > self.last_data_persist+Reticulum.PERSIST_INTERVAL: self.__persist_data() time.sleep(Reticulum.JOB_INTERVAL) def __start_local_interface(self): if self.share_instance: try: interface = LocalInterface.LocalServerInterface( RNS.Transport, self.local_interface_port ) interface.OUT = True if hasattr(Reticulum, "_force_shared_instance_bitrate"): interface.bitrate = Reticulum._force_shared_instance_bitrate interface._force_bitrate = Reticulum._force_shared_instance_bitrate RNS.log(f"Forcing shared instance bitrate of {RNS.prettyspeed(interface.bitrate)}", RNS.LOG_WARNING) if self.require_shared == True: interface.detach() self.is_shared_instance = True RNS.log("Existing shared instance required, but this instance started as shared instance. Aborting startup.", RNS.LOG_VERBOSE) else: RNS.Transport.interfaces.append(interface) self.is_shared_instance = True RNS.log("Started shared instance interface: "+str(interface), RNS.LOG_DEBUG) self.__start_jobs() except Exception as e: try: interface = LocalInterface.LocalClientInterface( RNS.Transport, "Local shared instance", self.local_interface_port) interface.target_port = self.local_interface_port interface.OUT = True if hasattr(Reticulum, "_force_shared_instance_bitrate"): interface.bitrate = Reticulum._force_shared_instance_bitrate interface._force_bitrate = True RNS.log(f"Forcing shared instance bitrate of {RNS.prettyspeed(interface.bitrate)}", RNS.LOG_WARNING) RNS.Transport.interfaces.append(interface) self.is_shared_instance = False 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: RNS.log("Local shared instance appears to be running, but it could not be connected", RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) self.is_shared_instance = False self.is_standalone_instance = True self.is_connected_to_shared_instance = False if self.is_shared_instance and self.require_shared: raise SystemError("No shared instance available, but application that started Reticulum required it") else: self.is_shared_instance = False self.is_standalone_instance = True self.is_connected_to_shared_instance = False self.__start_jobs() def __apply_config(self): if "logging" in self.config: for option in self.config["logging"]: value = self.config["logging"][option] if option == "loglevel" and self.requested_loglevel == None: RNS.loglevel = int(value) if self.requested_verbosity != None: RNS.loglevel += self.requested_verbosity if RNS.loglevel < 0: RNS.loglevel = 0 if RNS.loglevel > 7: RNS.loglevel = 7 if "reticulum" in self.config: for option in self.config["reticulum"]: value = self.config["reticulum"][option] if option == "share_instance": value = self.config["reticulum"].as_bool(option) self.share_instance = value if option == "shared_instance_port": value = int(self.config["reticulum"][option]) self.local_interface_port = value if option == "instance_control_port": value = int(self.config["reticulum"][option]) self.local_control_port = value if option == "rpc_key": try: value = bytes.fromhex(self.config["reticulum"][option]) self.rpc_key = value except Exception as e: RNS.log("Invalid shared instance RPC key specified, falling back to default key", RNS.LOG_ERROR) self.rpc_key = None if option == "enable_transport": 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: Reticulum.__allow_probes = True if option == "force_shared_instance_bitrate": v = self.config["reticulum"].as_int(option) Reticulum._force_shared_instance_bitrate = v if option == "panic_on_interface_error": v = self.config["reticulum"].as_bool(option) if v == True: Reticulum.panic_on_interface_error = True if option == "use_implicit_proof": v = self.config["reticulum"].as_bool(option) if v == True: Reticulum.__use_implicit_proof = True if v == False: Reticulum.__use_implicit_proof = False self.__start_local_interface() if self.is_shared_instance or self.is_standalone_instance: RNS.log("Bringing up system interfaces...", RNS.LOG_VERBOSE) interface_names = [] if "interfaces" in self.config: for name in self.config["interfaces"]: if not name in interface_names: c = self.config["interfaces"][name] interface_mode = Interface.Interface.MODE_FULL if "interface_mode" in c: c["interface_mode"] = str(c["interface_mode"]).lower() if c["interface_mode"] == "full": interface_mode = Interface.Interface.MODE_FULL elif c["interface_mode"] == "access_point" or c["interface_mode"] == "accesspoint" or c["interface_mode"] == "ap": interface_mode = Interface.Interface.MODE_ACCESS_POINT elif c["interface_mode"] == "pointtopoint" or c["interface_mode"] == "ptp": interface_mode = Interface.Interface.MODE_POINT_TO_POINT elif c["interface_mode"] == "roaming": interface_mode = Interface.Interface.MODE_ROAMING elif c["interface_mode"] == "boundary": interface_mode = Interface.Interface.MODE_BOUNDARY elif c["mode"] == "gateway" or c["mode"] == "gw": interface_mode = Interface.Interface.MODE_GATEWAY elif "mode" in c: c["mode"] = str(c["mode"]).lower() if c["mode"] == "full": interface_mode = Interface.Interface.MODE_FULL elif c["mode"] == "access_point" or c["mode"] == "accesspoint" or c["mode"] == "ap": interface_mode = Interface.Interface.MODE_ACCESS_POINT elif c["mode"] == "pointtopoint" or c["mode"] == "ptp": interface_mode = Interface.Interface.MODE_POINT_TO_POINT elif c["mode"] == "roaming": interface_mode = Interface.Interface.MODE_ROAMING elif c["mode"] == "boundary": interface_mode = Interface.Interface.MODE_BOUNDARY elif c["mode"] == "gateway" or c["mode"] == "gw": interface_mode = Interface.Interface.MODE_GATEWAY ifac_size = None if "ifac_size" in c: if c.as_int("ifac_size") >= Reticulum.IFAC_MIN_SIZE*8: ifac_size = c.as_int("ifac_size")//8 ifac_netname = None if "networkname" in c: if c["networkname"] != "": ifac_netname = c["networkname"] if "network_name" in c: if c["network_name"] != "": ifac_netname = c["network_name"] ifac_netkey = None if "passphrase" in c: if c["passphrase"] != "": ifac_netkey = c["passphrase"] if "pass_phrase" in c: if c["pass_phrase"] != "": ifac_netkey = c["pass_phrase"] ingress_control = True if "ingress_control" in c: ingress_control = c.as_bool("ingress_control") ic_max_held_announces = None if "ic_max_held_announces" in c: ic_max_held_announces = c.as_int("ic_max_held_announces") ic_burst_hold = None if "ic_burst_hold" in c: ic_burst_hold = c.as_float("ic_burst_hold") ic_burst_freq_new = None if "ic_burst_freq_new" in c: ic_burst_freq_new = c.as_float("ic_burst_freq_new") ic_burst_freq = None if "ic_burst_freq" in c: ic_burst_freq = c.as_float("ic_burst_freq") ic_new_time = None if "ic_new_time" in c: ic_new_time = c.as_float("ic_new_time") ic_burst_penalty = None if "ic_burst_penalty" in c: ic_burst_penalty = c.as_float("ic_burst_penalty") ic_held_release_interval = None if "ic_held_release_interval" in c: ic_held_release_interval = c.as_float("ic_held_release_interval") configured_bitrate = None if "bitrate" in c: if c.as_int("bitrate") >= Reticulum.MINIMUM_BITRATE: configured_bitrate = c.as_int("bitrate") announce_rate_target = None if "announce_rate_target" in c: if c.as_int("announce_rate_target") > 0: announce_rate_target = c.as_int("announce_rate_target") announce_rate_grace = None if "announce_rate_grace" in c: if c.as_int("announce_rate_grace") >= 0: announce_rate_grace = c.as_int("announce_rate_grace") announce_rate_penalty = None if "announce_rate_penalty" in c: if c.as_int("announce_rate_penalty") >= 0: announce_rate_penalty = c.as_int("announce_rate_penalty") if announce_rate_target != None and announce_rate_grace == None: announce_rate_grace = 0 if announce_rate_target != None and announce_rate_penalty == None: announce_rate_penalty = 0 announce_cap = Reticulum.ANNOUNCE_CAP/100.0 if "announce_cap" in c: if c.as_float("announce_cap") > 0 and c.as_float("announce_cap") <= 100: announce_cap = c.as_float("announce_cap")/100.0 try: def interface_post_init(interface): if interface != None: if "outgoing" in c and c.as_bool("outgoing") == False: interface.OUT = False else: interface.OUT = True interface.mode = interface_mode interface.announce_cap = announce_cap if configured_bitrate: interface.bitrate = configured_bitrate if ifac_size != None: interface.ifac_size = ifac_size else: interface.ifac_size = interface.DEFAULT_IFAC_SIZE interface.announce_rate_target = announce_rate_target interface.announce_rate_grace = announce_rate_grace interface.announce_rate_penalty = announce_rate_penalty interface.ingress_control = ingress_control if ic_max_held_announces != None: interface.ic_max_held_announces = ic_max_held_announces if ic_burst_hold != None: interface.ic_burst_hold = ic_burst_hold if ic_burst_freq_new != None: interface.ic_burst_freq_new = ic_burst_freq_new if ic_burst_freq != None: interface.ic_burst_freq = ic_burst_freq if ic_new_time != None: interface.ic_new_time = ic_new_time if ic_burst_penalty != None: interface.ic_burst_penalty = ic_burst_penalty if ic_held_release_interval != None: interface.ic_held_release_interval = ic_held_release_interval interface.ifac_netname = ifac_netname interface.ifac_netkey = ifac_netkey if interface.ifac_netname != None or interface.ifac_netkey != None: ifac_origin = b"" if interface.ifac_netname != None: ifac_origin += RNS.Identity.full_hash(interface.ifac_netname.encode("utf-8")) if interface.ifac_netkey != None: ifac_origin += RNS.Identity.full_hash(interface.ifac_netkey.encode("utf-8")) ifac_origin_hash = RNS.Identity.full_hash(ifac_origin) interface.ifac_key = RNS.Cryptography.hkdf( length=64, derive_from=ifac_origin_hash, salt=self.ifac_salt, context=None ) interface.ifac_identity = RNS.Identity.from_bytes(interface.ifac_key) interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key)) RNS.Transport.interfaces.append(interface) interface = None if (("interface_enabled" in c) and c.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True): interface_config = c interface_config["name"] = name interface_config["selected_interface_mode"] = interface_mode interface_config["configured_bitrate"] = configured_bitrate if c["type"] == "AutoInterface": interface = AutoInterface.AutoInterface(RNS.Transport, interface_config) interface_post_init(interface) if c["type"] == "UDPInterface": interface = UDPInterface.UDPInterface(RNS.Transport, interface_config) interface_post_init(interface) if c["type"] == "TCPServerInterface": if interface_mode == Interface.Interface.MODE_ACCESS_POINT: RNS.log(str(interface)+" does not support Access Point mode, reverting to default mode: Full", RNS.LOG_WARNING) interface_mode = Interface.Interface.MODE_FULL interface = TCPInterface.TCPServerInterface(RNS.Transport, interface_config) interface_post_init(interface) if c["type"] == "TCPClientInterface": if interface_mode == Interface.Interface.MODE_ACCESS_POINT: RNS.log(str(interface)+" does not support Access Point mode, reverting to default mode: Full", RNS.LOG_WARNING) interface_mode = Interface.Interface.MODE_FULL interface = TCPInterface.TCPClientInterface(RNS.Transport, interface_config) interface_post_init(interface) if c["type"] == "I2PInterface": if interface_mode == Interface.Interface.MODE_ACCESS_POINT: RNS.log(str(interface)+" does not support Access Point mode, reverting to default mode: Full", RNS.LOG_WARNING) interface_mode = Interface.Interface.MODE_FULL interface_config["storagepath"] = Reticulum.storagepath interface_config["ifac_netname"] = ifac_netname interface_config["ifac_netkey"] = ifac_netkey interface_config["ifac_size"] = ifac_size interface = I2PInterface.I2PInterface(RNS.Transport, interface_config) interface_post_init(interface) if c["type"] == "SerialInterface": interface = SerialInterface.SerialInterface(RNS.Transport, interface_config) interface_post_init(interface) if c["type"] == "PipeInterface": interface = PipeInterface.PipeInterface(RNS.Transport, interface_config) interface_post_init(interface) if c["type"] == "KISSInterface": interface = KISSInterface.KISSInterface(RNS.Transport, interface_config) interface_post_init(interface) if c["type"] == "AX25KISSInterface": interface = AX25KISSInterface.AX25KISSInterface(RNS.Transport, interface_config) interface_post_init(interface) if c["type"] == "RNodeInterface": interface = RNodeInterface.RNodeInterface(RNS.Transport, interface_config) interface_post_init(interface) if c["type"] == "RNodeMultiInterface": interface = RNodeMultiInterface.RNodeMultiInterface(RNS.Transport, interface_config) interface_post_init(interface) interface.start() if interface == None: # Interface was not handled by any internal interface types, # attempt to load and initialise it from user-supplied modules interface_type = c["type"] interface_file = f"{interface_type}.py" interface_path = os.path.join(self.interfacepath, interface_file) if not os.path.isfile(interface_path): RNS.log(f"Could not locate external interface module \"{interface_file}\" in \"{self.interfacepath}\"", RNS.LOG_ERROR) else: try: RNS.log(f"Loading external interface \"{interface_file}\" from \"{self.interfacepath}\"", RNS.LOG_NOTICE) interface_globals = {} interface_globals["Interface"] = Interface.Interface interface_globals["RNS"] = RNS with open(interface_path) as class_file: interface_code = class_file.read() exec(interface_code, interface_globals) interface_class = interface_globals["interface_class"] if interface_class != None: interface = interface_class(RNS.Transport, interface_config) interface_post_init(interface) except Exception as e: RNS.log(f"External interface initialisation failed for {interface_type} / {name}", RNS.LOG_ERROR) RNS.trace_exception(e) else: RNS.log("Skipping disabled interface \""+name+"\"", RNS.LOG_DEBUG) except Exception as e: RNS.log("The interface \""+name+"\" could not be created. Check your configuration file for errors!", RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.panic() else: RNS.log("The interface name \""+name+"\" was already used. Check your configuration file for errors!", RNS.LOG_ERROR) RNS.panic() RNS.log("System interfaces are ready", RNS.LOG_VERBOSE) def _add_interface(self, interface, mode = None, configured_bitrate=None, ifac_size=None, ifac_netname=None, ifac_netkey=None, announce_cap=None, announce_rate_target=None, announce_rate_grace=None, announce_rate_penalty=None): if not self.is_connected_to_shared_instance: if interface != None and issubclass(type(interface), RNS.Interfaces.Interface.Interface): if mode == None: mode = Interface.Interface.MODE_FULL interface.mode = mode if configured_bitrate: interface.bitrate = configured_bitrate if ifac_size != None: interface.ifac_size = ifac_size else: interface.ifac_size = 8 interface.announce_cap = announce_cap if announce_cap != None else Reticulum.ANNOUNCE_CAP/100.0 interface.announce_rate_target = announce_rate_target interface.announce_rate_grace = announce_rate_grace interface.announce_rate_penalty = announce_rate_penalty interface.ifac_netname = ifac_netname interface.ifac_netkey = ifac_netkey if interface.ifac_netname != None or interface.ifac_netkey != None: ifac_origin = b"" if interface.ifac_netname != None: ifac_origin += RNS.Identity.full_hash(interface.ifac_netname.encode("utf-8")) if interface.ifac_netkey != None: ifac_origin += RNS.Identity.full_hash(interface.ifac_netkey.encode("utf-8")) ifac_origin_hash = RNS.Identity.full_hash(ifac_origin) interface.ifac_key = RNS.Cryptography.hkdf( length=64, derive_from=ifac_origin_hash, salt=self.ifac_salt, context=None ) interface.ifac_identity = RNS.Identity.from_bytes(interface.ifac_key) interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key)) RNS.Transport.interfaces.append(interface) def _should_persist_data(self): if time.time() > self.last_data_persist+Reticulum.GRACIOUS_PERSIST_INTERVAL: self.__persist_data() def __persist_data(self): RNS.Transport.persist_data() RNS.Identity.persist_data() self.last_data_persist = time.time() def __clean_caches(self): RNS.log("Cleaning resource and packet caches...", RNS.LOG_EXTREME) now = time.time() # Clean resource caches for filename in os.listdir(self.resourcepath): try: if len(filename) == (RNS.Identity.HASHLENGTH//8)*2: filepath = self.resourcepath + "/" + filename mtime = os.path.getmtime(filepath) age = now - mtime if age > Reticulum.RESOURCE_CACHE: os.unlink(filepath) except Exception as e: RNS.log("Error while cleaning resources cache, the contained exception was: "+str(e), RNS.LOG_ERROR) # Clean packet caches for filename in os.listdir(self.cachepath): try: if len(filename) == (RNS.Identity.HASHLENGTH//8)*2: filepath = self.cachepath + "/" + filename mtime = os.path.getmtime(filepath) age = now - mtime if age > RNS.Transport.DESTINATION_TIMEOUT: os.unlink(filepath) except Exception as e: RNS.log("Error while cleaning resources cache, the contained exception was: "+str(e), RNS.LOG_ERROR) def __create_default_config(self): self.config = ConfigObj(__default_rns_config__) self.config.filename = Reticulum.configpath if not os.path.isdir(Reticulum.configdir): os.makedirs(Reticulum.configdir) self.config.write() def rpc_loop(self): while True: try: rpc_connection = self.rpc_listener.accept() call = rpc_connection.recv() if "get" in call: path = call["get"] if path == "interface_stats": rpc_connection.send(self.get_interface_stats()) if path == "path_table": mh = call["max_hops"] rpc_connection.send(self.get_path_table(max_hops=mh)) if path == "rate_table": rpc_connection.send(self.get_rate_table()) if path == "next_hop_if_name": rpc_connection.send(self.get_next_hop_if_name(call["destination_hash"])) if path == "next_hop": rpc_connection.send(self.get_next_hop(call["destination_hash"])) if path == "first_hop_timeout": rpc_connection.send(self.get_first_hop_timeout(call["destination_hash"])) if path == "link_count": rpc_connection.send(self.get_link_count()) if path == "packet_rssi": rpc_connection.send(self.get_packet_rssi(call["packet_hash"])) if path == "packet_snr": rpc_connection.send(self.get_packet_snr(call["packet_hash"])) if path == "packet_q": rpc_connection.send(self.get_packet_q(call["packet_hash"])) if "drop" in call: path = call["drop"] if path == "path": rpc_connection.send(self.drop_path(call["destination_hash"])) if path == "all_via": rpc_connection.send(self.drop_all_via(call["destination_hash"])) if path == "announce_queues": rpc_connection.send(self.drop_announce_queues()) rpc_connection.close() except Exception as e: RNS.log("An error ocurred while handling RPC call from local client: "+str(e), RNS.LOG_ERROR) def get_interface_stats(self): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"get": "interface_stats"}) response = rpc_connection.recv() return response else: interfaces = [] for interface in RNS.Transport.interfaces: ifstats = {} if hasattr(interface, "clients"): ifstats["clients"] = interface.clients else: ifstats["clients"] = None if hasattr(interface, "parent_interface") and interface.parent_interface != None: ifstats["parent_interface_name"] = str(interface.parent_interface) ifstats["parent_interface_hash"] = interface.parent_interface.get_hash() if hasattr(interface, "i2p") and hasattr(interface, "connectable"): if interface.connectable: ifstats["i2p_connectable"] = True else: ifstats["i2p_connectable"] = False if hasattr(interface, "b32"): if interface.b32 != None: ifstats["i2p_b32"] = interface.b32+".b32.i2p" else: ifstats["i2p_b32"] = None if hasattr(interface, "i2p_tunnel_state"): if interface.i2p_tunnel_state != None: state_description = "Unknown State" if interface.i2p_tunnel_state == I2PInterface.I2PInterfacePeer.TUNNEL_STATE_ACTIVE: state_description = "Tunnel Active" elif interface.i2p_tunnel_state == I2PInterface.I2PInterfacePeer.TUNNEL_STATE_INIT: state_description = "Creating Tunnel" elif interface.i2p_tunnel_state == I2PInterface.I2PInterfacePeer.TUNNEL_STATE_STALE: state_description = "Tunnel Unresponsive" ifstats["tunnelstate"] = state_description else: ifstats["tunnelstate"] = None if hasattr(interface, "r_airtime_short"): ifstats["airtime_short"] = interface.r_airtime_short if hasattr(interface, "r_airtime_long"): ifstats["airtime_long"] = interface.r_airtime_long if hasattr(interface, "r_channel_load_short"): ifstats["channel_load_short"] = interface.r_channel_load_short if hasattr(interface, "r_channel_load_long"): ifstats["channel_load_long"] = interface.r_channel_load_long if hasattr(interface, "r_battery_state"): if interface.r_battery_state != 0x00: ifstats["battery_state"] = interface.get_battery_state_string() if hasattr(interface, "r_battery_percent"): ifstats["battery_percent"] = interface.r_battery_percent if hasattr(interface, "bitrate"): if interface.bitrate != None: ifstats["bitrate"] = interface.bitrate else: ifstats["bitrate"] = None if hasattr(interface, "peers"): if interface.peers != None: ifstats["peers"] = len(interface.peers) else: ifstats["peers"] = None if hasattr(interface, "ifac_signature"): ifstats["ifac_signature"] = interface.ifac_signature ifstats["ifac_size"] = interface.ifac_size ifstats["ifac_netname"] = interface.ifac_netname else: ifstats["ifac_signature"] = None ifstats["ifac_size"] = None ifstats["ifac_netname"] = None if hasattr(interface, "announce_queue"): if interface.announce_queue != None: ifstats["announce_queue"] = len(interface.announce_queue) else: ifstats["announce_queue"] = None ifstats["name"] = str(interface) ifstats["short_name"] = str(interface.name) ifstats["hash"] = interface.get_hash() ifstats["type"] = str(type(interface).__name__) ifstats["rxb"] = interface.rxb ifstats["txb"] = interface.txb ifstats["incoming_announce_frequency"] = interface.incoming_announce_frequency() ifstats["outgoing_announce_frequency"] = interface.outgoing_announce_frequency() ifstats["held_announces"] = len(interface.held_announces) ifstats["status"] = interface.online ifstats["mode"] = interface.mode interfaces.append(ifstats) stats = {} stats["interfaces"] = interfaces if Reticulum.transport_enabled(): stats["transport_id"] = RNS.Transport.identity.hash stats["transport_uptime"] = time.time()-RNS.Transport.start_time if Reticulum.probe_destination_enabled(): stats["probe_responder"] = RNS.Transport.probe_destination.hash else: stats["probe_responder"] = None return stats def get_path_table(self, max_hops=None): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"get": "path_table", "max_hops": max_hops}) response = rpc_connection.recv() return response else: path_table = [] for dst_hash in RNS.Transport.destination_table: path_hops = RNS.Transport.destination_table[dst_hash][2] if max_hops == None or path_hops <= max_hops: entry = { "hash": dst_hash, "timestamp": RNS.Transport.destination_table[dst_hash][0], "via": RNS.Transport.destination_table[dst_hash][1], "hops": path_hops, "expires": RNS.Transport.destination_table[dst_hash][3], "interface": str(RNS.Transport.destination_table[dst_hash][5]), } path_table.append(entry) return path_table def get_rate_table(self): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"get": "rate_table"}) response = rpc_connection.recv() return response else: rate_table = [] for dst_hash in RNS.Transport.announce_rate_table: entry = { "hash": dst_hash, "last": RNS.Transport.announce_rate_table[dst_hash]["last"], "rate_violations": RNS.Transport.announce_rate_table[dst_hash]["rate_violations"], "blocked_until": RNS.Transport.announce_rate_table[dst_hash]["blocked_until"], "timestamps": RNS.Transport.announce_rate_table[dst_hash]["timestamps"], } rate_table.append(entry) return rate_table def drop_path(self, destination): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"drop": "path", "destination_hash": destination}) response = rpc_connection.recv() return response else: return RNS.Transport.expire_path(destination) def drop_all_via(self, transport_hash): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"drop": "all_via", "destination_hash": transport_hash}) response = rpc_connection.recv() return response else: dropped_count = 0 for destination_hash in RNS.Transport.destination_table: if RNS.Transport.destination_table[destination_hash][1] == transport_hash: RNS.Transport.expire_path(destination_hash) dropped_count += 1 return dropped_count def drop_announce_queues(self): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"drop": "announce_queues"}) response = rpc_connection.recv() return response else: return RNS.Transport.drop_announce_queues() def get_next_hop_if_name(self, destination): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"get": "next_hop_if_name", "destination_hash": destination}) response = rpc_connection.recv() return response else: return str(RNS.Transport.next_hop_interface(destination)) def get_first_hop_timeout(self, destination): if self.is_connected_to_shared_instance: try: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"get": "first_hop_timeout", "destination_hash": destination}) response = rpc_connection.recv() if self.is_connected_to_shared_instance and hasattr(self, "_force_shared_instance_bitrate") and self._force_shared_instance_bitrate: simulated_latency = ((1/self._force_shared_instance_bitrate)*8)*RNS.Reticulum.MTU RNS.log("Adding simulated latency of "+RNS.prettytime(simulated_latency)+" to first hop timeout", RNS.LOG_DEBUG) response += simulated_latency return response except Exception as e: RNS.log("An error occurred while getting first hop timeout from shared instance: "+str(e), RNS.LOG_ERROR) return RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT else: return RNS.Transport.first_hop_timeout(destination) def get_next_hop(self, destination): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"get": "next_hop", "destination_hash": destination}) response = rpc_connection.recv() # TODO: Remove this debugging function # if not response: # response = RNS.Transport.next_hop(destination) return response else: return RNS.Transport.next_hop(destination) def get_link_count(self): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"get": "link_count"}) response = rpc_connection.recv() return response else: return len(RNS.Transport.link_table) def get_packet_rssi(self, packet_hash): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"get": "packet_rssi", "packet_hash": packet_hash}) response = rpc_connection.recv() return response else: for entry in RNS.Transport.local_client_rssi_cache: if entry[0] == packet_hash: return entry[1] return None def get_packet_snr(self, packet_hash): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"get": "packet_snr", "packet_hash": packet_hash}) response = rpc_connection.recv() return response else: for entry in RNS.Transport.local_client_snr_cache: if entry[0] == packet_hash: return entry[1] return None def get_packet_q(self, packet_hash): if self.is_connected_to_shared_instance: rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key) rpc_connection.send({"get": "packet_q", "packet_hash": packet_hash}) response = rpc_connection.recv() return response else: for entry in RNS.Transport.local_client_q_cache: if entry[0] == packet_hash: return entry[1] return None def halt_interface(self, interface): pass def resume_interface(self, interface): pass def reload_interface(self, interface): pass @staticmethod def should_use_implicit_proof(): """ Returns whether proofs sent are explicit or implicit. :returns: True if the current running configuration specifies to use implicit proofs. False if not. """ return Reticulum.__use_implicit_proof @staticmethod def transport_enabled(): """ Returns whether Transport is enabled for the running instance. When Transport is enabled, Reticulum will route traffic for other peers, respond to path requests and pass announces over the network. :returns: True if Transport is enabled, False if not. """ 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 # Default configuration file: __default_rns_config__ = '''# This is the default Reticulum config file. # You should probably edit it to include any additional, # interfaces and settings you might need. # Only the most basic options are included in this default # configuration. To see a more verbose, and much longer, # configuration example, you can run the command: # rnsd --exampleconfig [reticulum] # If you enable Transport, your system will route traffic # for other peers, pass announces and serve path requests. # This should only be done for systems that are suited to # act as transport nodes, ie. if they are stationary and # always-on. This directive is optional and can be removed # for brevity. enable_transport = False # By default, the first program to launch the Reticulum # Network Stack will create a shared instance, that other # programs can communicate with. Only the shared instance # opens all the configured interfaces directly, and other # local programs communicate with the shared instance over # a local socket. This is completely transparent to the # user, and should generally be turned on. This directive # is optional and can be removed for brevity. share_instance = Yes # If you want to run multiple *different* shared instances # on the same system, you will need to specify different # shared instance ports for each. The defaults are given # below, and again, these options can be left out if you # don't need them. shared_instance_port = 37428 instance_control_port = 37429 # 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 # an optional directive, and can be left out for brevity. # This behaviour is disabled by default. panic_on_interface_error = No [logging] # Valid log levels are 0 through 7: # 0: Log only critical information # 1: Log errors and lower log levels # 2: Log warnings and lower log levels # 3: Log notices and lower log levels # 4: Log info and lower (this is the default) # 5: Verbose logging # 6: Debug logging # 7: Extreme logging loglevel = 4 # The interfaces section defines the physical and virtual # interfaces Reticulum will use to communicate on. This # section will contain examples for a variety of interface # types. You can modify these or use them as a basis for # your own config, or simply remove the unused ones. [interfaces] # This interface enables communication with other # link-local Reticulum nodes over UDP. It does not # need any functional IP infrastructure like routers # or DHCP servers, but will require that at least link- # local IPv6 is enabled in your operating system, which # should be enabled by default in almost any OS. See # the Reticulum Manual for more configuration options. [[Default Interface]] type = AutoInterface enabled = Yes '''.splitlines()