Reticulum.py
1 # Reticulum License 2 # 3 # Copyright (c) 2016-2025 Mark Qvist 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # - The Software shall not be used in any kind of system which includes amongst 13 # its functions the ability to purposefully do harm to human beings. 14 # 15 # - The Software shall not be used, directly or indirectly, in the creation of 16 # an artificial intelligence, machine learning or language model training 17 # dataset, including but not limited to any use that contributes to the 18 # training or development of such a model or algorithm. 19 # 20 # - The above copyright notice and this permission notice shall be included in 21 # all copies or substantial portions of the Software. 22 # 23 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 # SOFTWARE. 30 31 from .vendor.platformutils import get_platform 32 33 if get_platform() == "android": 34 from .Interfaces import Interface 35 from .Interfaces import LocalInterface 36 from .Interfaces import AutoInterface 37 from .Interfaces import BackboneInterface 38 from .Interfaces import TCPInterface 39 from .Interfaces import UDPInterface 40 from .Interfaces import I2PInterface 41 from .Interfaces import RNodeMultiInterface 42 from .Interfaces import WeaveInterface 43 from .Interfaces.Android import RNodeInterface 44 from .Interfaces.Android import SerialInterface 45 from .Interfaces.Android import KISSInterface 46 else: 47 from RNS.Interfaces import * 48 49 from RNS.vendor.configobj import ConfigObj 50 import configparser 51 import multiprocessing.connection 52 import importlib.util 53 import threading 54 import signal 55 import atexit 56 import struct 57 import array 58 import time 59 import os 60 import RNS 61 62 class Reticulum: 63 """ 64 This class is used to initialise access to Reticulum within a 65 program. You must create exactly one instance of this class before 66 carrying out any other RNS operations, such as creating destinations 67 or sending traffic. Every independently executed program must create 68 their own instance of the Reticulum class, but Reticulum will 69 automatically handle inter-program communication on the same system, 70 and expose all connected programs to external interfaces as well. 71 72 As soon as an instance of this class is created, Reticulum will start 73 opening and configuring any hardware devices specified in the supplied 74 configuration. 75 76 Currently the first running instance must be kept running while other 77 local instances are connected, as the first created instance will 78 act as a master instance that directly communicates with external 79 hardware such as modems, TNCs and radios. If a master instance is 80 asked to exit, it will not exit until all client processes have 81 terminated (unless killed forcibly). 82 83 If you are running Reticulum on a system with several different 84 programs that use RNS starting and terminating at different times, 85 it will be advantageous to run a master RNS instance as a daemon for 86 other programs to use on demand. 87 """ 88 89 # Future minimum will probably be locked in at 251 bytes to support 90 # networks with segments of different MTUs. Absolute minimum is 219. 91 MTU = 500 92 """ 93 The MTU that Reticulum adheres to, and will expect other peers to 94 adhere to. By default, the MTU is 500 bytes. In custom RNS network 95 implementations, it is possible to change this value, but doing so will 96 completely break compatibility with all other RNS networks. An identical 97 MTU is a prerequisite for peers to communicate in the same network. 98 99 Unless you really know what you are doing, the MTU should be left at 100 the default value. 101 """ 102 103 LINK_MTU_DISCOVERY = True 104 """ 105 Whether automatic link MTU discovery is enabled by default in this 106 release. Link MTU discovery significantly increases throughput over 107 fast links, but requires all intermediary hops to also support it. 108 Support for this feature was added in RNS version 0.9.0. This option 109 will become enabled by default in the near future. Please update your 110 RNS instances. 111 """ 112 113 MAX_QUEUED_ANNOUNCES = 16384 114 QUEUED_ANNOUNCE_LIFE = 60*60*24 115 116 ANNOUNCE_CAP = 2 117 """ 118 The maximum percentage of interface bandwidth that, at any given time, 119 may be used to propagate announces. If an announce was scheduled for 120 broadcasting on an interface, but doing so would exceed the allowed 121 bandwidth allocation, the announce will be queued for transmission 122 when there is bandwidth available. 123 124 Reticulum will always prioritise propagating announces with fewer 125 hops, ensuring that distant, large networks with many peers on fast 126 links don't overwhelm the capacity of smaller networks on slower 127 mediums. If an announce remains queued for an extended amount of time, 128 it will eventually be dropped. 129 130 This value will be applied by default to all created interfaces, 131 but it can be configured individually on a per-interface basis. In 132 general, the global default setting should not be changed, and any 133 alterations should be made on a per-interface basis instead. 134 """ 135 136 MINIMUM_BITRATE = 5 137 """ 138 Minimum bitrate required across a medium for Reticulum to be able 139 to successfully establish links. Currently 5 bits per second. 140 """ 141 142 # TODO: Let Reticulum somehow continously build a map of per-hop 143 # latencies and use this map for global timeout calculation. 144 DEFAULT_PER_HOP_TIMEOUT = 6 145 146 # Length of truncated hashes in bits. 147 TRUNCATED_HASHLENGTH = 128 148 149 HEADER_MINSIZE = 2+1+(TRUNCATED_HASHLENGTH//8)*1 150 HEADER_MAXSIZE = 2+1+(TRUNCATED_HASHLENGTH//8)*2 151 IFAC_MIN_SIZE = 1 152 IFAC_SALT = bytes.fromhex("adf54d882c9a9b80771eb4995d702d4a3e733391b2a0f53f416d9f907e55cff8") 153 154 MDU = MTU - HEADER_MAXSIZE - IFAC_MIN_SIZE 155 156 RESOURCE_CACHE = 24*60*60 157 JOB_INTERVAL = 5*60 158 CLEAN_INTERVAL = 15*60 159 PERSIST_INTERVAL = 60*60*12 160 GRACIOUS_PERSIST_INTERVAL = 60*5 161 162 router = None 163 config = None 164 165 # The default configuration path will be expanded to a directory 166 # named ".reticulum" inside the current users home directory 167 userdir = os.path.expanduser("~") 168 configdir = None 169 configpath = "" 170 storagepath = "" 171 cachepath = "" 172 interfacepath = "" 173 174 __instance = None 175 176 __interface_detach_ran = False 177 __exit_handler_ran = False 178 @staticmethod 179 def exit_handler(): 180 # This exit handler is called whenever Reticulum is asked to 181 # shut down, and will in turn call exit handlers in other 182 # classes, saving necessary information to disk and carrying 183 # out cleanup operations. 184 if not Reticulum.__exit_handler_ran: 185 Reticulum.__exit_handler_ran = True 186 if not Reticulum.__interface_detach_ran: 187 RNS.Transport.detach_interfaces() 188 RNS.Transport.exit_handler() 189 RNS.Identity.exit_handler() 190 191 if RNS.Profiler.ran(): 192 RNS.Profiler.results() 193 194 RNS.loglevel = -1 195 196 @staticmethod 197 def sigint_handler(signal, frame): 198 RNS.Transport.detach_interfaces() 199 Reticulum.__interface_detach_ran = True 200 RNS.exit() 201 202 @staticmethod 203 def sigterm_handler(signal, frame): 204 RNS.Transport.detach_interfaces() 205 Reticulum.__interface_detach_ran = True 206 RNS.exit() 207 208 @staticmethod 209 def get_instance(): 210 """ 211 Return the currently running Reticulum instance 212 """ 213 return Reticulum.__instance 214 215 def __init__(self,configdir=None, loglevel=None, logdest=None, verbosity=None, 216 require_shared_instance=False, shared_instance_type=None): 217 """ 218 Initialises and starts a Reticulum instance. This must be 219 done before any other operations, and Reticulum will not 220 pass any traffic before being instantiated. 221 222 :param configdir: Full path to a Reticulum configuration directory. 223 """ 224 225 if Reticulum.__instance != None: raise OSError("Attempt to reinitialise Reticulum, when it was already running") 226 else: Reticulum.__instance = self 227 228 RNS.vendor.platformutils.platform_checks() 229 230 if configdir != None: Reticulum.configdir = configdir 231 else: 232 if os.path.isdir("/etc/reticulum") and os.path.isfile("/etc/reticulum/config"): 233 Reticulum.configdir = "/etc/reticulum" 234 elif os.path.isdir(Reticulum.userdir+"/.config/reticulum") and os.path.isfile(Reticulum.userdir+"/.config/reticulum/config"): 235 Reticulum.configdir = Reticulum.userdir+"/.config/reticulum" 236 else: 237 Reticulum.configdir = Reticulum.userdir+"/.reticulum" 238 239 if logdest == RNS.LOG_FILE: 240 RNS.logdest = RNS.LOG_FILE 241 RNS.logfile = Reticulum.configdir+"/logfile" 242 elif callable(logdest): 243 RNS.logdest = RNS.LOG_CALLBACK 244 RNS.logcall = logdest 245 246 Reticulum.configpath = Reticulum.configdir+"/config" 247 Reticulum.storagepath = Reticulum.configdir+"/storage" 248 Reticulum.cachepath = Reticulum.configdir+"/storage/cache" 249 Reticulum.resourcepath = Reticulum.configdir+"/storage/resources" 250 Reticulum.identitypath = Reticulum.configdir+"/storage/identities" 251 Reticulum.blackholepath = Reticulum.configdir+"/storage/blackhole" 252 Reticulum.interfacepath = Reticulum.configdir+"/interfaces" 253 254 Reticulum.__network_identity = None 255 Reticulum.__transport_enabled = False 256 Reticulum.__link_mtu_discovery = Reticulum.LINK_MTU_DISCOVERY 257 Reticulum.__remote_management_enabled = False 258 Reticulum.__use_implicit_proof = True 259 Reticulum.__allow_probes = False 260 Reticulum.__discovery_enabled = False 261 Reticulum.__discover_interfaces = False 262 Reticulum.__autoconnect_discovered_interfaces = False 263 Reticulum.__required_discovery_value = None 264 Reticulum.__publish_blackhole = False 265 Reticulum.__blackhole_sources = [] 266 Reticulum.__interface_sources = [] 267 268 Reticulum.panic_on_interface_error = False 269 270 self.local_interface_port = 37428 271 self.local_control_port = 37429 272 self.local_socket_path = None 273 self.share_instance = True 274 self.shared_instance_type = shared_instance_type 275 self.rpc_listener = None 276 self.rpc_key = None 277 self.rpc_type = "AF_INET" 278 self.use_af_unix = False 279 self.bootstrap_configs = [] 280 281 self.ifac_salt = Reticulum.IFAC_SALT 282 283 self.requested_loglevel = loglevel 284 self.requested_verbosity = verbosity 285 if self.requested_loglevel != None: 286 if self.requested_loglevel > RNS.LOG_EXTREME: self.requested_loglevel = RNS.LOG_EXTREME 287 if self.requested_loglevel < RNS.LOG_CRITICAL: self.requested_loglevel = RNS.LOG_CRITICAL 288 289 RNS.loglevel = self.requested_loglevel 290 291 self.is_shared_instance = False 292 self.shared_instance_interface = None 293 self.require_shared = require_shared_instance 294 self.is_connected_to_shared_instance = False 295 self.is_standalone_instance = False 296 self.jobs_thread = None 297 self.last_data_persist = time.time() 298 self.last_cache_clean = 0 299 300 if not os.path.isdir(Reticulum.storagepath): os.makedirs(Reticulum.storagepath) 301 if not os.path.isdir(Reticulum.cachepath): os.makedirs(Reticulum.cachepath) 302 if not os.path.isdir(Reticulum.resourcepath): os.makedirs(Reticulum.resourcepath) 303 if not os.path.isdir(Reticulum.identitypath): os.makedirs(Reticulum.identitypath) 304 if not os.path.isdir(Reticulum.blackholepath): os.makedirs(Reticulum.blackholepath) 305 if not os.path.isdir(Reticulum.interfacepath): os.makedirs(Reticulum.interfacepath) 306 if not os.path.isdir(os.path.join(Reticulum.cachepath, "announces")): os.makedirs(os.path.join(Reticulum.cachepath, "announces")) 307 308 if os.path.isfile(self.configpath): 309 try: self.config = ConfigObj(self.configpath) 310 except Exception as e: 311 RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR) 312 RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR) 313 RNS.panic() 314 else: 315 RNS.log("Could not load config file, creating default configuration file...") 316 self.__create_default_config() 317 RNS.log("Default config file created. Make any necessary changes in "+Reticulum.configdir+"/config and restart Reticulum if needed.") 318 time.sleep(1.5) 319 320 self.__apply_config() 321 RNS.log(f"Utilising cryptography backend \"{RNS.Cryptography.Provider.backend()}\"", RNS.LOG_DEBUG) 322 RNS.log(f"Configuration loaded from {self.configpath}", RNS.LOG_VERBOSE) 323 324 RNS.Identity.load_known_destinations() 325 RNS.Transport.start(self) 326 327 if self.use_af_unix: 328 self.rpc_addr = f"\0rns/{self.local_socket_path}/rpc" 329 self.rpc_type = "AF_UNIX" 330 else: 331 self.rpc_addr = ("127.0.0.1", self.local_control_port) 332 self.rpc_type = "AF_INET" 333 334 if self.rpc_key == None: 335 self.rpc_key = RNS.Identity.full_hash(RNS.Transport.identity.get_private_key()) 336 337 if self.is_shared_instance: 338 self.rpc_listener = multiprocessing.connection.Listener(self.rpc_addr, family=self.rpc_type, authkey=self.rpc_key) 339 thread = threading.Thread(target=self.rpc_loop) 340 thread.daemon = True 341 thread.start() 342 343 if self.is_shared_instance or self.is_standalone_instance: 344 if Reticulum.__discovery_enabled: RNS.Transport.enable_discovery() 345 if Reticulum.__discover_interfaces: RNS.Transport.discover_interfaces() 346 if Reticulum.__blackhole_sources: RNS.Transport.enable_blackhole_updater() 347 348 atexit.register(Reticulum.exit_handler) 349 signal.signal(signal.SIGINT, Reticulum.sigint_handler) 350 signal.signal(signal.SIGTERM, Reticulum.sigterm_handler) 351 352 def __start_jobs(self): 353 if self.jobs_thread == None: 354 RNS.Identity._clean_ratchets() 355 self.jobs_thread = threading.Thread(target=self.__jobs) 356 self.jobs_thread.daemon = True 357 self.jobs_thread.start() 358 359 def __jobs(self): 360 while True: 361 now = time.time() 362 363 if now > self.last_cache_clean+Reticulum.CLEAN_INTERVAL: 364 self.__clean_caches() 365 self.last_cache_clean = time.time() 366 367 if now > self.last_data_persist+Reticulum.PERSIST_INTERVAL: 368 self.__persist_data() 369 370 time.sleep(Reticulum.JOB_INTERVAL) 371 372 def __start_local_interface(self): 373 if self.share_instance: 374 try: 375 interface = LocalInterface.LocalServerInterface( 376 RNS.Transport, 377 self.local_interface_port, 378 socket_path=self.local_socket_path 379 ) 380 interface.OUT = True 381 if hasattr(Reticulum, "_force_shared_instance_bitrate"): 382 interface.bitrate = Reticulum._force_shared_instance_bitrate 383 interface._force_bitrate = Reticulum._force_shared_instance_bitrate 384 RNS.log(f"Forcing shared instance bitrate of {RNS.prettyspeed(interface.bitrate)}", RNS.LOG_WARNING) 385 interface.optimise_mtu() 386 387 if self.require_shared == True: 388 interface.detach() 389 self.is_shared_instance = True 390 RNS.log("Existing shared instance required, but this instance started as shared instance. Aborting startup.", RNS.LOG_VERBOSE) 391 392 else: 393 RNS.Transport.interfaces.append(interface) 394 self.shared_instance_interface = interface 395 self.is_shared_instance = True 396 RNS.log("Started shared instance interface: "+str(interface), RNS.LOG_DEBUG) 397 self.__start_jobs() 398 399 except Exception as e: 400 try: 401 interface = LocalInterface.LocalClientInterface( 402 RNS.Transport, 403 "Local shared instance", 404 self.local_interface_port, 405 socket_path=self.local_socket_path) 406 interface.target_port = self.local_interface_port 407 interface.OUT = True 408 if hasattr(Reticulum, "_force_shared_instance_bitrate"): 409 interface.bitrate = Reticulum._force_shared_instance_bitrate 410 interface._force_bitrate = True 411 RNS.log(f"Forcing shared instance bitrate of {RNS.prettyspeed(interface.bitrate)}", RNS.LOG_WARNING) 412 interface.optimise_mtu() 413 RNS.Transport.interfaces.append(interface) 414 self.is_shared_instance = False 415 self.is_standalone_instance = False 416 self.is_connected_to_shared_instance = True 417 Reticulum.__transport_enabled = False 418 Reticulum.__remote_management_enabled = False 419 Reticulum.__allow_probes = False 420 RNS.log("Connected to locally available Reticulum instance via: "+str(interface), RNS.LOG_DEBUG) 421 except Exception as e: 422 RNS.log("Local shared instance appears to be running, but it could not be connected", RNS.LOG_ERROR) 423 RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) 424 self.is_shared_instance = False 425 self.is_standalone_instance = True 426 self.is_connected_to_shared_instance = False 427 428 if self.is_shared_instance and self.require_shared: 429 raise SystemError("No shared instance available, but application that started Reticulum required it") 430 431 else: 432 self.is_shared_instance = False 433 self.is_standalone_instance = True 434 self.is_connected_to_shared_instance = False 435 self.__start_jobs() 436 437 def __apply_config(self): 438 if "logging" in self.config: 439 for option in self.config["logging"]: 440 value = self.config["logging"][option] 441 if option == "loglevel" and self.requested_loglevel == None: 442 RNS.loglevel = int(value) 443 if self.requested_verbosity != None: 444 RNS.loglevel += self.requested_verbosity 445 if RNS.loglevel < 0: 446 RNS.loglevel = 0 447 if RNS.loglevel > 7: 448 RNS.loglevel = 7 449 450 if "reticulum" in self.config: 451 for option in self.config["reticulum"]: 452 value = self.config["reticulum"][option] 453 454 if option == "share_instance": 455 value = self.config["reticulum"].as_bool(option) 456 self.share_instance = value 457 458 if RNS.vendor.platformutils.use_af_unix(): 459 if option == "instance_name": 460 value = self.config["reticulum"][option] 461 self.local_socket_path = value 462 463 if option == "shared_instance_type": 464 if self.shared_instance_type == None: 465 value = self.config["reticulum"][option].lower() 466 if value in ["tcp", "unix"]: 467 self.shared_instance_type = value 468 469 if option == "shared_instance_port": 470 value = int(self.config["reticulum"][option]) 471 self.local_interface_port = value 472 473 if option == "instance_control_port": 474 value = int(self.config["reticulum"][option]) 475 self.local_control_port = value 476 477 if option == "rpc_key": 478 try: 479 value = bytes.fromhex(self.config["reticulum"][option]) 480 self.rpc_key = value 481 except Exception as e: 482 RNS.log("Invalid shared instance RPC key specified, falling back to default key", RNS.LOG_ERROR) 483 self.rpc_key = None 484 485 if option == "enable_transport": 486 v = self.config["reticulum"].as_bool(option) 487 if v == True: Reticulum.__transport_enabled = True 488 489 if option == "network_identity": 490 if Reticulum.__network_identity == None: 491 path = self.config["reticulum"][option] 492 identitypath = os.path.expanduser(path) 493 try: 494 network_identity = None 495 if not os.path.isfile(identitypath): 496 network_identity = RNS.Identity() 497 network_identity.to_file(identitypath) 498 RNS.log(f"Network identity generated and persisted to {identitypath}", RNS.LOG_VERBOSE) 499 500 else: 501 network_identity = RNS.Identity.from_file(identitypath) 502 RNS.log(f"Network identity loaded from {identitypath}", RNS.LOG_VERBOSE) 503 504 if network_identity: 505 Reticulum.__network_identity = network_identity 506 RNS.Transport.set_network_identity(Reticulum.__network_identity) 507 508 else: raise ValueError("Network identity initialisation failed") 509 510 except Exception as e: raise ValueError(f"Could not set network identity from {path}: {e}") 511 512 if option == "link_mtu_discovery": 513 v = self.config["reticulum"].as_bool(option) 514 if v == True: Reticulum.__link_mtu_discovery = True 515 516 if option == "enable_remote_management": 517 v = self.config["reticulum"].as_bool(option) 518 if v == True: Reticulum.__remote_management_enabled = True 519 520 if option == "remote_management_allowed": 521 v = self.config["reticulum"].as_list(option) 522 for hexhash in v: 523 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 524 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)) 525 try: allowed_hash = bytes.fromhex(hexhash) 526 except Exception as e: raise ValueError("Invalid identity hash for remote management ACL: "+str(hexhash)) 527 528 if not allowed_hash in RNS.Transport.remote_management_allowed: 529 RNS.Transport.remote_management_allowed.append(allowed_hash) 530 531 if option == "respond_to_probes": 532 v = self.config["reticulum"].as_bool(option) 533 if v == True: Reticulum.__allow_probes = True 534 535 if option == "force_shared_instance_bitrate": 536 v = self.config["reticulum"].as_int(option) 537 Reticulum._force_shared_instance_bitrate = v 538 539 if option == "panic_on_interface_error": 540 v = self.config["reticulum"].as_bool(option) 541 if v == True: Reticulum.panic_on_interface_error = True 542 543 if option == "use_implicit_proof": 544 v = self.config["reticulum"].as_bool(option) 545 if v == True: Reticulum.__use_implicit_proof = True 546 if v == False: Reticulum.__use_implicit_proof = False 547 548 if option == "discover_interfaces": 549 v = self.config["reticulum"].as_bool(option) 550 if v == True: Reticulum.__discover_interfaces = True 551 if v == False: Reticulum.__discover_interfaces = False 552 553 if option == "required_discovery_value": 554 v = self.config["reticulum"].as_int(option) 555 if v > 0: Reticulum.__required_discovery_value = v 556 else: Reticulum.__required_discovery_value = None 557 558 if option == "publish_blackhole": 559 v = self.config["reticulum"].as_bool(option) 560 if v == True: Reticulum.__publish_blackhole = True 561 if v == False: Reticulum.__publish_blackhole = False 562 563 if option == "blackhole_sources": 564 v = self.config["reticulum"].as_list(option) 565 for hexhash in v: 566 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 567 if len(hexhash) != dest_len: raise ValueError(f"Identity hash length for blackhole source {hexhash} is invalid, must be {dest_len} hexadecimal characters ({dest_len//2} bytes).") 568 try: source_identity_hash = bytes.fromhex(hexhash) 569 except Exception as e: raise ValueError(f"Invalid identity hash for remote blackhole source: {hexhash}") 570 if not source_identity_hash in Reticulum.__blackhole_sources: Reticulum.__blackhole_sources.append(source_identity_hash) 571 572 if option == "interface_discovery_sources": 573 v = self.config["reticulum"].as_list(option) 574 for hexhash in v: 575 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 576 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).") 577 try: source_identity_hash = bytes.fromhex(hexhash) 578 except Exception as e: raise ValueError(f"Invalid identity hash for interface discovery source: {hexhash}") 579 if not source_identity_hash in Reticulum.__interface_sources: Reticulum.__interface_sources.append(source_identity_hash) 580 581 if option == "autoconnect_discovered_interfaces": 582 v = self.config["reticulum"].as_int(option) 583 if v > 0: Reticulum.__autoconnect_discovered_interfaces = v 584 585 if RNS.compiled: RNS.log("Reticulum running in compiled mode", RNS.LOG_DEBUG) 586 else: RNS.log("Reticulum running in interpreted mode", RNS.LOG_DEBUG) 587 588 if RNS.vendor.platformutils.use_af_unix(): 589 if self.shared_instance_type == "tcp": self.use_af_unix = False 590 else: self.use_af_unix = True 591 else: 592 self.shared_instance_type = "tcp" 593 self.use_af_unix = False 594 595 if self.local_socket_path == None and self.use_af_unix: 596 self.local_socket_path = "default" 597 598 self.__start_local_interface() 599 600 if self.is_shared_instance or self.is_standalone_instance: 601 RNS.log("Bringing up system interfaces...", RNS.LOG_VERBOSE) 602 interface_names = [] 603 if "interfaces" in self.config: 604 for name in self.config["interfaces"]: 605 if not name in interface_names: 606 c = self.config["interfaces"][name] 607 self._synthesize_interface(c, name, instance_init=True) 608 609 else: 610 RNS.log("The interface name \""+name+"\" was already used. Check your configuration file for errors!", RNS.LOG_ERROR) 611 RNS.panic() 612 613 RNS.log("System interfaces are ready", RNS.LOG_VERBOSE) 614 615 def _synthesize_interface(self, config, name, instance_init=False): 616 c = config 617 interface_mode = Interface.Interface.MODE_FULL 618 619 if "interface_mode" in c: 620 c["interface_mode"] = str(c["interface_mode"]).lower() 621 if c["interface_mode"] == "full": 622 interface_mode = Interface.Interface.MODE_FULL 623 elif c["interface_mode"] == "access_point" or c["interface_mode"] == "accesspoint" or c["interface_mode"] == "ap": 624 interface_mode = Interface.Interface.MODE_ACCESS_POINT 625 elif c["interface_mode"] == "pointtopoint" or c["interface_mode"] == "ptp": 626 interface_mode = Interface.Interface.MODE_POINT_TO_POINT 627 elif c["interface_mode"] == "roaming": 628 interface_mode = Interface.Interface.MODE_ROAMING 629 elif c["interface_mode"] == "boundary": 630 interface_mode = Interface.Interface.MODE_BOUNDARY 631 elif c["mode"] == "gateway" or c["mode"] == "gw": 632 interface_mode = Interface.Interface.MODE_GATEWAY 633 634 elif "mode" in c: 635 c["mode"] = str(c["mode"]).lower() 636 if c["mode"] == "full": 637 interface_mode = Interface.Interface.MODE_FULL 638 elif c["mode"] == "access_point" or c["mode"] == "accesspoint" or c["mode"] == "ap": 639 interface_mode = Interface.Interface.MODE_ACCESS_POINT 640 elif c["mode"] == "pointtopoint" or c["mode"] == "ptp": 641 interface_mode = Interface.Interface.MODE_POINT_TO_POINT 642 elif c["mode"] == "roaming": 643 interface_mode = Interface.Interface.MODE_ROAMING 644 elif c["mode"] == "boundary": 645 interface_mode = Interface.Interface.MODE_BOUNDARY 646 elif c["mode"] == "gateway" or c["mode"] == "gw": 647 interface_mode = Interface.Interface.MODE_GATEWAY 648 649 ifac_size = None 650 if "ifac_size" in c: 651 if c.as_int("ifac_size") >= Reticulum.IFAC_MIN_SIZE*8: 652 ifac_size = c.as_int("ifac_size")//8 653 654 ifac_netname = None 655 if "networkname" in c: 656 if c["networkname"] != "": 657 ifac_netname = c["networkname"] 658 if "network_name" in c: 659 if c["network_name"] != "": 660 ifac_netname = c["network_name"] 661 662 ifac_netkey = None 663 if "passphrase" in c: 664 if c["passphrase"] != "": 665 ifac_netkey = c["passphrase"] 666 if "pass_phrase" in c: 667 if c["pass_phrase"] != "": 668 ifac_netkey = c["pass_phrase"] 669 670 ingress_control = True 671 if "ingress_control" in c: ingress_control = c.as_bool("ingress_control") 672 ic_max_held_announces = None 673 if "ic_max_held_announces" in c: ic_max_held_announces = c.as_int("ic_max_held_announces") 674 ic_burst_hold = None 675 if "ic_burst_hold" in c: ic_burst_hold = c.as_float("ic_burst_hold") 676 ic_burst_freq_new = None 677 if "ic_burst_freq_new" in c: ic_burst_freq_new = c.as_float("ic_burst_freq_new") 678 ic_burst_freq = None 679 if "ic_burst_freq" in c: ic_burst_freq = c.as_float("ic_burst_freq") 680 ic_new_time = None 681 if "ic_new_time" in c: ic_new_time = c.as_float("ic_new_time") 682 ic_burst_penalty = None 683 if "ic_burst_penalty" in c: ic_burst_penalty = c.as_float("ic_burst_penalty") 684 ic_held_release_interval = None 685 if "ic_held_release_interval" in c: ic_held_release_interval = c.as_float("ic_held_release_interval") 686 687 configured_bitrate = None 688 if "bitrate" in c: 689 if c.as_int("bitrate") >= Reticulum.MINIMUM_BITRATE: 690 configured_bitrate = c.as_int("bitrate") 691 692 announce_rate_target = None 693 if "announce_rate_target" in c: 694 if c.as_int("announce_rate_target") > 0: 695 announce_rate_target = c.as_int("announce_rate_target") 696 697 announce_rate_grace = None 698 if "announce_rate_grace" in c: 699 if c.as_int("announce_rate_grace") >= 0: 700 announce_rate_grace = c.as_int("announce_rate_grace") 701 702 announce_rate_penalty = None 703 if "announce_rate_penalty" in c: 704 if c.as_int("announce_rate_penalty") >= 0: 705 announce_rate_penalty = c.as_int("announce_rate_penalty") 706 707 if announce_rate_target != None and announce_rate_grace == None: 708 announce_rate_grace = 0 709 710 if announce_rate_target != None and announce_rate_penalty == None: 711 announce_rate_penalty = 0 712 713 announce_cap = Reticulum.ANNOUNCE_CAP/100.0 714 if "announce_cap" in c: 715 if c.as_float("announce_cap") > 0 and c.as_float("announce_cap") <= 100: 716 announce_cap = c.as_float("announce_cap")/100.0 717 718 bootstrap_only = False 719 if "bootstrap_only" in c: bootstrap_only = c.as_bool("bootstrap_only") 720 721 ignore_config_warnings = False 722 if "ignore_config_warnings" in c: ignore_config_warnings = c.as_bool("ignore_config_warnings") 723 724 discoverable = False 725 discovery_announce_interval = None 726 discovery_stamp_value = None 727 discovery_name = None 728 discovery_encrypt = False 729 reachable_on = None 730 publish_ifac = False 731 latitude = None 732 longitude = None 733 height = None 734 discovery_frequency = None 735 discovery_bandwidth = None 736 discovery_modulation = None 737 if "discoverable" in c: 738 discoverable = c.as_bool("discoverable") 739 if discoverable: 740 Reticulum.__discovery_enabled = True 741 if "announce_interval" in c: 742 discovery_announce_interval = c.as_int("announce_interval")*60 743 if discovery_announce_interval < 5*60: discovery_announce_interval = 5*60 744 745 if discovery_announce_interval == None: discovery_announce_interval = 6*60*60 746 if "discovery_stamp_value" in c: discovery_stamp_value = c.as_int("discovery_stamp_value") 747 if "discovery_name" in c: discovery_name = c["discovery_name"] 748 if "discovery_encrypt" in c: discovery_encrypt = c.as_bool("discovery_encrypt") 749 if "reachable_on" in c: reachable_on = c["reachable_on"] 750 if "publish_ifac" in c: publish_ifac = c.as_bool("publish_ifac") 751 if "latitude" in c: latitude = c.as_float("latitude") 752 if "longitude" in c: longitude = c.as_float("longitude") 753 if "height" in c: height = c.as_float("height") 754 if "discovery_frequency" in c: discovery_frequency = c.as_int("discovery_frequency") 755 if "discovery_bandwidth" in c: discovery_bandwidth = c.as_int("discovery_bandwidth") 756 if "discovery_modulation" in c: discovery_modulation = c.as_int("discovery_modulation") 757 758 if not interface_mode in [Interface.Interface.MODE_GATEWAY, Interface.Interface.MODE_ACCESS_POINT]: 759 if not ignore_config_warnings: 760 if c["type"] in ["RNodeInterface", "RNodeMultiInterface"]: 761 interface_mode = Interface.Interface.MODE_ACCESS_POINT 762 RNS.log(f"Discovery enabled on interface {name} without gateway or AP mode. Auto-configured to AP mode.", RNS.LOG_NOTICE) 763 else: 764 interface_mode = Interface.Interface.MODE_GATEWAY 765 RNS.log(f"Discovery enabled on interface {name} without gateway or AP mode. Auto-configured to gateway mode.", RNS.LOG_NOTICE) 766 767 try: 768 def interface_post_init(interface): 769 if interface != None: 770 if "outgoing" in c and c.as_bool("outgoing") == False: interface.OUT = False 771 else: interface.OUT = True 772 773 interface.mode = interface_mode 774 interface.announce_cap = announce_cap 775 interface.bootstrap_only = bootstrap_only 776 if configured_bitrate: interface.bitrate = configured_bitrate 777 interface.optimise_mtu() 778 779 if ifac_size != None: interface.ifac_size = ifac_size 780 else: interface.ifac_size = interface.DEFAULT_IFAC_SIZE 781 782 interface.discoverable = discoverable 783 interface.discovery_announce_interval = discovery_announce_interval 784 interface.discovery_publish_ifac = publish_ifac 785 interface.reachable_on = reachable_on 786 interface.discovery_name = discovery_name 787 interface.discovery_encrypt = discovery_encrypt 788 interface.discovery_stamp_value = discovery_stamp_value 789 interface.discovery_latitude = latitude 790 interface.discovery_longitude = longitude 791 interface.discovery_height = height 792 interface.discovery_frequency = discovery_frequency 793 interface.discovery_bandwidth = discovery_bandwidth 794 interface.discovery_modulation = discovery_modulation 795 796 interface.announce_rate_target = announce_rate_target 797 interface.announce_rate_grace = announce_rate_grace 798 interface.announce_rate_penalty = announce_rate_penalty 799 interface.ingress_control = ingress_control 800 if ic_max_held_announces != None: interface.ic_max_held_announces = ic_max_held_announces 801 if ic_burst_hold != None: interface.ic_burst_hold = ic_burst_hold 802 if ic_burst_freq_new != None: interface.ic_burst_freq_new = ic_burst_freq_new 803 if ic_burst_freq != None: interface.ic_burst_freq = ic_burst_freq 804 if ic_new_time != None: interface.ic_new_time = ic_new_time 805 if ic_burst_penalty != None: interface.ic_burst_penalty = ic_burst_penalty 806 if ic_held_release_interval != None: interface.ic_held_release_interval = ic_held_release_interval 807 808 interface.ifac_netname = ifac_netname 809 interface.ifac_netkey = ifac_netkey 810 811 if interface.ifac_netname != None or interface.ifac_netkey != None: 812 ifac_origin = b"" 813 814 if interface.ifac_netname != None: 815 ifac_origin += RNS.Identity.full_hash(interface.ifac_netname.encode("utf-8")) 816 817 if interface.ifac_netkey != None: 818 ifac_origin += RNS.Identity.full_hash(interface.ifac_netkey.encode("utf-8")) 819 820 ifac_origin_hash = RNS.Identity.full_hash(ifac_origin) 821 interface.ifac_key = RNS.Cryptography.hkdf( 822 length=64, 823 derive_from=ifac_origin_hash, 824 salt=self.ifac_salt, 825 context=None 826 ) 827 828 interface.ifac_identity = RNS.Identity.from_bytes(interface.ifac_key) 829 interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key)) 830 831 RNS.Transport.interfaces.append(interface) 832 interface.final_init() 833 834 interface = None 835 if (("interface_enabled" in c) and c.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True): 836 interface_config = c 837 interface_config["name"] = name 838 interface_config["selected_interface_mode"] = interface_mode 839 interface_config["configured_bitrate"] = configured_bitrate 840 841 if c["type"] == "AutoInterface": 842 interface = AutoInterface.AutoInterface(RNS.Transport, interface_config) 843 interface_post_init(interface) 844 845 if c["type"] == "BackboneInterface" or c["type"] == "BackboneClientInterface": 846 if "port" in c: c["listen_port"] = c["port"] 847 if "port" in c: c["target_port"] = c["port"] 848 if "remote" in c: c["target_host"] = c["remote"] 849 if "listen_on" in c: c["listen_ip"] = c["listen_on"] 850 851 if c["type"] == "BackboneInterface": 852 if "target_host" in c: interface = BackboneInterface.BackboneClientInterface(RNS.Transport, interface_config) 853 else: interface = BackboneInterface.BackboneInterface(RNS.Transport, interface_config) 854 interface_post_init(interface) 855 856 if c["type"] == "BackboneClientInterface": 857 interface = BackboneInterface.BackboneClientInterface(RNS.Transport, interface_config) 858 interface_post_init(interface) 859 860 if c["type"] == "UDPInterface": 861 interface = UDPInterface.UDPInterface(RNS.Transport, interface_config) 862 interface_post_init(interface) 863 864 if c["type"] == "TCPServerInterface": 865 interface = TCPInterface.TCPServerInterface(RNS.Transport, interface_config) 866 interface_post_init(interface) 867 868 if c["type"] == "TCPClientInterface": 869 interface = TCPInterface.TCPClientInterface(RNS.Transport, interface_config) 870 interface_post_init(interface) 871 872 if c["type"] == "I2PInterface": 873 interface_config["storagepath"] = Reticulum.storagepath 874 interface_config["ifac_netname"] = ifac_netname 875 interface_config["ifac_netkey"] = ifac_netkey 876 interface_config["ifac_size"] = ifac_size 877 878 interface = I2PInterface.I2PInterface(RNS.Transport, interface_config) 879 interface_post_init(interface) 880 881 if c["type"] == "SerialInterface": 882 interface = SerialInterface.SerialInterface(RNS.Transport, interface_config) 883 interface_post_init(interface) 884 885 if c["type"] == "PipeInterface": 886 interface = PipeInterface.PipeInterface(RNS.Transport, interface_config) 887 interface_post_init(interface) 888 889 if c["type"] == "KISSInterface": 890 interface = KISSInterface.KISSInterface(RNS.Transport, interface_config) 891 interface_post_init(interface) 892 893 if c["type"] == "AX25KISSInterface": 894 interface = AX25KISSInterface.AX25KISSInterface(RNS.Transport, interface_config) 895 interface_post_init(interface) 896 897 if c["type"] == "RNodeInterface": 898 interface = RNodeInterface.RNodeInterface(RNS.Transport, interface_config) 899 interface_post_init(interface) 900 901 if c["type"] == "RNodeMultiInterface": 902 interface = RNodeMultiInterface.RNodeMultiInterface(RNS.Transport, interface_config) 903 interface_post_init(interface) 904 interface.start() 905 906 if c["type"] == "WeaveInterface": 907 interface = WeaveInterface.WeaveInterface(RNS.Transport, interface_config) 908 interface_post_init(interface) 909 910 if bootstrap_only and instance_init: self.bootstrap_configs.append(interface_config) 911 912 if interface == None: 913 # Interface was not handled by any internal interface types, 914 # attempt to load and initialise it from user-supplied modules 915 interface_type = c["type"] 916 interface_file = f"{interface_type}.py" 917 interface_path = os.path.join(self.interfacepath, interface_file) 918 if not os.path.isfile(interface_path): 919 RNS.log(f"Could not locate external interface module \"{interface_file}\" in \"{self.interfacepath}\"", RNS.LOG_ERROR) 920 921 else: 922 try: 923 RNS.log(f"Loading external interface \"{interface_file}\" from \"{self.interfacepath}\"", RNS.LOG_NOTICE) 924 interface_globals = {} 925 interface_globals["Interface"] = Interface.Interface 926 interface_globals["RNS"] = RNS 927 with open(interface_path) as class_file: 928 interface_code = class_file.read() 929 exec(interface_code, interface_globals) 930 interface_class = interface_globals["interface_class"] 931 932 if interface_class != None: 933 interface = interface_class(RNS.Transport, interface_config) 934 interface_post_init(interface) 935 936 except Exception as e: 937 RNS.log(f"External interface initialisation failed for {interface_type} / {name}", RNS.LOG_ERROR) 938 RNS.trace_exception(e) 939 940 else: 941 RNS.log("Skipping disabled interface \""+name+"\"", RNS.LOG_DEBUG) 942 943 except Exception as e: 944 RNS.log("The interface \""+name+"\" could not be created. Check your configuration file for errors!", RNS.LOG_ERROR) 945 RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) 946 RNS.trace_exception(e) 947 RNS.panic() 948 949 def _add_interface(self, interface, mode = None, configured_bitrate=None, ifac_size=None, ifac_netname=None, ifac_netkey=None, 950 announce_cap=None, announce_rate_target=None, announce_rate_grace=None, announce_rate_penalty=None, bootstrap_only=False): 951 if not self.is_connected_to_shared_instance: 952 if interface != None and issubclass(type(interface), RNS.Interfaces.Interface.Interface): 953 954 if mode == None: mode = Interface.Interface.MODE_FULL 955 interface.mode = mode 956 interface.OUT = True 957 958 if configured_bitrate: interface.bitrate = configured_bitrate 959 if bootstrap_only == True: interface.bootstrap_only = True 960 interface.optimise_mtu() 961 962 if ifac_size != None: interface.ifac_size = ifac_size 963 else: interface.ifac_size = 8 964 965 interface.announce_cap = announce_cap if announce_cap != None else Reticulum.ANNOUNCE_CAP/100.0 966 interface.announce_rate_target = announce_rate_target 967 interface.announce_rate_grace = announce_rate_grace 968 interface.announce_rate_penalty = announce_rate_penalty 969 970 interface.ifac_netname = ifac_netname 971 interface.ifac_netkey = ifac_netkey 972 973 if interface.ifac_netname != None or interface.ifac_netkey != None: 974 ifac_origin = b"" 975 976 if interface.ifac_netname != None: 977 ifac_origin += RNS.Identity.full_hash(interface.ifac_netname.encode("utf-8")) 978 979 if interface.ifac_netkey != None: 980 ifac_origin += RNS.Identity.full_hash(interface.ifac_netkey.encode("utf-8")) 981 982 ifac_origin_hash = RNS.Identity.full_hash(ifac_origin) 983 interface.ifac_key = RNS.Cryptography.hkdf( 984 length=64, 985 derive_from=ifac_origin_hash, 986 salt=self.ifac_salt, 987 context=None 988 ) 989 990 interface.ifac_identity = RNS.Identity.from_bytes(interface.ifac_key) 991 interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key)) 992 993 RNS.Transport.interfaces.append(interface) 994 interface.final_init() 995 996 def _should_persist_data(self): 997 if time.time() > self.last_data_persist+Reticulum.GRACIOUS_PERSIST_INTERVAL: 998 self.__persist_data() 999 1000 def __persist_data(self): 1001 RNS.Transport.persist_data() 1002 RNS.Identity.persist_data() 1003 self.last_data_persist = time.time() 1004 1005 def __clean_caches(self): 1006 RNS.log("Cleaning resource and packet caches...", RNS.LOG_EXTREME) 1007 now = time.time() 1008 1009 # Clean resource caches 1010 for filename in os.listdir(self.resourcepath): 1011 try: 1012 if len(filename) == (RNS.Identity.HASHLENGTH//8)*2: 1013 filepath = self.resourcepath + "/" + filename 1014 mtime = os.path.getmtime(filepath) 1015 age = now - mtime 1016 if age > Reticulum.RESOURCE_CACHE: 1017 os.unlink(filepath) 1018 1019 except Exception as e: 1020 RNS.log("Error while cleaning resources cache, the contained exception was: "+str(e), RNS.LOG_ERROR) 1021 1022 # Clean packet caches 1023 for filename in os.listdir(self.cachepath): 1024 try: 1025 if len(filename) == (RNS.Identity.HASHLENGTH//8)*2: 1026 filepath = self.cachepath + "/" + filename 1027 mtime = os.path.getmtime(filepath) 1028 age = now - mtime 1029 if age > RNS.Transport.DESTINATION_TIMEOUT: 1030 os.unlink(filepath) 1031 1032 except Exception as e: 1033 RNS.log("Error while cleaning resources cache, the contained exception was: "+str(e), RNS.LOG_ERROR) 1034 1035 def __create_default_config(self): 1036 self.config = ConfigObj(__default_rns_config__) 1037 self.config.filename = Reticulum.configpath 1038 1039 if not os.path.isdir(Reticulum.configdir): 1040 os.makedirs(Reticulum.configdir) 1041 self.config.write() 1042 1043 def rpc_loop(self): 1044 while True: 1045 try: 1046 rpc_connection = self.rpc_listener.accept() 1047 call = rpc_connection.recv() 1048 1049 if "get" in call: 1050 path = call["get"] 1051 1052 if path == "path_table": 1053 mh = call["max_hops"] 1054 rpc_connection.send(self.get_path_table(max_hops=mh)) 1055 1056 if path == "interface_stats": rpc_connection.send(self.get_interface_stats()) 1057 if path == "rate_table": rpc_connection.send(self.get_rate_table()) 1058 if path == "next_hop_if_name": rpc_connection.send(self.get_next_hop_if_name(call["destination_hash"])) 1059 if path == "next_hop": rpc_connection.send(self.get_next_hop(call["destination_hash"])) 1060 if path == "first_hop_timeout": rpc_connection.send(self.get_first_hop_timeout(call["destination_hash"])) 1061 if path == "link_count": rpc_connection.send(self.get_link_count()) 1062 if path == "packet_rssi": rpc_connection.send(self.get_packet_rssi(call["packet_hash"])) 1063 if path == "packet_snr": rpc_connection.send(self.get_packet_snr(call["packet_hash"])) 1064 if path == "packet_q": rpc_connection.send(self.get_packet_q(call["packet_hash"])) 1065 if path == "blackholed_identities": rpc_connection.send(self.get_blackholed_identities()) 1066 1067 if "drop" in call: 1068 path = call["drop"] 1069 if path == "path": rpc_connection.send(self.drop_path(call["destination_hash"])) 1070 if path == "all_via": rpc_connection.send(self.drop_all_via(call["destination_hash"])) 1071 if path == "announce_queues": rpc_connection.send(self.drop_announce_queues()) 1072 1073 if "blackhole_identity" in call: 1074 identity_hash = call["blackhole_identity"] 1075 until = call["until"] 1076 reason = call["reason"] 1077 rpc_connection.send(self.blackhole_identity(identity_hash, until=until, reason=reason)) 1078 1079 if "unblackhole_identity" in call: 1080 identity_hash = call["unblackhole_identity"] 1081 rpc_connection.send(self.unblackhole_identity(identity_hash)) 1082 1083 rpc_connection.close() 1084 1085 except Exception as e: 1086 RNS.log("An error ocurred while handling RPC call from local client: "+str(e), RNS.LOG_ERROR) 1087 1088 def get_rpc_client(self): return multiprocessing.connection.Client(self.rpc_addr, family=self.rpc_type, authkey=self.rpc_key) 1089 1090 def get_interface_stats(self): 1091 if self.is_connected_to_shared_instance: 1092 rpc_connection = self.get_rpc_client() 1093 rpc_connection.send({"get": "interface_stats"}) 1094 response = rpc_connection.recv() 1095 return response 1096 else: 1097 interfaces = [] 1098 for interface in RNS.Transport.interfaces: 1099 ifstats = {} 1100 1101 if hasattr(interface, "clients"): 1102 ifstats["clients"] = interface.clients 1103 else: 1104 ifstats["clients"] = None 1105 1106 if hasattr(interface, "parent_interface") and interface.parent_interface != None: 1107 ifstats["parent_interface_name"] = str(interface.parent_interface) 1108 ifstats["parent_interface_hash"] = interface.parent_interface.get_hash() 1109 1110 if hasattr(interface, "i2p") and hasattr(interface, "connectable"): 1111 if interface.connectable: 1112 ifstats["i2p_connectable"] = True 1113 else: 1114 ifstats["i2p_connectable"] = False 1115 1116 if hasattr(interface, "b32"): 1117 if interface.b32 != None: 1118 ifstats["i2p_b32"] = interface.b32+".b32.i2p" 1119 else: 1120 ifstats["i2p_b32"] = None 1121 1122 if hasattr(interface, "i2p_tunnel_state"): 1123 if interface.i2p_tunnel_state != None: 1124 state_description = "Unknown State" 1125 if interface.i2p_tunnel_state == I2PInterface.I2PInterfacePeer.TUNNEL_STATE_ACTIVE: 1126 state_description = "Tunnel Active" 1127 elif interface.i2p_tunnel_state == I2PInterface.I2PInterfacePeer.TUNNEL_STATE_INIT: 1128 state_description = "Creating Tunnel" 1129 elif interface.i2p_tunnel_state == I2PInterface.I2PInterfacePeer.TUNNEL_STATE_STALE: 1130 state_description = "Tunnel Unresponsive" 1131 ifstats["tunnelstate"] = state_description 1132 else: 1133 ifstats["tunnelstate"] = None 1134 1135 if hasattr(interface, "r_airtime_short"): 1136 ifstats["airtime_short"] = interface.r_airtime_short 1137 1138 if hasattr(interface, "r_airtime_long"): 1139 ifstats["airtime_long"] = interface.r_airtime_long 1140 1141 if hasattr(interface, "r_channel_load_short"): 1142 ifstats["channel_load_short"] = interface.r_channel_load_short 1143 1144 if hasattr(interface, "r_channel_load_long"): 1145 ifstats["channel_load_long"] = interface.r_channel_load_long 1146 1147 if hasattr(interface, "r_noise_floor"): 1148 ifstats["noise_floor"] = interface.r_noise_floor 1149 1150 if hasattr(interface, "r_interference"): 1151 ifstats["interference"] = interface.r_interference 1152 1153 if hasattr(interface, "r_interference_l") and type(interface.r_interference_l) == list: 1154 ifstats["interference_last_ts"] = interface.r_interference_l[0] 1155 ifstats["interference_last_dbm"] = interface.r_interference_l[1] 1156 1157 if hasattr(interface, "cpu_temp"): 1158 ifstats["cpu_temp"] = interface.cpu_temp 1159 1160 if hasattr(interface, "cpu_load"): 1161 ifstats["cpu_load"] = interface.cpu_load 1162 1163 if hasattr(interface, "mem_load"): 1164 ifstats["mem_load"] = interface.mem_load 1165 1166 if hasattr(interface, "switch_id"): 1167 if interface.switch_id != None: ifstats["switch_id"] = RNS.hexrep(interface.switch_id) 1168 else: ifstats["switch_id"] = None 1169 1170 if hasattr(interface, "via_switch_id"): 1171 if interface.via_switch_id != None: ifstats["via_switch_id"] = RNS.hexrep(interface.via_switch_id) 1172 else: ifstats["via_switch_id"] = None 1173 1174 if hasattr(interface, "endpoint_id"): 1175 if interface.endpoint_id != None: ifstats["endpoint_id"] = RNS.hexrep(interface.endpoint_id) 1176 else: ifstats["endpoint_id"] = None 1177 1178 if hasattr(interface, "r_battery_state"): 1179 if interface.r_battery_state != 0x00: 1180 ifstats["battery_state"] = interface.get_battery_state_string() 1181 1182 if hasattr(interface, "r_battery_percent"): 1183 ifstats["battery_percent"] = interface.r_battery_percent 1184 1185 if hasattr(interface, "bitrate"): 1186 if interface.bitrate != None: 1187 ifstats["bitrate"] = interface.bitrate 1188 else: 1189 ifstats["bitrate"] = None 1190 1191 if hasattr(interface, "current_rx_speed"): 1192 if interface.current_rx_speed != None: 1193 ifstats["rxs"] = interface.current_rx_speed 1194 else: 1195 ifstats["rxs"] = 0 1196 else: 1197 ifstats["rxs"] = 0 1198 1199 if hasattr(interface, "current_tx_speed"): 1200 if interface.current_tx_speed != None: 1201 ifstats["txs"] = interface.current_tx_speed 1202 else: 1203 ifstats["txs"] = 0 1204 else: 1205 ifstats["txs"] = 0 1206 1207 if hasattr(interface, "peers"): 1208 if interface.peers != None: 1209 ifstats["peers"] = len(interface.peers) 1210 else: 1211 ifstats["peers"] = None 1212 1213 if hasattr(interface, "ifac_signature"): 1214 ifstats["ifac_signature"] = interface.ifac_signature 1215 ifstats["ifac_size"] = interface.ifac_size 1216 ifstats["ifac_netname"] = interface.ifac_netname 1217 else: 1218 ifstats["ifac_signature"] = None 1219 ifstats["ifac_size"] = None 1220 ifstats["ifac_netname"] = None 1221 1222 if hasattr(interface, "autoconnect_source"): 1223 ifstats["autoconnect_source"] = interface.autoconnect_source 1224 else: 1225 ifstats["autoconnect_source"] = None 1226 1227 if hasattr(interface, "announce_queue"): 1228 if interface.announce_queue != None: 1229 ifstats["announce_queue"] = len(interface.announce_queue) 1230 else: 1231 ifstats["announce_queue"] = None 1232 1233 ifstats["name"] = str(interface) 1234 ifstats["short_name"] = str(interface.name) 1235 ifstats["hash"] = interface.get_hash() 1236 ifstats["type"] = str(type(interface).__name__) 1237 ifstats["rxb"] = interface.rxb 1238 ifstats["txb"] = interface.txb 1239 ifstats["incoming_announce_frequency"] = interface.incoming_announce_frequency() 1240 ifstats["outgoing_announce_frequency"] = interface.outgoing_announce_frequency() 1241 ifstats["held_announces"] = len(interface.held_announces) 1242 ifstats["status"] = interface.online 1243 ifstats["mode"] = interface.mode 1244 1245 interfaces.append(ifstats) 1246 1247 stats = {} 1248 stats["interfaces"] = interfaces 1249 stats["rxb"] = RNS.Transport.traffic_rxb 1250 stats["txb"] = RNS.Transport.traffic_txb 1251 stats["rxs"] = RNS.Transport.speed_rx 1252 stats["txs"] = RNS.Transport.speed_tx 1253 if Reticulum.transport_enabled(): 1254 stats["transport_id"] = RNS.Transport.identity.hash 1255 stats["network_id"] = RNS.Transport.network_identity.hash if RNS.Transport.network_identity else None 1256 stats["transport_uptime"] = time.time()-RNS.Transport.start_time 1257 if Reticulum.probe_destination_enabled(): 1258 stats["probe_responder"] = RNS.Transport.probe_destination.hash 1259 else: 1260 stats["probe_responder"] = None 1261 1262 if importlib.util.find_spec('psutil') != None: 1263 import psutil 1264 process = psutil.Process() 1265 stats["rss"] = process.memory_info().rss 1266 else: 1267 stats["rss"] = None 1268 1269 return stats 1270 1271 def get_path_table(self, max_hops=None): 1272 if self.is_connected_to_shared_instance: 1273 rpc_connection = self.get_rpc_client() 1274 rpc_connection.send({"get": "path_table", "max_hops": max_hops}) 1275 response = rpc_connection.recv() 1276 return response 1277 1278 else: 1279 path_table = [] 1280 for dst_hash in RNS.Transport.path_table: 1281 path_hops = RNS.Transport.path_table[dst_hash][2] 1282 if max_hops == None or path_hops <= max_hops: 1283 entry = { 1284 "hash": dst_hash, 1285 "timestamp": RNS.Transport.path_table[dst_hash][0], 1286 "via": RNS.Transport.path_table[dst_hash][1], 1287 "hops": path_hops, 1288 "expires": RNS.Transport.path_table[dst_hash][3], 1289 "interface": str(RNS.Transport.path_table[dst_hash][5]), 1290 } 1291 path_table.append(entry) 1292 1293 return path_table 1294 1295 def get_rate_table(self): 1296 if self.is_connected_to_shared_instance: 1297 rpc_connection = self.get_rpc_client() 1298 rpc_connection.send({"get": "rate_table"}) 1299 response = rpc_connection.recv() 1300 return response 1301 1302 else: 1303 rate_table = [] 1304 for dst_hash in RNS.Transport.announce_rate_table: 1305 entry = { 1306 "hash": dst_hash, 1307 "last": RNS.Transport.announce_rate_table[dst_hash]["last"], 1308 "rate_violations": RNS.Transport.announce_rate_table[dst_hash]["rate_violations"], 1309 "blocked_until": RNS.Transport.announce_rate_table[dst_hash]["blocked_until"], 1310 "timestamps": RNS.Transport.announce_rate_table[dst_hash]["timestamps"], 1311 } 1312 rate_table.append(entry) 1313 1314 return rate_table 1315 1316 def drop_path(self, destination): 1317 if self.is_connected_to_shared_instance: 1318 rpc_connection = self.get_rpc_client() 1319 rpc_connection.send({"drop": "path", "destination_hash": destination}) 1320 response = rpc_connection.recv() 1321 return response 1322 1323 else: 1324 return RNS.Transport.expire_path(destination) 1325 1326 def drop_all_via(self, transport_hash): 1327 if self.is_connected_to_shared_instance: 1328 rpc_connection = self.get_rpc_client() 1329 rpc_connection.send({"drop": "all_via", "destination_hash": transport_hash}) 1330 response = rpc_connection.recv() 1331 return response 1332 1333 else: 1334 dropped_count = 0 1335 for destination_hash in RNS.Transport.path_table: 1336 if RNS.Transport.path_table[destination_hash][1] == transport_hash: 1337 RNS.Transport.expire_path(destination_hash) 1338 dropped_count += 1 1339 1340 return dropped_count 1341 1342 def drop_announce_queues(self): 1343 if self.is_connected_to_shared_instance: 1344 rpc_connection = self.get_rpc_client() 1345 rpc_connection.send({"drop": "announce_queues"}) 1346 response = rpc_connection.recv() 1347 return response 1348 1349 else: 1350 return RNS.Transport.drop_announce_queues() 1351 1352 def get_next_hop_if_name(self, destination): 1353 if self.is_connected_to_shared_instance: 1354 rpc_connection = self.get_rpc_client() 1355 rpc_connection.send({"get": "next_hop_if_name", "destination_hash": destination}) 1356 response = rpc_connection.recv() 1357 return response 1358 1359 else: 1360 return str(RNS.Transport.next_hop_interface(destination)) 1361 1362 def get_first_hop_timeout(self, destination): 1363 if self.is_connected_to_shared_instance: 1364 try: 1365 rpc_connection = self.get_rpc_client() 1366 rpc_connection.send({"get": "first_hop_timeout", "destination_hash": destination}) 1367 response = rpc_connection.recv() 1368 1369 if self.is_connected_to_shared_instance and hasattr(self, "_force_shared_instance_bitrate") and self._force_shared_instance_bitrate: 1370 simulated_latency = ((1/self._force_shared_instance_bitrate)*8)*RNS.Reticulum.MTU 1371 RNS.log("Adding simulated latency of "+RNS.prettytime(simulated_latency)+" to first hop timeout", RNS.LOG_DEBUG) 1372 response += simulated_latency 1373 1374 return response 1375 except Exception as e: 1376 RNS.log("An error occurred while getting first hop timeout from shared instance: "+str(e), RNS.LOG_ERROR) 1377 return RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT 1378 1379 else: 1380 return RNS.Transport.first_hop_timeout(destination) 1381 1382 def get_next_hop(self, destination): 1383 if self.is_connected_to_shared_instance: 1384 rpc_connection = self.get_rpc_client() 1385 rpc_connection.send({"get": "next_hop", "destination_hash": destination}) 1386 response = rpc_connection.recv() 1387 1388 return response 1389 1390 else: 1391 return RNS.Transport.next_hop(destination) 1392 1393 def get_link_count(self): 1394 if self.is_connected_to_shared_instance: 1395 rpc_connection = self.get_rpc_client() 1396 rpc_connection.send({"get": "link_count"}) 1397 response = rpc_connection.recv() 1398 return response 1399 1400 else: 1401 return len(RNS.Transport.link_table) 1402 1403 def get_packet_rssi(self, packet_hash): 1404 if self.is_connected_to_shared_instance: 1405 rpc_connection = self.get_rpc_client() 1406 rpc_connection.send({"get": "packet_rssi", "packet_hash": packet_hash}) 1407 response = rpc_connection.recv() 1408 return response 1409 1410 else: 1411 for entry in RNS.Transport.local_client_rssi_cache: 1412 if entry[0] == packet_hash: 1413 return entry[1] 1414 1415 return None 1416 1417 def get_packet_snr(self, packet_hash): 1418 if self.is_connected_to_shared_instance: 1419 rpc_connection = self.get_rpc_client() 1420 rpc_connection.send({"get": "packet_snr", "packet_hash": packet_hash}) 1421 response = rpc_connection.recv() 1422 return response 1423 1424 else: 1425 for entry in RNS.Transport.local_client_snr_cache: 1426 if entry[0] == packet_hash: 1427 return entry[1] 1428 1429 return None 1430 1431 def get_packet_q(self, packet_hash): 1432 if self.is_connected_to_shared_instance: 1433 rpc_connection = self.get_rpc_client() 1434 rpc_connection.send({"get": "packet_q", "packet_hash": packet_hash}) 1435 response = rpc_connection.recv() 1436 return response 1437 1438 else: 1439 for entry in RNS.Transport.local_client_q_cache: 1440 if entry[0] == packet_hash: 1441 return entry[1] 1442 1443 return None 1444 1445 def halt_interface(self, interface): 1446 pass 1447 1448 def resume_interface(self, interface): 1449 pass 1450 1451 def reload_interface(self, interface): 1452 pass 1453 1454 def get_blackholed_identities(self): 1455 if self.is_connected_to_shared_instance: 1456 rpc_connection = self.get_rpc_client() 1457 rpc_connection.send({"get": "blackholed_identities"}) 1458 response = rpc_connection.recv() 1459 return response 1460 1461 else: return RNS.Transport.blackholed_identities 1462 1463 def blackhole_identity(self, identity_hash, until=None, reason=None): 1464 if len(identity_hash) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8: return False 1465 else: 1466 if self.is_connected_to_shared_instance: 1467 rpc_connection = self.get_rpc_client() 1468 rpc_connection.send({"blackhole_identity": identity_hash, "until": until, "reason": reason}) 1469 response = rpc_connection.recv() 1470 return response 1471 1472 else: return RNS.Transport.blackhole_identity(identity_hash, until=until, reason=reason) 1473 1474 def unblackhole_identity(self, identity_hash): 1475 if len(identity_hash) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8: return False 1476 else: 1477 if self.is_connected_to_shared_instance: 1478 rpc_connection = self.get_rpc_client() 1479 rpc_connection.send({"unblackhole_identity": identity_hash}) 1480 response = rpc_connection.recv() 1481 return response 1482 1483 else: return RNS.Transport.unblackhole_identity(identity_hash) 1484 1485 @staticmethod 1486 def should_use_implicit_proof(): 1487 """ 1488 Returns whether proofs sent are explicit or implicit. 1489 1490 :returns: True if the current running configuration specifies to use implicit proofs. False if not. 1491 """ 1492 return Reticulum.__use_implicit_proof 1493 1494 @staticmethod 1495 def transport_enabled(): 1496 """ 1497 Returns whether Transport is enabled for the running 1498 instance. 1499 1500 When Transport is enabled, Reticulum will 1501 route traffic for other peers, respond to path requests 1502 and pass announces over the network. 1503 1504 :returns: True if Transport is enabled, False if not. 1505 """ 1506 return Reticulum.__transport_enabled 1507 1508 @staticmethod 1509 def link_mtu_discovery(): 1510 """ 1511 Returns whether link MTU discovery is enabled for the running 1512 instance. 1513 1514 When link MTU discovery is enabled, Reticulum will 1515 automatically upgrade link MTUs to the highest supported 1516 value, increasing transfer speed and efficiency. 1517 1518 :returns: True if link MTU discovery is enabled, False if not. 1519 """ 1520 return Reticulum.__link_mtu_discovery 1521 1522 @staticmethod 1523 def remote_management_enabled(): 1524 """ 1525 Returns whether remote management is enabled for the 1526 running instance. 1527 1528 When remote management is enabled, authenticated peers 1529 can remotely query and manage this instance. 1530 1531 :returns: True if remote management is enabled, False if not. 1532 """ 1533 return Reticulum.__remote_management_enabled 1534 1535 @staticmethod 1536 def probe_destination_enabled(): 1537 return Reticulum.__allow_probes 1538 1539 @staticmethod 1540 def required_discovery_value(): 1541 """ 1542 Returns the required stamp value for a discovered interface 1543 to be considered valid and remembered. 1544 1545 :returns: The required stamp value as an integer. 1546 """ 1547 return Reticulum.__required_discovery_value 1548 1549 @staticmethod 1550 def publish_blackhole_enabled(): 1551 """ 1552 Returns whether blackhole list publishing is enabled for the 1553 running instance. 1554 1555 :returns: True if blackhole list publishing is enabled, False if not. 1556 """ 1557 return Reticulum.__publish_blackhole 1558 1559 @staticmethod 1560 def blackhole_sources(): 1561 """ 1562 Returns the list of transport identity hashes from which 1563 blackhole lists are sourced. 1564 1565 :returns: A list of identity hashes. 1566 """ 1567 return Reticulum.__blackhole_sources 1568 1569 @staticmethod 1570 def discovered_interfaces(): 1571 """ 1572 Returns a list of interfaces discovered over the network. 1573 1574 :returns: A list of discovered interfaces. 1575 """ 1576 return RNS.Discovery.InterfaceDiscovery(discover_interfaces=False).list_discovered_interfaces() 1577 1578 @staticmethod 1579 def interface_discovery_sources(): 1580 """ 1581 Returns the list of network identity hashes from which 1582 interfaces are discovered. 1583 1584 :returns: A list of identity hashes. 1585 """ 1586 return Reticulum.__interface_sources 1587 1588 @staticmethod 1589 def should_autoconnect_discovered_interfaces(): 1590 return Reticulum.__autoconnect_discovered_interfaces > 0 1591 1592 @staticmethod 1593 def max_autoconnected_interfaces(): 1594 return Reticulum.__autoconnect_discovered_interfaces 1595 1596 # Default configuration file: 1597 __default_rns_config__ = '''# This is the default Reticulum config file. 1598 # You should probably edit it to include any additional, 1599 # interfaces and settings you might need. 1600 1601 # Only the most basic options are included in this default 1602 # configuration. To see a more verbose, and much longer, 1603 # configuration example, you can run the command: 1604 # rnsd --exampleconfig 1605 1606 1607 [reticulum] 1608 1609 # If you enable Transport, your system will route traffic 1610 # for other peers, pass announces and serve path requests. 1611 # This should only be done for systems that are suited to 1612 # act as transport nodes, ie. if they are stationary and 1613 # always-on. This directive is optional and can be removed 1614 # for brevity. 1615 1616 enable_transport = False 1617 1618 1619 # By default, the first program to launch the Reticulum 1620 # Network Stack will create a shared instance, that other 1621 # programs can communicate with. Only the shared instance 1622 # opens all the configured interfaces directly, and other 1623 # local programs communicate with the shared instance over 1624 # a local socket. This is completely transparent to the 1625 # user, and should generally be turned on. This directive 1626 # is optional and can be removed for brevity. 1627 1628 share_instance = Yes 1629 1630 1631 # If you want to run multiple *different* shared instances 1632 # on the same system, you will need to specify different 1633 # instance names for each. On platforms supporting domain 1634 # sockets, this can be done with the instance_name option: 1635 1636 instance_name = default 1637 1638 1639 # Some platforms don't support domain sockets, and if that 1640 # is the case, you can isolate different instances by 1641 # specifying a unique set of ports for each: 1642 1643 # shared_instance_port = 37428 1644 # instance_control_port = 37429 1645 1646 1647 # If you want to explicitly use TCP for shared instance 1648 # communication, instead of domain sockets, this is also 1649 # possible, by using the following option: 1650 1651 # shared_instance_type = tcp 1652 1653 1654 # You can configure whether Reticulum should discover 1655 # available interfaces from other Transport Instances over 1656 # the network. If this option is enabled, Reticulum will 1657 # collect interface information discovered from the network. 1658 1659 # discover_interfaces = No 1660 1661 1662 # You can configure Reticulum to panic and forcibly close 1663 # if an unrecoverable interface error occurs, such as the 1664 # hardware device for an interface disappearing. This is 1665 # an optional directive, and can be left out for brevity. 1666 # This behaviour is disabled by default. 1667 1668 # panic_on_interface_error = No 1669 1670 1671 # If you're connecting to a large external network, you 1672 # can use one or more external blackhole list to block 1673 # spammy and excessive announces onto your network. This 1674 # funtionality is especially useful if you're hosting public 1675 # entrypoints or gateways. The list source below provides a 1676 # functional example, but better, more timely maintained 1677 # lists probably exist in the community. 1678 1679 # blackhole_sources = 521c87a83afb8f29e4455e77930b973b 1680 1681 1682 [logging] 1683 # Valid log levels are 0 through 7: 1684 # 0: Log only critical information 1685 # 1: Log errors and lower log levels 1686 # 2: Log warnings and lower log levels 1687 # 3: Log notices and lower log levels 1688 # 4: Log info and lower (this is the default) 1689 # 5: Verbose logging 1690 # 6: Debug logging 1691 # 7: Extreme logging 1692 1693 loglevel = 4 1694 1695 1696 # The interfaces section defines the physical and virtual 1697 # interfaces Reticulum will use to communicate on. This 1698 # section will contain examples for a variety of interface 1699 # types. You can modify these or use them as a basis for 1700 # your own config, or simply remove the unused ones. 1701 1702 [interfaces] 1703 1704 # This interface enables communication with other 1705 # link-local Reticulum nodes over UDP. It does not 1706 # need any functional IP infrastructure like routers 1707 # or DHCP servers, but will require that at least link- 1708 # local IPv6 is enabled in your operating system, which 1709 # should be enabled by default in almost any OS. See 1710 # the Reticulum Manual for more configuration options. 1711 1712 [[Default Interface]] 1713 type = AutoInterface 1714 enabled = Yes 1715 1716 '''.splitlines()