Reticulum/RNS/Reticulum.py

1697 lines
83 KiB
Python
Raw Normal View History

2022-04-01 15:18:18 +00:00
# MIT License
#
2024-09-04 15:37:18 +00:00
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
2022-04-01 15:18:18 +00:00
#
# 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.
2021-12-01 10:40:44 +00:00
from .vendor.platformutils import get_platform
if get_platform() == "android":
2022-01-12 11:02:00 +00:00
from .Interfaces import Interface
from .Interfaces import LocalInterface
from .Interfaces import AutoInterface
from .Interfaces import TCPInterface
from .Interfaces import UDPInterface
2022-02-23 16:40:31 +00:00
from .Interfaces import I2PInterface
2024-10-10 21:49:20 +00:00
from .Interfaces import RNodeMultiInterface
from .Interfaces.Android import RNodeInterface
2022-10-15 09:39:23 +00:00
from .Interfaces.Android import SerialInterface
2022-10-15 12:56:23 +00:00
from .Interfaces.Android import KISSInterface
2021-12-01 10:40:44 +00:00
else:
from .Interfaces import *
2020-04-22 10:07:13 +00:00
from .vendor.configobj import ConfigObj
2021-09-24 18:10:04 +00:00
import configparser
import multiprocessing.connection
import signal
2021-09-24 18:10:04 +00:00
import threading
import atexit
import struct
import array
2022-07-02 11:12:54 +00:00
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.
2020-08-13 10:15:56 +00:00
MTU = 500
2021-09-02 16:00:03 +00:00
"""
The MTU that Reticulum adheres to, and will expect other peers to
2023-10-31 10:09:54 +00:00
adhere to. By default, the MTU is 500 bytes. In custom RNS network
2021-09-02 16:00:03 +00:00
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.
2021-09-02 18:35:42 +00:00
Unless you really know what you are doing, the MTU should be left at
the default value.
2021-09-02 16:00:03 +00:00
"""
MAX_QUEUED_ANNOUNCES = 16384
QUEUED_ANNOUNCE_LIFE = 60*60*24
2022-04-17 18:14:20 +00:00
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,
2023-10-31 10:09:54 +00:00
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.
2022-04-17 18:14:20 +00:00
"""
2023-10-31 10:09:54 +00:00
MINIMUM_BITRATE = 5
"""
Minimum bitrate required across a medium for Reticulum to be able
to successfully establish links. Currently 5 bits per second.
"""
2023-10-31 10:09:54 +00:00
# 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
2021-12-05 10:45:13 +00:00
2021-09-02 16:00:03 +00:00
# Length of truncated hashes in bits.
2022-06-30 12:02:57 +00:00
TRUNCATED_HASHLENGTH = 128
2021-09-02 16:00:03 +00:00
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")
2021-09-02 16:00:03 +00:00
MDU = MTU - HEADER_MAXSIZE - IFAC_MIN_SIZE
2020-08-13 10:15:56 +00:00
RESOURCE_CACHE = 24*60*60
JOB_INTERVAL = 5*60
CLEAN_INTERVAL = 15*60
PERSIST_INTERVAL = 60*60*12
GRACIOUS_PERSIST_INTERVAL = 60*5
2022-07-02 11:12:54 +00:00
router = None
config = None
2020-08-13 10:15:56 +00:00
# The default configuration path will be expanded to a directory
# named ".reticulum" inside the current users home directory
2022-09-14 14:21:34 +00:00
userdir = os.path.expanduser("~")
configdir = None
configpath = ""
storagepath = ""
cachepath = ""
__instance = None
2020-08-13 10:15:56 +00:00
@staticmethod
def exit_handler():
2021-05-16 13:52:45 +00:00
# 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()
2020-08-13 10:15:56 +00:00
2024-09-15 13:12:53 +00:00
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):
"""
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
2022-01-12 09:07:44 +00:00
RNS.vendor.platformutils.platform_checks()
2020-08-13 10:15:56 +00:00
if configdir != None:
Reticulum.configdir = configdir
2022-09-14 14:21:34 +00:00
else:
2022-09-30 18:41:11 +00:00
if os.path.isdir("/etc/reticulum") and os.path.isfile("/etc/reticulum/config"):
Reticulum.configdir = "/etc/reticulum"
2024-10-07 08:44:18 +00:00
elif os.path.isdir(f"{Reticulum.userdir}/.config/reticulum") and os.path.isfile(f"{Reticulum.userdir}/.config/reticulum/config"):
Reticulum.configdir = f"{Reticulum.userdir}/.config/reticulum"
2022-09-30 18:37:46 +00:00
else:
2024-10-07 08:44:18 +00:00
Reticulum.configdir = f"{Reticulum.userdir}/.reticulum"
2022-09-14 14:21:34 +00:00
if logdest == RNS.LOG_FILE:
RNS.logdest = RNS.LOG_FILE
2024-10-07 08:44:18 +00:00
RNS.logfile = f"{Reticulum.configdir}/logfile"
2020-08-13 10:15:56 +00:00
2024-10-07 08:44:18 +00:00
Reticulum.configpath = f"{Reticulum.configdir}/config"
Reticulum.storagepath = f"{Reticulum.configdir}/storage"
Reticulum.cachepath = f"{Reticulum.configdir}/storage/cache"
Reticulum.resourcepath = f"{Reticulum.configdir}/storage/resources"
Reticulum.identitypath = f"{Reticulum.configdir}/storage/identities"
2020-08-13 10:15:56 +00:00
Reticulum.__transport_enabled = False
Reticulum.__remote_management_enabled = False
2020-08-13 10:15:56 +00:00
Reticulum.__use_implicit_proof = True
Reticulum.__allow_probes = False
2020-08-13 10:15:56 +00:00
Reticulum.panic_on_interface_error = False
2020-08-13 10:15:56 +00:00
self.local_interface_port = 37428
2021-09-24 18:10:04 +00:00
self.local_control_port = 37429
self.share_instance = True
self.rpc_listener = None
self.rpc_key = None
2020-08-13 10:15:56 +00:00
self.ifac_salt = Reticulum.IFAC_SALT
2021-09-24 13:18:06 +00:00
self.requested_loglevel = loglevel
self.requested_verbosity = verbosity
2021-09-24 13:34:03 +00:00
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
2021-09-24 13:34:03 +00:00
RNS.loglevel = self.requested_loglevel
2021-09-24 13:18:06 +00:00
2020-08-13 10:15:56 +00:00
self.is_shared_instance = False
self.is_connected_to_shared_instance = False
self.is_standalone_instance = False
2022-07-02 11:24:07 +00:00
self.jobs_thread = None
self.last_data_persist = time.time()
self.last_cache_clean = 0
2020-08-13 10:15:56 +00:00
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)
2022-05-22 17:09:16 +00:00
if not os.path.isdir(Reticulum.identitypath):
os.makedirs(Reticulum.identitypath)
2020-08-13 10:15:56 +00:00
if os.path.isfile(self.configpath):
try:
self.config = ConfigObj(self.configpath)
except Exception as e:
2024-10-07 08:44:18 +00:00
RNS.log(f"Could not parse the configuration at {self.configpath}", RNS.LOG_ERROR)
2020-08-13 10:15:56 +00:00
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()
2024-10-07 08:44:18 +00:00
RNS.log(f"Default config file created. Make any necessary changes in {Reticulum.configdir}/config and restart Reticulum if needed.")
2021-12-09 15:07:36 +00:00
time.sleep(1.5)
2020-08-13 10:15:56 +00:00
self.__apply_config()
2024-10-07 08:44:18 +00:00
RNS.log(f"Configuration loaded from {self.configpath}", RNS.LOG_VERBOSE)
2021-09-24 12:13:31 +00:00
2021-05-16 14:15:57 +00:00
RNS.Identity.load_known_destinations()
2020-08-13 10:15:56 +00:00
RNS.Transport.start(self)
2021-09-24 18:10:04 +00:00
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())
2021-09-24 18:10:04 +00:00
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
2021-09-24 18:10:04 +00:00
thread.start()
2020-08-13 10:15:56 +00:00
atexit.register(Reticulum.exit_handler)
signal.signal(signal.SIGINT, Reticulum.sigint_handler)
signal.signal(signal.SIGTERM, Reticulum.sigterm_handler)
2020-08-13 10:15:56 +00:00
2022-07-02 11:24:07 +00:00
def __start_jobs(self):
if self.jobs_thread == None:
2024-09-05 13:02:22 +00:00
RNS.Identity._clean_ratchets()
2022-07-02 11:24:07 +00:00
self.jobs_thread = threading.Thread(target=self.__jobs)
self.jobs_thread.daemon = True
2022-07-02 11:24:07 +00:00
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()
2022-07-02 11:24:07 +00:00
time.sleep(Reticulum.JOB_INTERVAL)
def __start_local_interface(self):
2020-08-13 10:15:56 +00:00
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
2023-11-02 11:24:42 +00:00
RNS.log(f"Forcing shared instance bitrate of {RNS.prettyspeed(interface.bitrate)}", RNS.LOG_WARNING)
2020-08-13 10:15:56 +00:00
RNS.Transport.interfaces.append(interface)
2021-09-24 18:10:04 +00:00
2020-08-13 10:15:56 +00:00
self.is_shared_instance = True
2024-10-07 08:44:18 +00:00
RNS.log(f"Started shared instance interface: {interface}", RNS.LOG_DEBUG)
2022-07-02 11:24:07 +00:00
self.__start_jobs()
2022-07-02 11:12:54 +00:00
2020-08-13 10:15:56 +00:00
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
2023-11-02 11:24:42 +00:00
RNS.log(f"Forcing shared instance bitrate of {RNS.prettyspeed(interface.bitrate)}", RNS.LOG_WARNING)
2020-08-13 10:15:56 +00:00
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
2024-10-07 08:44:18 +00:00
RNS.log(f"Connected to locally available Reticulum instance via: {interface}", RNS.LOG_DEBUG)
2020-08-13 10:15:56 +00:00
except Exception as e:
RNS.log("Local shared instance appears to be running, but it could not be connected", RNS.LOG_ERROR)
2024-10-07 08:44:18 +00:00
RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR)
2020-08-13 10:15:56 +00:00
self.is_shared_instance = False
self.is_standalone_instance = True
self.is_connected_to_shared_instance = False
else:
self.is_shared_instance = False
self.is_standalone_instance = True
self.is_connected_to_shared_instance = False
2022-07-02 11:24:07 +00:00
self.__start_jobs()
2020-08-13 10:15:56 +00:00
def __apply_config(self):
2020-08-13 10:15:56 +00:00
if "logging" in self.config:
for option in self.config["logging"]:
value = self.config["logging"][option]
2021-09-24 13:18:06 +00:00
if option == "loglevel" and self.requested_loglevel == None:
2020-08-13 10:15:56 +00:00
RNS.loglevel = int(value)
if self.requested_verbosity != None:
RNS.loglevel += self.requested_verbosity
2020-08-13 10:15:56 +00:00
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
2021-09-24 18:10:04 +00:00
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
2020-08-13 10:15:56 +00:00
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:
2024-10-07 08:44:18 +00:00
raise ValueError(f"Identity hash length for remote management ACL {hexhash} is invalid, must be {dest_len} hexadecimal characters ({dest_len // 2} bytes).")
try:
allowed_hash = bytes.fromhex(hexhash)
except Exception as e:
2024-10-07 08:44:18 +00:00
raise ValueError(f"Invalid identity hash for remote management ACL: {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
2020-08-13 10:15:56 +00:00
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()
2020-08-13 10:15:56 +00:00
if self.is_shared_instance or self.is_standalone_instance:
2022-05-14 18:19:46 +00:00
RNS.log("Bringing up system interfaces...", RNS.LOG_VERBOSE)
2020-08-13 10:15:56 +00:00
interface_names = []
2022-07-07 22:21:48 +00:00
if "interfaces" in self.config:
for name in self.config["interfaces"]:
if not name in interface_names:
# TODO: We really need to generalise this way of instantiating
# and configuring interfaces. Ideally, interfaces should just
# have a conrfig dict passed to their init method, and return
# a ready interface, onto which this routine can configure any
# generic or extra parameters.
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"]
2023-09-30 19:07:22 +00:00
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")
2023-09-30 19:07:22 +00:00
2022-07-07 22:21:48 +00:00
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:
interface = None
if (("interface_enabled" in c) and c.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True):
if c["type"] == "AutoInterface":
2024-05-16 16:09:11 +00:00
group_id = c["group_id"] if "group_id" in c else None
discovery_scope = c["discovery_scope"] if "discovery_scope" in c else None
discovery_port = int(c["discovery_port"]) if "discovery_port" in c else None
multicast_address_type = c["multicast_address_type"] if "multicast_address_type" in c else None
data_port = int(c["data_port"]) if "data_port" in c else None
allowed_interfaces = c.as_list("devices") if "devices" in c else None
ignored_interfaces = c.as_list("ignored_devices") if "ignored_devices" in c else None
interface = AutoInterface.AutoInterface(
RNS.Transport,
name,
group_id,
discovery_scope,
discovery_port,
multicast_address_type,
data_port,
allowed_interfaces,
ignored_interfaces
)
if "outgoing" in c and c.as_bool("outgoing") == False:
interface.OUT = False
else:
interface.OUT = True
interface.mode = interface_mode
2022-07-07 22:21:48 +00:00
2024-05-16 16:09:11 +00:00
interface.announce_cap = announce_cap
if configured_bitrate:
interface.bitrate = configured_bitrate
if ifac_size != None:
interface.ifac_size = ifac_size
2022-07-07 22:21:48 +00:00
else:
2024-05-16 16:09:11 +00:00
interface.ifac_size = 16
2022-07-07 22:21:48 +00:00
if c["type"] == "UDPInterface":
device = c["device"] if "device" in c else None
port = int(c["port"]) if "port" in c else None
listen_ip = c["listen_ip"] if "listen_ip" in c else None
listen_port = int(c["listen_port"]) if "listen_port" in c else None
forward_ip = c["forward_ip"] if "forward_ip" in c else None
forward_port = int(c["forward_port"]) if "forward_port" in c else None
if port != None:
if listen_port == None:
listen_port = port
if forward_port == None:
forward_port = port
interface = UDPInterface.UDPInterface(
RNS.Transport,
name,
2022-07-07 22:21:48 +00:00
device,
listen_ip,
listen_port,
forward_ip,
forward_port
)
if "outgoing" in c and c.as_bool("outgoing") == False:
interface.OUT = False
else:
interface.OUT = True
2022-02-25 17:47:55 +00:00
interface.mode = interface_mode
2022-04-17 18:14:20 +00:00
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 = 16
2022-07-07 22:21:48 +00:00
if c["type"] == "TCPServerInterface":
device = c["device"] if "device" in c else None
port = int(c["port"]) if "port" in c else None
listen_ip = c["listen_ip"] if "listen_ip" in c else None
listen_port = int(c["listen_port"]) if "listen_port" in c else None
i2p_tunneled = c.as_bool("i2p_tunneled") if "i2p_tunneled" in c else False
if port != None:
listen_port = port
2022-07-07 22:21:48 +00:00
interface = TCPInterface.TCPServerInterface(
RNS.Transport,
name,
device,
listen_ip,
listen_port,
i2p_tunneled
)
if "outgoing" in c and c.as_bool("outgoing") == False:
interface.OUT = False
else:
interface.OUT = True
if interface_mode == Interface.Interface.MODE_ACCESS_POINT:
2024-10-07 08:44:18 +00:00
RNS.log(f"{interface} does not support Access Point mode, reverting to default mode: Full", RNS.LOG_WARNING)
2022-07-07 22:21:48 +00:00
interface_mode = Interface.Interface.MODE_FULL
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 = 16
if c["type"] == "TCPClientInterface":
kiss_framing = False
if "kiss_framing" in c and c.as_bool("kiss_framing") == True:
kiss_framing = True
i2p_tunneled = c.as_bool("i2p_tunneled") if "i2p_tunneled" in c else False
tcp_connect_timeout = c.as_int("connect_timeout") if "connect_timeout" in c else None
2022-07-07 22:21:48 +00:00
interface = TCPInterface.TCPClientInterface(
RNS.Transport,
name,
c["target_host"],
int(c["target_port"]),
kiss_framing = kiss_framing,
i2p_tunneled = i2p_tunneled,
connect_timeout = tcp_connect_timeout,
2022-07-07 22:21:48 +00:00
)
if "outgoing" in c and c.as_bool("outgoing") == False:
interface.OUT = False
else:
interface.OUT = True
if interface_mode == Interface.Interface.MODE_ACCESS_POINT:
2024-10-07 08:44:18 +00:00
RNS.log(f"{interface} does not support Access Point mode, reverting to default mode: Full", RNS.LOG_WARNING)
2022-07-07 22:21:48 +00:00
interface_mode = Interface.Interface.MODE_FULL
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 = 16
if c["type"] == "I2PInterface":
i2p_peers = c.as_list("peers") if "peers" in c else None
connectable = c.as_bool("connectable") if "connectable" in c else False
if ifac_size == None:
ifac_size = 16
2022-07-07 22:21:48 +00:00
interface = I2PInterface.I2PInterface(
RNS.Transport,
name,
Reticulum.storagepath,
i2p_peers,
connectable = connectable,
ifac_size = ifac_size,
ifac_netname = ifac_netname,
ifac_netkey = ifac_netkey,
)
2022-07-07 22:21:48 +00:00
if "outgoing" in c and c.as_bool("outgoing") == False:
interface.OUT = False
else:
interface.OUT = True
2022-07-07 22:21:48 +00:00
if interface_mode == Interface.Interface.MODE_ACCESS_POINT:
2024-10-07 08:44:18 +00:00
RNS.log(f"{interface} does not support Access Point mode, reverting to default mode: Full", RNS.LOG_WARNING)
2022-07-07 22:21:48 +00:00
interface_mode = Interface.Interface.MODE_FULL
interface.mode = interface_mode
2022-04-17 18:14:20 +00:00
2022-07-07 22:21:48 +00:00
interface.announce_cap = announce_cap
if configured_bitrate:
interface.bitrate = configured_bitrate
2020-08-13 10:15:56 +00:00
2022-07-07 22:21:48 +00:00
if c["type"] == "SerialInterface":
port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
if port == None:
raise ValueError("No port specified for serial interface")
interface = SerialInterface.SerialInterface(
RNS.Transport,
name,
port,
speed,
databits,
parity,
stopbits
)
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 = 8
if c["type"] == "PipeInterface":
command = c["command"] if "command" in c else None
respawn_delay = c.as_float("respawn_delay") if "respawn_delay" in c else None
if command == None:
raise ValueError("No command specified for PipeInterface")
interface = PipeInterface.PipeInterface(
RNS.Transport,
name,
command,
respawn_delay,
)
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 = 8
if c["type"] == "KISSInterface":
preamble = int(c["preamble"]) if "preamble" in c else None
txtail = int(c["txtail"]) if "txtail" in c else None
persistence = int(c["persistence"]) if "persistence" in c else None
slottime = int(c["slottime"]) if "slottime" in c else None
flow_control = c.as_bool("flow_control") if "flow_control" in c else False
port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
beacon_interval = int(c["id_interval"]) if "id_interval" in c else None
beacon_data = c["id_callsign"] if "id_callsign" in c else None
if port == None:
raise ValueError("No port specified for serial interface")
interface = KISSInterface.KISSInterface(
RNS.Transport,
name,
port,
speed,
databits,
parity,
stopbits,
preamble,
txtail,
persistence,
slottime,
flow_control,
beacon_interval,
beacon_data
)
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 = 8
if c["type"] == "AX25KISSInterface":
preamble = int(c["preamble"]) if "preamble" in c else None
txtail = int(c["txtail"]) if "txtail" in c else None
persistence = int(c["persistence"]) if "persistence" in c else None
slottime = int(c["slottime"]) if "slottime" in c else None
flow_control = c.as_bool("flow_control") if "flow_control" in c else False
port = c["port"] if "port" in c else None
speed = int(c["speed"]) if "speed" in c else 9600
databits = int(c["databits"]) if "databits" in c else 8
parity = c["parity"] if "parity" in c else "N"
stopbits = int(c["stopbits"]) if "stopbits" in c else 1
callsign = c["callsign"] if "callsign" in c else ""
ssid = int(c["ssid"]) if "ssid" in c else -1
if port == None:
raise ValueError("No port specified for serial interface")
interface = AX25KISSInterface.AX25KISSInterface(
RNS.Transport,
name,
callsign,
ssid,
port,
speed,
databits,
parity,
stopbits,
preamble,
txtail,
persistence,
slottime,
flow_control
)
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 = 8
if c["type"] == "RNodeInterface":
frequency = int(c["frequency"]) if "frequency" in c else None
bandwidth = int(c["bandwidth"]) if "bandwidth" in c else None
txpower = int(c["txpower"]) if "txpower" in c else None
spreadingfactor = int(c["spreadingfactor"]) if "spreadingfactor" in c else None
codingrate = int(c["codingrate"]) if "codingrate" in c else None
flow_control = c.as_bool("flow_control") if "flow_control" in c else False
id_interval = int(c["id_interval"]) if "id_interval" in c else None
id_callsign = c["id_callsign"] if "id_callsign" in c else None
st_alock = float(c["airtime_limit_short"]) if "airtime_limit_short" in c else None
lt_alock = float(c["airtime_limit_long"]) if "airtime_limit_long" in c else None
2022-07-07 22:21:48 +00:00
force_ble = False
ble_name = None
ble_addr = None
2022-07-07 22:21:48 +00:00
port = c["port"] if "port" in c else None
2022-07-07 22:21:48 +00:00
if port == None:
raise ValueError("No port specified for RNode interface")
if port != None:
ble_uri_scheme = "ble://"
if port.lower().startswith(ble_uri_scheme):
force_ble = True
ble_string = port[len(ble_uri_scheme):]
port = None
if len(ble_string) == 0:
pass
elif len(ble_string.split(":")) == 6 and len(ble_string) == 17:
ble_addr = ble_string
else:
ble_name = ble_string
2022-07-07 22:21:48 +00:00
interface = RNodeInterface.RNodeInterface(
RNS.Transport,
name,
port,
frequency = frequency,
bandwidth = bandwidth,
txpower = txpower,
sf = spreadingfactor,
cr = codingrate,
flow_control = flow_control,
id_interval = id_interval,
id_callsign = id_callsign,
st_alock = st_alock,
lt_alock = lt_alock,
ble_addr = ble_addr,
ble_name = ble_name,
force_ble = force_ble,
2022-07-07 22:21:48 +00:00
)
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 = 8
2024-08-19 07:19:42 +00:00
if c["type"] == "RNodeMultiInterface":
count = 0
enabled_count = 0
# Count how many interfaces are in the file
for subinterface in c:
# if the retrieved entry is not a string, it must be a dictionary, which is what we want
if not isinstance(c[subinterface], str):
count += 1
# Count how many interfaces are enabled to allow for appropriate matrix sizing
for subinterface in c:
# if the retrieved entry is not a string, it must be a dictionary, which is what we want
if not isinstance(c[subinterface], str):
subinterface_config = self.config["interfaces"][name][subinterface]
if (("interface_enabled" in subinterface_config) and subinterface_config.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True):
enabled_count += 1
# Create an array with a row for each subinterface
subint_config = [[0 for x in range(11)] for y in range(enabled_count)]
2024-08-19 07:19:42 +00:00
subint_index = 0
for subinterface in c:
# If the retrieved entry is not a string, it must be a dictionary, which is what we want
if not isinstance(c[subinterface], str):
subinterface_config = self.config["interfaces"][name][subinterface]
if (("interface_enabled" in subinterface_config) and subinterface_config.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True):
subint_config[subint_index][0] = subinterface
subint_vport = subinterface_config["vport"] if "vport" in subinterface_config else None
subint_config[subint_index][1] = subint_vport
frequency = int(subinterface_config["frequency"]) if "frequency" in subinterface_config else None
subint_config[subint_index][2] = frequency
bandwidth = int(subinterface_config["bandwidth"]) if "bandwidth" in subinterface_config else None
subint_config[subint_index][3] = bandwidth
txpower = int(subinterface_config["txpower"]) if "txpower" in subinterface_config else None
subint_config[subint_index][4] = txpower
spreadingfactor = int(subinterface_config["spreadingfactor"]) if "spreadingfactor" in subinterface_config else None
subint_config[subint_index][5] = spreadingfactor
codingrate = int(subinterface_config["codingrate"]) if "codingrate" in subinterface_config else None
subint_config[subint_index][6] = codingrate
flow_control = subinterface_config.as_bool("flow_control") if "flow_control" in subinterface_config else False
subint_config[subint_index][7] = flow_control
st_alock = float(subinterface_config["airtime_limit_short"]) if "airtime_limit_short" in subinterface_config else None
subint_config[subint_index][8] = st_alock
lt_alock = float(subinterface_config["airtime_limit_long"]) if "airtime_limit_long" in subinterface_config else None
subint_config[subint_index][9] = lt_alock
if "outgoing" in subinterface_config and subinterface_config.as_bool("outgoing") == False:
subint_config[subint_index][10] = False
else:
subint_config[subint_index][10] = True
2024-08-19 07:19:42 +00:00
subint_index += 1
# if no subinterfaces are defined
if count == 0:
2024-10-07 08:44:18 +00:00
raise ValueError(f"No subinterfaces configured for {name}")
2024-08-19 07:19:42 +00:00
# if no subinterfaces are enabled
elif enabled_count == 0:
2024-10-07 08:44:18 +00:00
raise ValueError(f"No subinterfaces enabled for {name}")
2024-08-19 07:19:42 +00:00
id_interval = int(c["id_interval"]) if "id_interval" in c else None
id_callsign = c["id_callsign"] if "id_callsign" in c else None
port = c["port"] if "port" in c else None
if port == None:
2024-10-07 08:44:18 +00:00
raise ValueError(f"No port specified for {name}")
2024-08-19 07:19:42 +00:00
interface = RNodeMultiInterface.RNodeMultiInterface(
RNS.Transport,
name,
port,
subint_config,
id_interval = id_interval,
id_callsign = id_callsign
)
interface.IN = False
interface.OUT = False
2024-08-19 07:19:42 +00:00
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 = 8
2022-07-07 22:21:48 +00:00
if interface != None:
interface.announce_rate_target = announce_rate_target
interface.announce_rate_grace = announce_rate_grace
interface.announce_rate_penalty = announce_rate_penalty
2023-09-30 19:07:22 +00:00
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
2022-07-07 22:21:48 +00:00
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)
if isinstance(interface, RNS.Interfaces.RNodeMultiInterface.RNodeMultiInterface):
interface.start()
2022-07-07 22:21:48 +00:00
else:
2024-10-07 08:44:18 +00:00
RNS.log(f"Skipping disabled interface \"{name}\"", RNS.LOG_DEBUG)
2022-07-07 22:21:48 +00:00
except Exception as e:
2024-10-07 08:44:18 +00:00
RNS.log(f"The interface \"{name}\" could not be created. Check your configuration file for errors!", RNS.LOG_ERROR)
RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR)
2022-07-07 22:21:48 +00:00
RNS.panic()
else:
2024-10-07 08:44:18 +00:00
RNS.log(f"The interface name \"{name}\" was already used. Check your configuration file for errors!", RNS.LOG_ERROR)
2020-08-13 10:15:56 +00:00
RNS.panic()
2022-05-14 18:19:46 +00:00
RNS.log("System interfaces are ready", RNS.LOG_VERBOSE)
2022-07-07 22:21:48 +00:00
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
2022-07-07 22:21:48 +00:00
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()
2020-08-13 10:15:56 +00:00
2022-07-02 11:12:54 +00:00
def __clean_caches(self):
2022-07-02 11:34:17 +00:00
RNS.log("Cleaning resource and packet caches...", RNS.LOG_EXTREME)
2022-07-02 11:12:54 +00:00
now = time.time()
# Clean resource caches
for filename in os.listdir(self.resourcepath):
try:
if len(filename) == (RNS.Identity.HASHLENGTH//8)*2:
2024-10-07 08:44:18 +00:00
filepath = f"{self.resourcepath}/{filename}"
2022-07-02 11:12:54 +00:00
mtime = os.path.getmtime(filepath)
age = now - mtime
2022-07-02 13:15:47 +00:00
if age > Reticulum.RESOURCE_CACHE:
2022-07-02 11:12:54 +00:00
os.unlink(filepath)
except Exception as e:
2024-10-07 08:44:18 +00:00
RNS.log(f"Error while cleaning resources cache, the contained exception was: {e}", RNS.LOG_ERROR)
2022-07-02 11:12:54 +00:00
# Clean packet caches
for filename in os.listdir(self.cachepath):
try:
if len(filename) == (RNS.Identity.HASHLENGTH//8)*2:
2024-10-07 08:44:18 +00:00
filepath = f"{self.cachepath}/{filename}"
2022-07-02 11:12:54 +00:00
mtime = os.path.getmtime(filepath)
age = now - mtime
2022-07-02 13:15:47 +00:00
if age > RNS.Transport.DESTINATION_TIMEOUT:
2022-07-02 11:12:54 +00:00
os.unlink(filepath)
except Exception as e:
2024-10-07 08:44:18 +00:00
RNS.log(f"Error while cleaning resources cache, the contained exception was: {e}", RNS.LOG_ERROR)
2022-07-02 11:12:54 +00:00
def __create_default_config(self):
2020-08-13 10:15:56 +00:00
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()
2021-09-24 18:10:04 +00:00
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())
2021-09-25 09:27:43 +00:00
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"]))
2024-05-25 23:28:40 +00:00
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())
2021-09-24 18:10:04 +00:00
rpc_connection.close()
2021-09-24 18:10:04 +00:00
except Exception as e:
2024-10-07 08:44:18 +00:00
RNS.log(f"An error ocurred while handling RPC call from local client: {e}", RNS.LOG_ERROR)
2021-09-24 18:10:04 +00:00
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:
2022-04-20 07:59:58 +00:00
interfaces = []
2021-09-24 18:10:04 +00:00
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
2021-09-24 18:10:04 +00:00
if hasattr(interface, "b32"):
if interface.b32 != None:
2024-10-07 08:44:18 +00:00
ifstats["i2p_b32"] = f"{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
2022-04-17 17:07:32 +00:00
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
2021-09-24 18:10:04 +00:00
ifstats["name"] = str(interface)
ifstats["short_name"] = str(interface.name)
ifstats["hash"] = interface.get_hash()
ifstats["type"] = str(type(interface).__name__)
2021-09-24 18:10:04 +00:00
ifstats["rxb"] = interface.rxb
ifstats["txb"] = interface.txb
2023-09-30 17:13:58 +00:00
ifstats["incoming_announce_frequency"] = interface.incoming_announce_frequency()
ifstats["outgoing_announce_frequency"] = interface.outgoing_announce_frequency()
2023-09-30 22:16:32 +00:00
ifstats["held_announces"] = len(interface.held_announces)
2021-09-24 18:10:04 +00:00
ifstats["status"] = interface.online
2022-04-16 21:26:57 +00:00
ifstats["mode"] = interface.mode
2022-04-17 17:07:32 +00:00
2022-04-20 07:59:58 +00:00
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
2021-09-24 18:10:04 +00:00
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()
2021-09-25 09:27:43 +00:00
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
2021-09-25 09:27:43 +00:00
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()
2023-11-02 11:24:42 +00:00
2023-11-02 12:04:09 +00:00
if self.is_connected_to_shared_instance and hasattr(self, "_force_shared_instance_bitrate") and self._force_shared_instance_bitrate:
2023-11-02 11:24:42 +00:00
simulated_latency = ((1/self._force_shared_instance_bitrate)*8)*RNS.Reticulum.MTU
2024-10-07 08:44:18 +00:00
RNS.log(f"Adding simulated latency of {RNS.prettytime(simulated_latency)} to first hop timeout", RNS.LOG_DEBUG)
2023-11-02 11:24:42 +00:00
response += simulated_latency
return response
except Exception as e:
2024-10-07 08:44:18 +00:00
RNS.log(f"An error occurred while getting first hop timeout from shared instance: {e}", RNS.LOG_ERROR)
return RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT
else:
return RNS.Transport.first_hop_timeout(destination)
2021-09-25 09:27:43 +00:00
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()
2024-08-29 12:54:40 +00:00
# TODO: Remove this debugging function
# if not response:
# response = RNS.Transport.next_hop(destination)
2021-09-25 09:27:43 +00:00
return response
2021-09-25 09:27:43 +00:00
else:
return RNS.Transport.next_hop(destination)
2024-05-25 23:28:40 +00:00
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
2020-08-13 10:15:56 +00:00
@staticmethod
def should_use_implicit_proof():
"""
2021-05-16 13:52:45 +00:00
Returns whether proofs sent are explicit or implicit.
2021-05-16 13:52:45 +00:00
:returns: True if the current running configuration specifies to use implicit proofs. False if not.
"""
2020-08-13 10:15:56 +00:00
return Reticulum.__use_implicit_proof
@staticmethod
def transport_enabled():
"""
2021-05-16 13:52:45 +00:00
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.
"""
2020-08-13 10:15:56 +00:00
return Reticulum.__transport_enabled
2020-04-27 10:04:14 +00:00
@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
2022-02-25 20:48:25 +00:00
[reticulum]
# If you enable Transport, your system will route traffic
# for other peers, pass announces and serve path requests.
2022-04-06 13:51:27 +00:00
# This should only be done for systems that are suited to
# act as transport nodes, ie. if they are stationary and
2021-08-20 09:23:35 +00:00
# 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
2021-09-24 12:13:31 +00:00
# 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
2021-09-24 12:13:31 +00:00
instance_control_port = 37429
2022-02-25 20:48:25 +00:00
# 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
2021-12-09 15:07:36 +00:00
# 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
2022-05-25 21:10:05 +00:00
enabled = Yes
2021-12-09 15:07:36 +00:00
'''.splitlines()