Compare commits

..

18 Commits

Author SHA1 Message Date
Mark Qvist bfade97051 Prepare release 2026-07-19 02:30:28 +02:00
Mark Qvist 3c4ef622cc Updated changelog 2026-07-19 02:29:09 +02:00
Mark Qvist 3898d63689 Block fast flapping configuration 2026-07-19 02:10:52 +02:00
Mark Qvist ec8d43a548 Updated rnsh config args to work similarly to other RNS utils 2026-07-19 00:57:27 +02:00
Mark Qvist 5081255317 Updated logging 2026-07-18 23:03:35 +02:00
Mark Qvist 13a538168f Cleanup 2026-07-18 21:30:46 +02:00
Mark Qvist a5ed0a4321 Improved HDLC handling 2026-07-18 21:26:57 +02:00
Mark Qvist d93f9798ae Cleanup 2026-07-18 19:23:02 +02:00
Mark Qvist 88833f170b Updated documentation 2026-07-18 16:46:04 +02:00
Mark Qvist cd6911ed53 Implemented automated fast-flapping client blocking on BackboneInterface 2026-07-18 16:45:42 +02:00
Mark Qvist b95c51b96e Added LOG_PATHING loglevel 2026-07-18 15:07:06 +02:00
Mark Qvist 3a36c367fe Improved resource handling 2026-07-18 02:46:06 +02:00
Mark Qvist cf6010da59 Enable discovery for internal-mode interfaces 2026-07-17 11:43:14 +02:00
Mark Qvist bb2897445f Added RESOURCE_RCL signal on resource receiver cancel. Improved link identify and resource safeguards. 2026-07-16 15:18:08 +02:00
Mark Qvist 406b141370 Updated version 2026-07-14 20:42:42 +02:00
Mark Qvist 00b2f81a82 Added ability to resolve discoverable interface location from external executable 2026-07-14 20:41:28 +02:00
Mark Qvist 9adf045a31 Added discovery location script info to docs 2026-07-14 20:40:45 +02:00
Mark Qvist de0f399a16 Prepare release 2026-07-10 12:42:56 +02:00
55 changed files with 926 additions and 589 deletions
+24 -6
View File
@@ -1,12 +1,20 @@
### 2026-07-10: RNS 1.3.8 ### 2026-07-19: RNS 1.3.9
This release fixes various inconsistencies in link and hop-count related APIs. **Critical Security Update**: This release fixes a severe security flaw in `rnsh`. Due to the nature of the issue, I will not disclose any further details for the time being. Once operators have had time to update, I will provide a full report for transparency. If you use `rnsh`, update **right now**.
**Important**: The new version of `rnsh` changes default identity file locations, and these will now be sourced from `~/.rnsh/identity` (initiator) and `~/.rnsh/identity.default` (listener). Make sure you copy your old files to this directory, or specify a custom identity path using the command line arguments. The `--config` argument has also been renamed to `--rnsconfig`, and the `--config` argument will now specify the `rnsh` configuration directory instead of the RNS configuration directory, bringing the behavior into alignment with other RNS utilities.
Additionally, this release includes automated blocking of fast-flapping clients on `BackboneInterface` listeners (see the Interfaces chapter of the manual for details), and a number of improvements to resource handling. The logging system has also been improved, and path and destination information moved to a new `LOG_PATHING` loglevel, to decrease log noise.
**Changes** **Changes**
- Fixed inconsistent link traffic stats calculation - Fixed a critical security issue in `rnsh`
- Fixed link hop-count metric only being available on initiator side - Added automated blocking of fast-flapping clients to `BackboneInterface`
- Fixed potential hop-count serialization error on transport - Added new `LOG_PATHING` loglevel, improved logging
- Updated `WeaveInterface` to support latest Weave firmware - Added ability to make internal-mode interfaces discoverable
- Added ability to get discoverable interface location from external script
- Added `RESOURCE_RCL` signal on resource receiver cancel
- Improved resource handling and reliability
- Updated `rnsh` config args to work similarly to other RNS utilities
**Verified Retrieval** **Verified Retrieval**
You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To retrieve only the installation `.whl` package, and the release manifest for future updates, you can use: You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To retrieve only the installation `.whl` package, and the release manifest for future updates, you can use:
@@ -36,6 +44,16 @@ rnid -i bc7291552be7a58f361522990465165c -V rns_*.rsm *.rsg
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded. The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
### 2026-07-10: RNS 1.3.8
This release fixes various inconsistencies in link and hop-count related APIs.
**Changes**
- Fixed inconsistent link traffic stats calculation
- Fixed link hop-count metric only being available on initiator side
- Fixed potential hop-count serialization error on transport
- Updated `WeaveInterface` to support latest Weave firmware
### 2026-07-03: RNS 1.3.7 ### 2026-07-03: RNS 1.3.7
This maintenance release improves announces propagation logic, and adds additional options for configuring announce propagation and interface behavior in transport mode. This maintenance release improves announces propagation logic, and adds additional options for configuring announce propagation and interface behavior in transport mode.
+28 -1
View File
@@ -99,6 +99,33 @@ class InterfaceAnnouncer():
if not interface_type in self.DISCOVERABLE_INTERFACE_TYPES: return None if not interface_type in self.DISCOVERABLE_INTERFACE_TYPES: return None
else: else:
if not RNS.vendor.platformutils.is_windows() and interface.discovery_location:
try:
discovery_location = self.sanitize(interface.discovery_location)
exec_path = os.path.expanduser(discovery_location)
if os.path.isfile(exec_path) and os.access(exec_path, os.X_OK):
RNS.log(f"Evaluating discovery location from executable at {exec_path}", RNS.LOG_DEBUG)
exec_result = subprocess.run([exec_path], stdout=subprocess.PIPE)
exec_stdout = exec_result.stdout.decode("utf-8")
if exec_result.returncode != 0: raise ValueError("Non-zero exit code from subprocess")
discovery_location = self.sanitize(exec_stdout)
location_components = discovery_location.replace(" ", "").split(",")
if len(location_components) != 3: raise ValueError(f"Invalid location component count: {len(location_components)}")
dlat = float(location_components[0])
dlon = float(location_components[1])
dhgt = float(location_components[2])
if dlat < -90 or dlat > 90: raise ValueError(f"Invalid latitude: {dlat}")
if dlon < -180 or dlat > 180: raise ValueError(f"Invalid longitude: {dlon}")
if dhgt < -4000 or dhgt > 1e6: raise ValueError(f"Invalid height: {dhgt}")
interface.discovery_latitude = dlat
interface.discovery_longitude = dlon
interface.discovery_height = dhgt
except Exception as e:
RNS.log(f"Error while getting reachable_on from executable at {interface.reachable_on}: {e}", RNS.LOG_ERROR)
RNS.log(f"Aborting discovery announce", RNS.LOG_ERROR)
return None
flags = 0x00 flags = 0x00
info = {INTERFACE_TYPE: interface_type, info = {INTERFACE_TYPE: interface_type,
TRANSPORT: RNS.Reticulum.transport_enabled(), TRANSPORT: RNS.Reticulum.transport_enabled(),
@@ -237,7 +264,7 @@ class InterfaceAnnounceHandler:
valid = self.stamper.stamp_valid(stamp, self.required_value, workblock) valid = self.stamper.stamp_valid(stamp, self.required_value, workblock)
if not valid: if not valid:
RNS.log(f"Ignored discovered interface with invalid stamp", RNS.LOG_DEBUG) RNS.log(f"Ignored discovered interface with insufficient stamp value {value}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
return return
if value < self.required_value: RNS.log(f"Ignored discovered interface with stamp value {value}", RNS.LOG_DEBUG) if value < self.required_value: RNS.log(f"Ignored discovered interface with stamp value {value}", RNS.LOG_DEBUG)
+2 -2
View File
@@ -213,7 +213,7 @@ class Identity:
except Exception as e: except Exception as e:
RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING) RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING)
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_VERBOSE) RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
temp_file = RNS.Reticulum.storagepath+f"/known_destinations.tmp.{time.time()}" temp_file = RNS.Reticulum.storagepath+f"/known_destinations.tmp.{time.time()}"
try: try:
@@ -230,7 +230,7 @@ class Identity:
if save_time < 1: time_str = str(round(save_time*1000,2))+"ms" if save_time < 1: time_str = str(round(save_time*1000,2))+"ms"
else: time_str = str(round(save_time,2))+"s" else: time_str = str(round(save_time,2))+"s"
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_VERBOSE) RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
except Exception as e: except Exception as e:
RNS.log("Error while saving known destinations to disk, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error while saving known destinations to disk, the contained exception was: "+str(e), RNS.LOG_ERROR)
+123 -40
View File
@@ -54,6 +54,13 @@ class BackboneInterface(Interface):
DEFAULT_IFAC_SIZE = 16 DEFAULT_IFAC_SIZE = 16
AUTOCONFIGURE_MTU = True AUTOCONFIGURE_MTU = True
BLOCK_FAST_FLAPPING = True
FAST_FLAP_THRESHOLD = 20
FAST_FLAP_GRACE = 5
FAST_FLAP_EXPIRY = 12*60*60
fast_flapping_lock = threading.Lock()
fast_flapping = {}
epoll = None epoll = None
listener_filenos = {} listener_filenos = {}
spawned_interface_filenos = {} spawned_interface_filenos = {}
@@ -109,13 +116,17 @@ class BackboneInterface(Interface):
super().__init__() super().__init__()
c = Interface.get_config_obj(configuration) c = Interface.get_config_obj(configuration)
name = c["name"] name = c["name"]
device = c["device"] if "device" in c else None device = c["device"] if "device" in c else None
port = int(c["port"]) if "port" in c else None port = int(c["port"]) if "port" in c else None
bindip = c["listen_ip"] if "listen_ip" in c else None bindip = c["listen_ip"] if "listen_ip" in c else None
bindport = int(c["listen_port"]) if "listen_port" in c else None bindport = int(c["listen_port"]) if "listen_port" in c else None
prefer_ipv6 = c.as_bool("prefer_ipv6") if "prefer_ipv6" in c else False prefer_ipv6 = c.as_bool("prefer_ipv6") if "prefer_ipv6" in c else False
flap_block = c.as_bool("block_fast_flapping") if "block_fast_flapping" in c else BackboneInterface.BLOCK_FAST_FLAPPING
flap_threshold = c.as_float("fast_flapping_threshold") if "fast_flapping_threshold" in c else BackboneInterface.FAST_FLAP_THRESHOLD
flap_grace = c.as_int("fast_flapping_grace") if "fast_flapping_grace" in c else BackboneInterface.FAST_FLAP_GRACE
flap_expiry = c.as_float("fast_flapping_block_time")*60 if "fast_flapping_block_time" in c else BackboneInterface.FAST_FLAP_EXPIRY
if port != None: bindport = port if port != None: bindport = port
@@ -128,11 +139,13 @@ class BackboneInterface(Interface):
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
self.spawned_interfaces = [] self.spawned_interfaces = []
self.supports_discovery = True self.supports_discovery = True
self.block_fast_flapping = flap_block
self.fast_flap_threshold = flap_threshold
self.fast_flap_grace = flap_grace
self.fast_flap_expiry = flap_expiry
if bindport == None: if bindport == None: raise SystemError(f"No TCP port configured for interface \"{name}\"")
raise SystemError(f"No TCP port configured for interface \"{name}\"") else: self.bind_port = bindport
else:
self.bind_port = bindport
bind_address = None bind_address = None
if device != None: if device != None:
@@ -282,7 +295,9 @@ class BackboneInterface(Interface):
if fileno in BackboneInterface.spawned_interface_filenos: if fileno in BackboneInterface.spawned_interface_filenos:
try: BackboneInterface.epoll.modify(fileno, select.EPOLLOUT) try: BackboneInterface.epoll.modify(fileno, select.EPOLLOUT)
except Exception as e: except Exception as e:
RNS.log(f"Error occurred on {interface} while modifying socket EPOLL state: {e}", RNS.LOG_WARNING) if str(e).endswith("No such file or directory"): pass
elif str(e).endswith("Bad file descriptor"): pass
else: RNS.log(f"Error occurred on {interface} while modifying socket EPOLL state: {e}", RNS.LOG_WARNING)
raise e raise e
@staticmethod @staticmethod
@@ -302,7 +317,7 @@ class BackboneInterface(Interface):
if client_socket and fileno == client_socket.fileno() and (event & select.EPOLLIN): if client_socket and fileno == client_socket.fileno() and (event & select.EPOLLIN):
try: received_bytes = client_socket.recv(spawned_interface.HW_MTU) try: received_bytes = client_socket.recv(spawned_interface.HW_MTU)
except Exception as e: except Exception as e:
RNS.log(f"Error while reading from {spawned_interface}: {e}", RNS.LOG_DEBUG) RNS.log(f"Error while reading from {spawned_interface}: {e}", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
received_bytes = b"" received_bytes = b""
if len(received_bytes): spawned_interface.receive(received_bytes) if len(received_bytes): spawned_interface.receive(received_bytes)
@@ -325,7 +340,11 @@ class BackboneInterface(Interface):
try: written = client_socket.send(spawned_interface.transmit_buffer) try: written = client_socket.send(spawned_interface.transmit_buffer)
except Exception as e: except Exception as e:
written = 0 written = 0
if not spawned_interface.detached: RNS.log(f"Error while writing to {spawned_interface}: {e}", RNS.LOG_DEBUG) if not spawned_interface.detached:
if RNS.sl(RNS.LOG_DEBUG):
if str(e).endswith("Connection timed out"): pass
elif str(e).endswith("Connection reset by peer"): pass
else: RNS.log(f"Error while writing to {spawned_interface}: {e}", RNS.LOG_DEBUG)
BackboneInterface.deregister_fileno(fileno) BackboneInterface.deregister_fileno(fileno)
try: try:
@@ -399,8 +418,22 @@ class BackboneInterface(Interface):
BackboneInterface.deregister_listeners() BackboneInterface.deregister_listeners()
def incoming_connection(self, socket): def incoming_connection(self, socket):
RNS.log("Accepting incoming connection", RNS.LOG_VERBOSE)
try: try:
remote_ip = socket.getpeername()[0]
remote_port = str(socket.getpeername()[1])
if self.blocked_ip_count > 0:
with BackboneInterface.fast_flapping_lock:
if remote_ip in BackboneInterface.fast_flapping:
now = time.time()
ffe = BackboneInterface.fast_flapping[remote_ip]
started_flapping = ffe[0]
last_flap = ffe[1]
flaps = ffe[2]
if flaps > self.fast_flap_grace:
RNS.log(f"Ignoring incoming connection from fast-flapping IP {remote_ip}", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
return False
RNS.log("Accepting incoming connection", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
spawned_configuration = {"name": "Client on "+self.name, "target_host": None, "target_port": None} spawned_configuration = {"name": "Client on "+self.name, "target_host": None, "target_port": None}
spawned_interface = BackboneClientInterface(self.owner, spawned_configuration, connected_socket=socket) spawned_interface = BackboneClientInterface(self.owner, spawned_configuration, connected_socket=socket)
spawned_interface.OUT = self.OUT spawned_interface.OUT = self.OUT
@@ -421,8 +454,8 @@ class BackboneInterface(Interface):
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
spawned_interface.socket = socket spawned_interface.socket = socket
spawned_interface.target_ip = socket.getpeername()[0] spawned_interface.target_ip = remote_ip
spawned_interface.target_port = str(socket.getpeername()[1]) spawned_interface.target_port = remote_port
spawned_interface.parent_interface = self spawned_interface.parent_interface = self
spawned_interface.bitrate = self.bitrate spawned_interface.bitrate = self.bitrate
spawned_interface.optimise_mtu() spawned_interface.optimise_mtu()
@@ -432,18 +465,12 @@ class BackboneInterface(Interface):
spawned_interface.ifac_netkey = self.ifac_netkey spawned_interface.ifac_netkey = self.ifac_netkey
if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None: if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
ifac_origin = b"" ifac_origin = b""
if spawned_interface.ifac_netname != None: if spawned_interface.ifac_netname != None: ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8")) if spawned_interface.ifac_netkey != None: ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
if spawned_interface.ifac_netkey != None:
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin) ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
spawned_interface.ifac_key = RNS.Cryptography.hkdf( spawned_interface.ifac_key = RNS.Cryptography.hkdf(length=64, derive_from=ifac_origin_hash,
length=64, salt=RNS.Reticulum.IFAC_SALT, context=None)
derive_from=ifac_origin_hash,
salt=RNS.Reticulum.IFAC_SALT,
context=None
)
spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key) spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key)) spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
@@ -452,12 +479,13 @@ class BackboneInterface(Interface):
spawned_interface.announce_rate_penalty = self.announce_rate_penalty spawned_interface.announce_rate_penalty = self.announce_rate_penalty
spawned_interface.mode = self.mode spawned_interface.mode = self.mode
spawned_interface.HW_MTU = self.HW_MTU spawned_interface.HW_MTU = self.HW_MTU
spawned_interface.online = True RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
RNS.Transport.add_interface(spawned_interface) RNS.Transport.add_interface(spawned_interface)
while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface) while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface)
self.spawned_interfaces.append(spawned_interface) self.spawned_interfaces.append(spawned_interface)
BackboneInterface.add_client_socket(socket, spawned_interface) BackboneInterface.add_client_socket(socket, spawned_interface)
spawned_interface.spawned_at = time.time()
spawned_interface.online = True
except Exception as e: except Exception as e:
RNS.log(f"An error occurred while accepting incoming connection on {self}: {e}", RNS.LOG_ERROR) RNS.log(f"An error occurred while accepting incoming connection on {self}: {e}", RNS.LOG_ERROR)
@@ -491,15 +519,38 @@ class BackboneInterface(Interface):
if callable(listener_socket.shutdown): if callable(listener_socket.shutdown):
try: listener_socket.shutdown(socket.SHUT_RDWR) try: listener_socket.shutdown(socket.SHUT_RDWR)
except Exception as e: except Exception as e:
if str(e).endswith("Transport endpoint is not connected"): pass if str(e).endswith("Transport endpoint is not connected"): pass
elif str(e).endswith("Bad file descriptor"): pass
else: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e), RNS.LOG_ERROR) else: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e), RNS.LOG_ERROR)
def __str__(self): @property
if ":" in self.bind_ip: def blocked_ip_count(self):
ip_str = f"[{self.bind_ip}]" if not self.block_fast_flapping: return 0
else: else:
ip_str = f"{self.bind_ip}" count = 0
expired = []
now = time.time()
with BackboneInterface.fast_flapping_lock:
for remote_ip in BackboneInterface.fast_flapping:
ffe = BackboneInterface.fast_flapping[remote_ip]
started_flapping = ffe[0]
last_flap = ffe[1]
flaps = ffe[2]
if now - last_flap > self.fast_flap_expiry: expired.append(remote_ip)
elif flaps > self.fast_flap_grace: count += 1
for remote_ip in expired:
if remote_ip in BackboneInterface.fast_flapping:
try:
BackboneInterface.fast_flapping.pop(remote_ip)
RNS.log(f"Fast-flapping block expired for {remote_ip}", RNS.LOG_DEBUG)
except Exception as e: RNS.log(f"Error while expiring fast-flapping block for {remote_ip}: {e}", RNS.LOG_ERROR)
return count
def __str__(self):
if ":" in self.bind_ip: ip_str = f"[{self.bind_ip}]"
else: ip_str = f"{self.bind_ip}"
return "BackboneInterface["+self.name+"/"+ip_str+":"+str(self.bind_port)+"]" return "BackboneInterface["+self.name+"/"+ip_str+":"+str(self.bind_port)+"]"
@@ -710,6 +761,14 @@ class BackboneClientInterface(Interface):
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
self.teardown() self.teardown()
def check_frame_len(self, frame_len):
if frame_len <= RNS.Reticulum.HEADER_MINSIZE: return False
elif frame_len > self.HW_MTU + (self.ifac_size or 0): return False
else: return True
def invalid_frame(self, frame_len):
RNS.log(f"Invalid HDLC frame of {RNS.prettysize(frame_len)} received on {self}, dropping frame", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
def receive(self, data_in): def receive(self, data_in):
try: try:
if len(data_in) > 0: if len(data_in) > 0:
@@ -723,12 +782,18 @@ class BackboneClientInterface(Interface):
frame = self.frame_buffer[frame_start+1:frame_end] frame = self.frame_buffer[frame_start+1:frame_end]
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG])) frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC])) frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
if len(frame) > RNS.Reticulum.HEADER_MINSIZE: frame_len = len(frame)
self.process_incoming(frame) if frame_len != 0:
if self.check_frame_len(frame_len): self.process_incoming(frame)
else: self.invalid_frame(len(frame))
self.frame_buffer = self.frame_buffer[frame_end:] self.frame_buffer = self.frame_buffer[frame_end:]
else: else:
if len(self.frame_buffer) > self.HW_MTU*2: self.frame_buffer = b""
flags_remaining = False flags_remaining = False
else: else:
self.frame_buffer = b""
flags_remaining = False flags_remaining = False
else: else:
@@ -738,7 +803,7 @@ class BackboneClientInterface(Interface):
def job(): self.reconnect() def job(): self.reconnect()
threading.Thread(target=job, daemon=True).start() threading.Thread(target=job, daemon=True).start()
else: else:
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_DEBUG) RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
self.teardown() self.teardown()
except Exception as e: except Exception as e:
@@ -755,11 +820,29 @@ class BackboneClientInterface(Interface):
def teardown(self): def teardown(self):
if self.initiator and not self.detached: if self.initiator and not self.detached:
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR) RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
if RNS.Reticulum.panic_on_interface_error: if RNS.Reticulum.panic_on_interface_error: RNS.panic()
RNS.panic()
else: else:
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_VERBOSE) RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
if self.parent_interface.block_fast_flapping and hasattr(self, "spawned_at"):
connected_time = time.time() - self.spawned_at
if connected_time < self.parent_interface.fast_flap_threshold:
try:
now = time.time()
remote_id = f"{self.target_ip}"
with BackboneInterface.fast_flapping_lock:
if remote_id in BackboneInterface.fast_flapping: ffe = BackboneInterface.fast_flapping[remote_id]
else: ffe = [now, now, 0]
ffe[1] = now
ffe[2] += 1
with BackboneInterface.fast_flapping_lock: BackboneInterface.fast_flapping[remote_id] = ffe
dt = now-ffe[0]; fff = ffe[2]/dt if dt > 0 else None
freq_str = f" at {RNS.prettyfrequency(fff)}" if fff else ""
RNS.log(f"{self} is fast flapping{freq_str}, connection time was {RNS.prettytime(connected_time)}, {ffe[2]} fast flaps", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
if ffe[2] > self.parent_interface.fast_flap_grace: RNS.log(f"Ignoring further connections from {remote_id} due to fast-flapping", RNS.LOG_WARNING)
except Exception as e: RNS.log(f"Error while updating fast-flapping interface statistics: {e}", RNS.LOG_ERROR)
self.online = False self.online = False
self.OUT = False self.OUT = False
+1 -1
View File
@@ -212,7 +212,7 @@ class Interface:
elif self.bitrate > 62_500: self.HW_MTU = 1024 elif self.bitrate > 62_500: self.HW_MTU = 1024
else: self.HW_MTU = None else: self.HW_MTU = None
RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_DEBUG) RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_PATHING)
def age(self): def age(self):
return time.time()-self.created return time.time()-self.created
+15 -2
View File
@@ -333,6 +333,13 @@ class TCPClientInterface(Interface):
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
self.teardown() self.teardown()
def check_frame_len(self, frame_len):
if frame_len <= RNS.Reticulum.HEADER_MINSIZE: return False
elif frame_len > self.HW_MTU + (self.ifac_size or 0): return False
else: return True
def invalid_frame(self, frame_len):
RNS.log(f"Invalid HDLC frame of {RNS.prettysize(frame_len)} received on {self}, dropping frame", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
def read_loop(self): def read_loop(self):
try: try:
@@ -389,12 +396,18 @@ class TCPClientInterface(Interface):
frame = frame_buffer[frame_start+1:frame_end] frame = frame_buffer[frame_start+1:frame_end]
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG])) frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC])) frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
if len(frame) > RNS.Reticulum.HEADER_MINSIZE: frame_len = len(frame)
self.process_incoming(frame) if frame_len != 0:
if self.check_frame_len(frame_len): self.process_incoming(frame)
else: self.invalid_frame(len(frame))
frame_buffer = frame_buffer[frame_end:] frame_buffer = frame_buffer[frame_end:]
else: else:
if len(frame_buffer) > self.HW_MTU*2: frame_buffer = b""
flags_remaining = False flags_remaining = False
else: else:
frame_buffer = b""
flags_remaining = False flags_remaining = False
else: else:
+40 -35
View File
@@ -664,6 +664,7 @@ class Link:
Closes the link and purges encryption keys. New keys will Closes the link and purges encryption keys. New keys will
be used if a new link to the same destination is established. be used if a new link to the same destination is established.
""" """
if self.status == Link.CLOSED: return
if self.status != Link.PENDING and self.status != Link.CLOSED: self.__teardown_packet() if self.status != Link.PENDING and self.status != Link.CLOSED: self.__teardown_packet()
self.status = Link.CLOSED self.status = Link.CLOSED
if self.initiator: self.teardown_reason = Link.INITIATOR_CLOSED if self.initiator: self.teardown_reason = Link.INITIATOR_CLOSED
@@ -807,7 +808,7 @@ class Link:
request_data = unpacked_request[2] request_data = unpacked_request[2]
if path_hash in self.destination.request_handlers: if path_hash in self.destination.request_handlers:
request_handler = self.destination.request_handlers[path_hash] request_handler = self.destination.request_handlers[path_hash]
path = request_handler[0] path = request_handler[0]
response_generator = request_handler[1] response_generator = request_handler[1]
allow = request_handler[2] allow = request_handler[2]
@@ -968,10 +969,11 @@ class Link:
self.teardown() self.teardown()
else: else:
self.__remote_identity = identity if self.__remote_identity == None:
if self.callbacks.remote_identified != None: self.__remote_identity = identity
try: self.callbacks.remote_identified(self, self.__remote_identity) if self.callbacks.remote_identified != None:
except Exception as e: RNS.log(f"Error while executing remote identified callback from {self}. The contained exception was: "+str(e), RNS.LOG_ERROR) try: self.callbacks.remote_identified(self, self.__remote_identity)
except Exception as e: RNS.log(f"Error while executing remote identified callback from {self}. The contained exception was: "+str(e), RNS.LOG_ERROR)
self.__update_phy_stats(packet, query_shared=True) self.__update_phy_stats(packet, query_shared=True)
@@ -1012,36 +1014,39 @@ class Link:
packet.plaintext = self.decrypt(packet.data) packet.plaintext = self.decrypt(packet.data)
if packet.plaintext != None: if packet.plaintext != None:
self.__update_phy_stats(packet, query_shared=True) self.__update_phy_stats(packet, query_shared=True)
try:
if RNS.ResourceAdvertisement.is_request(packet): if RNS.ResourceAdvertisement.is_request(packet):
RNS.Resource.accept(packet, callback=self.request_resource_concluded) if self.destination.request_handlers:
elif RNS.ResourceAdvertisement.is_response(packet): RNS.Resource.accept(packet, callback=self.request_resource_concluded)
request_id = RNS.ResourceAdvertisement.read_request_id(packet) elif RNS.ResourceAdvertisement.is_response(packet):
for pending_request in self.pending_requests: request_id = RNS.ResourceAdvertisement.read_request_id(packet)
if pending_request.request_id == request_id: for pending_request in self.pending_requests:
response_resource = RNS.Resource.accept(packet, callback=self.response_resource_concluded, progress_callback=pending_request.response_resource_progress, request_id = request_id) if pending_request.request_id == request_id:
if response_resource != None: response_resource = RNS.Resource.accept(packet, callback=self.response_resource_concluded, progress_callback=pending_request.response_resource_progress, request_id = request_id)
if pending_request.response_size == None: if response_resource != None:
pending_request.response_size = RNS.ResourceAdvertisement.read_size(packet) if pending_request.response_size == None:
if pending_request.response_transfer_size == None: pending_request.response_size = RNS.ResourceAdvertisement.read_size(packet)
pending_request.response_transfer_size = 0 if pending_request.response_transfer_size == None:
pending_request.response_transfer_size += RNS.ResourceAdvertisement.read_transfer_size(packet) pending_request.response_transfer_size = 0
if pending_request.started_at == None: pending_request.response_transfer_size += RNS.ResourceAdvertisement.read_transfer_size(packet)
pending_request.started_at = time.time() if pending_request.started_at == None:
pending_request.response_resource_progress(response_resource) pending_request.started_at = time.time()
pending_request.response_resource_progress(response_resource)
elif self.resource_strategy == Link.ACCEPT_NONE: pass elif self.resource_strategy == Link.ACCEPT_NONE: pass
elif self.resource_strategy == Link.ACCEPT_APP: elif self.resource_strategy == Link.ACCEPT_APP:
if self.callbacks.resource != None: if self.callbacks.resource != None:
try: try:
resource_advertisement = RNS.ResourceAdvertisement.unpack(packet.plaintext) resource_advertisement = RNS.ResourceAdvertisement.unpack(packet.plaintext)
resource_advertisement.link = self resource_advertisement.link = self
if self.callbacks.resource(resource_advertisement): RNS.Resource.accept(packet, self.callbacks.resource_concluded) if self.callbacks.resource(resource_advertisement): RNS.Resource.accept(packet, self.callbacks.resource_concluded)
else: RNS.Resource.reject(packet) else: RNS.Resource.reject(packet)
except Exception as e: except Exception as e:
RNS.log("Error while executing resource accept callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error while executing resource accept callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
elif self.resource_strategy == Link.ACCEPT_ALL: elif self.resource_strategy == Link.ACCEPT_ALL:
RNS.Resource.accept(packet, self.callbacks.resource_concluded) RNS.Resource.accept(packet, self.callbacks.resource_concluded)
except Exception as e:
RNS.log(f"Invalid resource advertisement on {self}: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
self.teardown()
elif packet.context == RNS.Packet.RESOURCE_REQ: elif packet.context == RNS.Packet.RESOURCE_REQ:
plaintext = self.decrypt(packet.data) plaintext = self.decrypt(packet.data)
+85 -98
View File
@@ -149,7 +149,7 @@ class Resource:
COMPLETE = 0x06 COMPLETE = 0x06
FAILED = 0x07 FAILED = 0x07
CORRUPT = 0x08 CORRUPT = 0x08
REJECTED = 0x00 REJECTED = 0x09
@staticmethod @staticmethod
def reject(advertisement_packet): def reject(advertisement_packet):
@@ -239,7 +239,7 @@ class Resource:
return None return None
except Exception as e: except Exception as e:
RNS.log("Could not decode resource advertisement, dropping resource", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log(f"Could not decode resource advertisement, dropping resource: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
return None return None
# Create a resource for transmission to a remote destination # Create a resource for transmission to a remote destination
@@ -314,30 +314,26 @@ class Resource:
self.input_file = data self.input_file = data
elif isinstance(data, bytes): elif isinstance(data, bytes):
data_size = len(data) data_size = len(data)
self.total_size = data_size + self.metadata_size self.total_size = data_size + self.metadata_size
resource_data = data resource_data = data
self.total_segments = 1 self.total_segments = 1
self.segment_index = 1 self.segment_index = 1
self.split = False self.split = False
elif data == None: elif data == None: pass
pass
else: else: raise TypeError("Invalid data instance type passed to resource initialisation")
raise TypeError("Invalid data instance type passed to resource initialisation")
if resource_data: if resource_data:
if self.has_metadata: data = self.metadata + resource_data if self.has_metadata: data = self.metadata + resource_data
else: data = resource_data else: data = resource_data
self.status = Resource.NONE self.status = Resource.NONE
self.link = link self.link = link
if self.link.mtu: if self.link.mtu: self.sdu = self.link.mtu - RNS.Reticulum.HEADER_MAXSIZE - RNS.Reticulum.IFAC_MIN_SIZE
self.sdu = self.link.mtu - RNS.Reticulum.HEADER_MAXSIZE - RNS.Reticulum.IFAC_MIN_SIZE else: self.sdu = link.mdu or Resource.SDU
else:
self.sdu = link.mdu or Resource.SDU
self.max_retries = Resource.MAX_RETRIES self.max_retries = Resource.MAX_RETRIES
self.max_adv_retries = Resource.MAX_ADV_RETRIES self.max_adv_retries = Resource.MAX_ADV_RETRIES
self.retries_left = self.max_retries self.retries_left = self.max_retries
@@ -482,12 +478,12 @@ class Resource:
def hashmap_update_packet(self, plaintext): def hashmap_update_packet(self, plaintext):
if not self.status == Resource.FAILED: if not self.status == Resource.FAILED:
self.last_activity = time.time() if self.waiting_for_hmu:
self.retries_left = self.max_retries self.last_activity = time.time()
self.retries_left = self.max_retries
update = umsgpack.unpackb(plaintext[RNS.Identity.HASHLENGTH//8:])
self.hashmap_update(update[0], update[1])
update = umsgpack.unpackb(plaintext[RNS.Identity.HASHLENGTH//8:])
self.hashmap_update(update[0], update[1])
def hashmap_update(self, segment, hashmap): def hashmap_update(self, segment, hashmap):
if not self.status == Resource.FAILED: if not self.status == Resource.FAILED:
@@ -495,12 +491,16 @@ class Resource:
seg_len = ResourceAdvertisement.HASHMAP_MAX_LEN seg_len = ResourceAdvertisement.HASHMAP_MAX_LEN
hashes = len(hashmap)//Resource.MAPHASH_LEN hashes = len(hashmap)//Resource.MAPHASH_LEN
for i in range(0,hashes): for i in range(0,hashes):
if self.hashmap[i+segment*seg_len] == None: if self.hashmap[i+segment*seg_len] == None: self.hashmap_height += 1
self.hashmap_height += 1
self.hashmap[i+segment*seg_len] = hashmap[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN] self.hashmap[i+segment*seg_len] = hashmap[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN]
self.waiting_for_hmu = False if hashes < 1:
self.request_next() RNS.log("Invalid HMU received, cancelling transfer", RNS.LOG_ERROR)
self.cancel()
else:
self.waiting_for_hmu = False
self.request_next()
def get_map_hash(self, data): def get_map_hash(self, data):
return RNS.Identity.full_hash(data+self.random_hash)[:Resource.MAPHASH_LEN] return RNS.Identity.full_hash(data+self.random_hash)[:Resource.MAPHASH_LEN]
@@ -517,6 +517,14 @@ class Resource:
prepare_thread = threading.Thread(target=self.__prepare_next_segment, daemon=True) prepare_thread = threading.Thread(target=self.__prepare_next_segment, daemon=True)
prepare_thread.start() prepare_thread.start()
def ensure_link(self):
if not self.link or self.link.status != RNS.Link.ACTIVE:
RNS.log(f"Invalid link state for {self}, aborting transfer", RNS.LOG_VERBOSE)
try: self.cancel()
except Exception as e: RNS.log(f"Error while cancelling resource on link-state abort: {e}", RNS.LOG_ERROR)
return False
else: return True
def __advertise_job(self): def __advertise_job(self):
self.advertisement_packet = RNS.Packet(self.link, ResourceAdvertisement(self).pack(), context=RNS.Packet.RESOURCE_ADV) self.advertisement_packet = RNS.Packet(self.link, ResourceAdvertisement(self).pack(), context=RNS.Packet.RESOURCE_ADV)
while not self.link.ready_for_new_resource(): while not self.link.ready_for_new_resource():
@@ -524,6 +532,7 @@ class Resource:
sleep(0.25) sleep(0.25)
try: try:
if not self.ensure_link(): return
self.advertisement_packet.send() self.advertisement_packet.send()
self.last_activity = time.time() self.last_activity = time.time()
self.started_transferring = self.last_activity self.started_transferring = self.last_activity
@@ -541,18 +550,13 @@ class Resource:
self.watchdog_job() self.watchdog_job()
def update_eifr(self): def update_eifr(self):
if self.rtt == None: if self.rtt == None: rtt = self.link.rtt
rtt = self.link.rtt else: rtt = self.rtt
else:
rtt = self.rtt
if self.req_data_rtt_rate != 0: if self.req_data_rtt_rate != 0: expected_inflight_rate = self.req_data_rtt_rate*8
expected_inflight_rate = self.req_data_rtt_rate*8
else: else:
if self.previous_eifr != None: if self.previous_eifr != None: expected_inflight_rate = self.previous_eifr
expected_inflight_rate = self.previous_eifr else: expected_inflight_rate = self.link.establishment_cost*8 / rtt
else:
expected_inflight_rate = self.link.establishment_cost*8 / rtt
self.eifr = expected_inflight_rate self.eifr = expected_inflight_rate
if self.link: self.link.expected_rate = self.eifr if self.link: self.link.expected_rate = self.eifr
@@ -566,8 +570,7 @@ class Resource:
this_job_id = self.__watchdog_job_id this_job_id = self.__watchdog_job_id
while self.status < Resource.ASSEMBLING and this_job_id == self.__watchdog_job_id: while self.status < Resource.ASSEMBLING and this_job_id == self.__watchdog_job_id:
while self.watchdog_lock: while self.watchdog_lock: sleep(0.025)
sleep(0.025)
sleep_time = None sleep_time = None
if self.status == Resource.ADVERTISED: if self.status == Resource.ADVERTISED:
@@ -581,6 +584,7 @@ class Resource:
try: try:
RNS.log("No part requests received, retrying resource advertisement...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("No part requests received, retrying resource advertisement...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
self.retries_left -= 1 self.retries_left -= 1
if not self.ensure_link(): return
self.advertisement_packet = RNS.Packet(self.link, ResourceAdvertisement(self).pack(), context=RNS.Packet.RESOURCE_ADV) self.advertisement_packet = RNS.Packet(self.link, ResourceAdvertisement(self).pack(), context=RNS.Packet.RESOURCE_ADV)
self.advertisement_packet.send() self.advertisement_packet.send()
self.last_activity = time.time() self.last_activity = time.time()
@@ -657,13 +661,13 @@ class Resource:
self.last_part_sent = time.time() self.last_part_sent = time.time()
sleep_time = 0.001 sleep_time = 0.001
elif self.status == Resource.REJECTED: elif self.status >= Resource.ASSEMBLING:
sleep_time = 0.001 sleep_time = 0.001
if sleep_time == 0: if sleep_time == 0:
RNS.log("Warning! Link watchdog sleep time of 0!", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Warning! Link watchdog sleep time of 0!", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
if sleep_time == None or sleep_time < 0: if sleep_time == None or sleep_time < 0:
RNS.log("Timing error, cancelling resource transfer.", RNS.LOG_ERROR) RNS.log(f"Timing error ({sleep_time}/{self.status}), cancelling resource transfer.", RNS.LOG_ERROR)
self.cancel() self.cancel()
if sleep_time != None: if sleep_time != None:
@@ -754,6 +758,7 @@ class Resource:
try: try:
proof = RNS.Identity.full_hash(self.data+self.hash) proof = RNS.Identity.full_hash(self.data+self.hash)
proof_data = self.hash+proof proof_data = self.hash+proof
if not self.ensure_link(): return
proof_packet = RNS.Packet(self.link, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.RESOURCE_PRF) proof_packet = RNS.Packet(self.link, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.RESOURCE_PRF)
proof_packet.send() proof_packet.send()
RNS.Transport.cache(proof_packet, force_cache=True) RNS.Transport.cache(proof_packet, force_cache=True)
@@ -776,8 +781,8 @@ class Resource:
advertise = False, advertise = False,
auto_compress = self.auto_compress_option, auto_compress = self.auto_compress_option,
sent_metadata_size = self.metadata_size) sent_metadata_size = self.metadata_size)
if self.__progress_callback:
self.next_segment.progress_callback(self.__progress_callback) if self.__progress_callback: self.next_segment.progress_callback(self.__progress_callback)
def validate_proof(self, proof_data): def validate_proof(self, proof_data):
if not self.status == Resource.FAILED: if not self.status == Resource.FAILED:
@@ -966,6 +971,7 @@ class Resource:
request_packet = RNS.Packet(self.link, request_data, context = RNS.Packet.RESOURCE_REQ) request_packet = RNS.Packet(self.link, request_data, context = RNS.Packet.RESOURCE_REQ)
try: try:
if not self.ensure_link(): return
request_packet.send() request_packet.send()
self.last_activity = time.time() self.last_activity = time.time()
self.req_sent = self.last_activity self.req_sent = self.last_activity
@@ -1010,11 +1016,11 @@ class Resource:
for part in requested_parts: for part in requested_parts:
try: try:
if not self.ensure_link(): return
if not part.sent: if not part.sent:
part.send() part.send()
self.sent_parts += 1 self.sent_parts += 1
else: else: part.resend()
part.resend()
self.last_activity = time.time() self.last_activity = time.time()
self.last_part_sent = self.last_activity self.last_part_sent = self.last_activity
@@ -1032,8 +1038,7 @@ class Resource:
search_end = self.receiver_min_consecutive_height+ResourceAdvertisement.COLLISION_GUARD_SIZE search_end = self.receiver_min_consecutive_height+ResourceAdvertisement.COLLISION_GUARD_SIZE
for part in self.parts[search_start:search_end]: for part in self.parts[search_start:search_end]:
part_index += 1 part_index += 1
if part.map_hash == last_map_hash: if part.map_hash == last_map_hash: break
break
self.receiver_min_consecutive_height = max(part_index-1-Resource.WINDOW_MAX, 0) self.receiver_min_consecutive_height = max(part_index-1-Resource.WINDOW_MAX, 0)
@@ -1041,10 +1046,8 @@ class Resource:
RNS.log("Resource sequencing error, cancelling transfer!", RNS.LOG_ERROR) RNS.log("Resource sequencing error, cancelling transfer!", RNS.LOG_ERROR)
self.cancel() self.cancel()
return return
else: else: segment = part_index // ResourceAdvertisement.HASHMAP_MAX_LEN
segment = part_index // ResourceAdvertisement.HASHMAP_MAX_LEN
hashmap_start = segment*ResourceAdvertisement.HASHMAP_MAX_LEN hashmap_start = segment*ResourceAdvertisement.HASHMAP_MAX_LEN
hashmap_end = min((segment+1)*ResourceAdvertisement.HASHMAP_MAX_LEN, len(self.parts)) hashmap_end = min((segment+1)*ResourceAdvertisement.HASHMAP_MAX_LEN, len(self.parts))
@@ -1052,10 +1055,16 @@ class Resource:
for i in range(hashmap_start,hashmap_end): for i in range(hashmap_start,hashmap_end):
hashmap += self.hashmap[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN] hashmap += self.hashmap[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN]
if not hashmap:
RNS.log("Resource HMU error, cancelling transfer!", RNS.LOG_ERROR)
self.cancel()
return
hmu = self.hash+umsgpack.packb([segment, hashmap]) hmu = self.hash+umsgpack.packb([segment, hashmap])
hmu_packet = RNS.Packet(self.link, hmu, context = RNS.Packet.RESOURCE_HMU) hmu_packet = RNS.Packet(self.link, hmu, context = RNS.Packet.RESOURCE_HMU)
try: try:
if not self.ensure_link(): return
hmu_packet.send() hmu_packet.send()
self.last_activity = time.time() self.last_activity = time.time()
except Exception as e: except Exception as e:
@@ -1090,10 +1099,14 @@ class Resource:
try: try:
cancel_packet = RNS.Packet(self.link, self.hash, context=RNS.Packet.RESOURCE_ICL) cancel_packet = RNS.Packet(self.link, self.hash, context=RNS.Packet.RESOURCE_ICL)
cancel_packet.send() cancel_packet.send()
except Exception as e: except Exception as e: RNS.log("Could not send resource cancel packet, the contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.log("Could not send resource cancel packet, the contained exception was: "+str(e), RNS.LOG_ERROR)
self.link.cancel_outgoing_resource(self) self.link.cancel_outgoing_resource(self)
else: else:
if self.link.status == RNS.Link.ACTIVE:
try:
cancel_packet = RNS.Packet(self.link, self.hash, context=RNS.Packet.RESOURCE_RCL)
cancel_packet.send()
except Exception as e: RNS.log("Could not send resource cancel packet, the contained exception was: "+str(e), RNS.LOG_ERROR)
self.link.cancel_incoming_resource(self) self.link.cancel_incoming_resource(self)
if self.callback != None: if self.callback != None:
@@ -1241,40 +1254,30 @@ class ResourceAdvertisement:
@staticmethod @staticmethod
def is_request(advertisement_packet): def is_request(advertisement_packet):
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext) adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
if adv.q != None and adv.u: if adv.q != None and adv.u: return True
return True else: return False
else:
return False
@staticmethod @staticmethod
def is_response(advertisement_packet): def is_response(advertisement_packet):
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext) adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
if adv.q != None and adv.p: return True
if adv.q != None and adv.p: else: return False
return True
else:
return False
@staticmethod @staticmethod
def read_request_id(advertisement_packet): def read_request_id(advertisement_packet):
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext) adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
return adv.q return adv.q
@staticmethod @staticmethod
def read_transfer_size(advertisement_packet): def read_transfer_size(advertisement_packet):
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext) adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
return adv.t return adv.t
@staticmethod @staticmethod
def read_size(advertisement_packet): def read_size(advertisement_packet):
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext) adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
return adv.d return adv.d
def __init__(self, resource=None, request_id=None, is_response=False): def __init__(self, resource=None, request_id=None, is_response=False):
self.link = None self.link = None
if resource != None: if resource != None:
@@ -1306,29 +1309,14 @@ class ResourceAdvertisement:
# Flags # Flags
self.f = 0x00 | self.x << 5 | self.p << 4 | self.u << 3 | self.s << 2 | self.c << 1 | self.e self.f = 0x00 | self.x << 5 | self.p << 4 | self.u << 3 | self.s << 2 | self.c << 1 | self.e
def get_transfer_size(self): def get_transfer_size(self): return self.t
return self.t def get_data_size(self): return self.d
def get_parts(self): return self.n
def get_data_size(self): def get_segments(self): return self.l
return self.d def get_hash(self): return self.h
def is_compressed(self): return self.c
def get_parts(self): def has_metadata(self): return self.x
return self.n def get_link(self): return self.link
def get_segments(self):
return self.l
def get_hash(self):
return self.h
def is_compressed(self):
return self.c
def has_metadata(self):
return self.x
def get_link(self):
return self.link
def pack(self, segment=0): def pack(self, segment=0):
hashmap_start = segment*ResourceAdvertisement.HASHMAP_MAX_LEN hashmap_start = segment*ResourceAdvertisement.HASHMAP_MAX_LEN
@@ -1338,23 +1326,20 @@ class ResourceAdvertisement:
for i in range(hashmap_start,hashmap_end): for i in range(hashmap_start,hashmap_end):
hashmap += self.m[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN] hashmap += self.m[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN]
dictionary = { dictionary = { "t": self.t, # Transfer size
"t": self.t, # Transfer size "d": self.d, # Data size
"d": self.d, # Data size "n": self.n, # Number of parts
"n": self.n, # Number of parts "h": self.h, # Resource hash
"h": self.h, # Resource hash "r": self.r, # Resource random hash
"r": self.r, # Resource random hash "o": self.o, # Original hash
"o": self.o, # Original hash "i": self.i, # Segment index
"i": self.i, # Segment index "l": self.l, # Total segments
"l": self.l, # Total segments "q": self.q, # Request ID
"q": self.q, # Request ID "f": self.f, # Resource flags
"f": self.f, # Resource flags "m": hashmap }
"m": hashmap
}
return umsgpack.packb(dictionary) return umsgpack.packb(dictionary)
@staticmethod @staticmethod
def unpack(data): def unpack(data):
dictionary = umsgpack.unpackb(data) dictionary = umsgpack.unpackb(data)
@@ -1378,4 +1363,6 @@ class ResourceAdvertisement:
adv.p = True if ((adv.f >> 4) & 0x01) == 0x01 else False adv.p = True if ((adv.f >> 4) & 0x01) == 0x01 else False
adv.x = True if ((adv.f >> 5) & 0x01) == 0x01 else False adv.x = True if ((adv.f >> 5) & 0x01) == 0x01 else False
if adv.t > Resource.MAX_EFFICIENT_SIZE*3: raise ValueError("Invalid transfer size")
return adv return adv
+12 -5
View File
@@ -829,6 +829,7 @@ class Reticulum:
latitude = None latitude = None
longitude = None longitude = None
height = None height = None
discovery_location = None
discovery_frequency = None discovery_frequency = None
discovery_bandwidth = None discovery_bandwidth = None
discovery_modulation = None discovery_modulation = None
@@ -846,6 +847,7 @@ class Reticulum:
if "discovery_encrypt" in c: discovery_encrypt = c.as_bool("discovery_encrypt") if "discovery_encrypt" in c: discovery_encrypt = c.as_bool("discovery_encrypt")
if "reachable_on" in c: reachable_on = c["reachable_on"] if "reachable_on" in c: reachable_on = c["reachable_on"]
if "publish_ifac" in c: publish_ifac = c.as_bool("publish_ifac") if "publish_ifac" in c: publish_ifac = c.as_bool("publish_ifac")
if "location_cmd" in c: discovery_location = c["location_cmd"]
if "latitude" in c: latitude = c.as_float("latitude") if "latitude" in c: latitude = c.as_float("latitude")
if "longitude" in c: longitude = c.as_float("longitude") if "longitude" in c: longitude = c.as_float("longitude")
if "height" in c: height = c.as_float("height") if "height" in c: height = c.as_float("height")
@@ -853,14 +855,14 @@ class Reticulum:
if "discovery_bandwidth" in c: discovery_bandwidth = c.as_int("discovery_bandwidth") if "discovery_bandwidth" in c: discovery_bandwidth = c.as_int("discovery_bandwidth")
if "discovery_modulation" in c: discovery_modulation = c.as_int("discovery_modulation") if "discovery_modulation" in c: discovery_modulation = c.as_int("discovery_modulation")
if not interface_mode in [Interface.Interface.MODE_GATEWAY, Interface.Interface.MODE_ACCESS_POINT]: if not interface_mode in [Interface.Interface.MODE_GATEWAY, Interface.Interface.MODE_ACCESS_POINT, Interface.Interface.MODE_INTERNAL]:
if not ignore_config_warnings: if not ignore_config_warnings:
if c["type"] in ["RNodeInterface", "RNodeMultiInterface"]: if c["type"] in ["RNodeInterface", "RNodeMultiInterface"]:
interface_mode = Interface.Interface.MODE_ACCESS_POINT interface_mode = Interface.Interface.MODE_ACCESS_POINT
RNS.log(f"Discovery enabled on interface {name} without gateway or AP mode. Auto-configured to AP mode.", RNS.LOG_NOTICE) RNS.log(f"Discovery enabled on interface {name} without gateway, internal or AP mode. Auto-configured to AP mode.", RNS.LOG_NOTICE)
else: else:
interface_mode = Interface.Interface.MODE_GATEWAY interface_mode = Interface.Interface.MODE_GATEWAY
RNS.log(f"Discovery enabled on interface {name} without gateway or AP mode. Auto-configured to gateway mode.", RNS.LOG_NOTICE) RNS.log(f"Discovery enabled on interface {name} without gateway, internal or AP mode. Auto-configured to gateway mode.", RNS.LOG_NOTICE)
try: try:
def interface_post_init(interface): def interface_post_init(interface):
@@ -884,6 +886,7 @@ class Reticulum:
interface.discovery_name = discovery_name interface.discovery_name = discovery_name
interface.discovery_encrypt = discovery_encrypt interface.discovery_encrypt = discovery_encrypt
interface.discovery_stamp_value = discovery_stamp_value interface.discovery_stamp_value = discovery_stamp_value
interface.discovery_location = discovery_location
interface.discovery_latitude = latitude interface.discovery_latitude = latitude
interface.discovery_longitude = longitude interface.discovery_longitude = longitude
interface.discovery_height = height interface.discovery_height = height
@@ -1412,6 +1415,9 @@ class Reticulum:
if interface.announce_queue != None: ifstats["announce_queue"] = len(interface.announce_queue) if interface.announce_queue != None: ifstats["announce_queue"] = len(interface.announce_queue)
else: ifstats["announce_queue"] = None else: ifstats["announce_queue"] = None
if hasattr(interface, "blocked_ip_count"):
ifstats["blocked_ips"] = interface.blocked_ip_count
ifstats["name"] = str(interface) ifstats["name"] = str(interface)
ifstats["short_name"] = str(interface.name) ifstats["short_name"] = str(interface.name)
ifstats["hash"] = interface.get_hash() ifstats["hash"] = interface.get_hash()
@@ -1892,7 +1898,7 @@ instance_name = default
[logging] [logging]
# Valid log levels are 0 through 7: # Valid log levels are 0 through 8:
# 0: Log only critical information # 0: Log only critical information
# 1: Log errors and lower log levels # 1: Log errors and lower log levels
# 2: Log warnings and lower log levels # 2: Log warnings and lower log levels
@@ -1900,7 +1906,8 @@ instance_name = default
# 4: Log info and lower (this is the default) # 4: Log info and lower (this is the default)
# 5: Verbose logging # 5: Verbose logging
# 6: Debug logging # 6: Debug logging
# 7: Extreme logging # 7: Path logging
# 8: Extreme logging
loglevel = 4 loglevel = 4
+51 -54
View File
@@ -217,7 +217,6 @@ class Transport:
@staticmethod @staticmethod
def start(reticulum_instance): def start(reticulum_instance):
Transport.owner = reticulum_instance Transport.owner = reticulum_instance
Transport.PR_LOGLEVEL = RNS.LOG_EXTREME
if Transport.identity == None: if Transport.identity == None:
transport_identity_path = RNS.Reticulum.storagepath+"/transport_identity" transport_identity_path = RNS.Reticulum.storagepath+"/transport_identity"
@@ -303,7 +302,6 @@ class Transport:
if RNS.Reticulum.transport_enabled(): if RNS.Reticulum.transport_enabled():
path_table_path = RNS.Reticulum.storagepath+"/destination_table" path_table_path = RNS.Reticulum.storagepath+"/destination_table"
tunnel_table_path = RNS.Reticulum.storagepath+"/tunnels" tunnel_table_path = RNS.Reticulum.storagepath+"/tunnels"
Transport.PR_LOGLEVEL = RNS.LOG_DEBUG
if os.path.isfile(path_table_path) and not Transport.owner.is_connected_to_shared_instance: if os.path.isfile(path_table_path) and not Transport.owner.is_connected_to_shared_instance:
serialised_destinations = [] serialised_destinations = []
@@ -339,15 +337,15 @@ class Transport:
announce_packet.hops += 1 announce_packet.hops += 1
with Transport.path_table_lock: with Transport.path_table_lock:
Transport.path_table[destination_hash] = [timestamp, received_from, hops, expires, random_blobs, receiving_interface, announce_packet.packet_hash] Transport.path_table[destination_hash] = [timestamp, received_from, hops, expires, random_blobs, receiving_interface, announce_packet.packet_hash]
RNS.log("Loaded path table entry for "+RNS.prettyhexrep(destination_hash)+" from storage", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Loaded path table entry for "+RNS.prettyhexrep(destination_hash)+" from storage", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
else: else:
RNS.log("Could not reconstruct path table entry from storage for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Could not reconstruct path table entry from storage for "+RNS.prettyhexrep(destination_hash), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
if announce_packet == None: if announce_packet == None:
RNS.log("The announce packet could not be loaded from cache", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("The announce packet could not be loaded from cache", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
if receiving_interface == None: if receiving_interface == None:
RNS.log("The interface is no longer available", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("The interface is no longer available", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
if blackholed: if blackholed:
RNS.log("The associated identity is blackholed", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("The associated identity is blackholed", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
if len(Transport.path_table) == 1: specifier = "entry" if len(Transport.path_table) == 1: specifier = "entry"
else: specifier = "entries" else: specifier = "entries"
@@ -508,8 +506,7 @@ class Transport:
Transport.speed_rx = rxs Transport.speed_rx = rxs
Transport.speed_tx = txs Transport.speed_tx = txs
except Exception as e: except Exception as e: RNS.log(f"An error occurred while counting interface traffic: {e}", RNS.LOG_ERROR)
RNS.log(f"An error occurred while counting interface traffic: {e}", RNS.LOG_ERROR)
@staticmethod @staticmethod
def jobloop(): def jobloop():
@@ -549,7 +546,7 @@ class Transport:
last_path_request = Transport.path_requests[link.destination.hash] last_path_request = Transport.path_requests[link.destination.hash]
if time.time() - last_path_request > Transport.PATH_REQUEST_MI: if time.time() - last_path_request > Transport.PATH_REQUEST_MI:
RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link.destination.hash)+" since an attempted link was never established", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link.destination.hash)+" since an attempted link was never established", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
if not link.destination.hash in path_requests: if not link.destination.hash in path_requests:
blocked_if = None blocked_if = None
path_requests[link.destination.hash] = blocked_if path_requests[link.destination.hash] = blocked_if
@@ -610,7 +607,7 @@ class Transport:
announce_data = packet.data announce_data = packet.data
announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=True) announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=True)
if not announce_identity: if not announce_identity:
RNS.log("Completed announce processing for "+RNS.prettyhexrep(destination_hash)+", the path was cleaned while waiting for announce rebroadcast", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Completed announce processing for "+RNS.prettyhexrep(destination_hash)+", the path was cleaned while waiting for announce rebroadcast", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
completed_announces.append(destination_hash) completed_announces.append(destination_hash)
else: else:
@@ -629,8 +626,8 @@ class Transport:
context_flag = packet.context_flag) context_flag = packet.context_flag)
new_packet.hops = announce_entry[4] new_packet.hops = announce_entry[4]
if block_rebroadcasts: RNS.log("Rebroadcasting announce as path response for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None if block_rebroadcasts: RNS.log("Rebroadcasting announce as path response for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
else: RNS.log("Rebroadcasting announce for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None else: RNS.log("Rebroadcasting announce for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
outgoing.append(new_packet) outgoing.append(new_packet)
@@ -643,7 +640,7 @@ class Transport:
if destination_hash in Transport.held_announces: if destination_hash in Transport.held_announces:
held_entry = Transport.held_announces.pop(destination_hash) held_entry = Transport.held_announces.pop(destination_hash)
Transport.announce_table[destination_hash] = held_entry Transport.announce_table[destination_hash] = held_entry
RNS.log("Reinserting held announce into table", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Reinserting held announce into table", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
for destination_hash in completed_announces: for destination_hash in completed_announces:
if destination_hash in Transport.announce_table: Transport.announce_table.pop(destination_hash) if destination_hash in Transport.announce_table: Transport.announce_table.pop(destination_hash)
@@ -719,14 +716,14 @@ class Transport:
# If the path has been invalidated between the time of # If the path has been invalidated between the time of
# making the link request and now, try to rediscover it # making the link request and now, try to rediscover it
if not Transport.has_path(link_entry[IDX_LT_DSTHASH]): if not Transport.has_path(link_entry[IDX_LT_DSTHASH]):
RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and path is now missing", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and path is now missing", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
path_request_conditions = True path_request_conditions = True
# If this link request was originated from a local client # If this link request was originated from a local client
# attempt to rediscover a path to the destination, if this # attempt to rediscover a path to the destination, if this
# has not already happened recently. # has not already happened recently.
elif not path_request_throttle and lr_taken_hops == 0: elif not path_request_throttle and lr_taken_hops == 0:
RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted local client link was never established", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted local client link was never established", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
path_request_conditions = True path_request_conditions = True
# If the link destination was previously only 1 hop # If the link destination was previously only 1 hop
@@ -735,7 +732,7 @@ class Transport:
# In that case, try to discover a new path, and mark # In that case, try to discover a new path, and mark
# the old one as unresponsive. # the old one as unresponsive.
elif not path_request_throttle and Transport.hops_to(link_entry[IDX_LT_DSTHASH]) == 1: elif not path_request_throttle and Transport.hops_to(link_entry[IDX_LT_DSTHASH]) == 1:
RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and destination was previously local to an interface on this instance", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and destination was previously local to an interface on this instance", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
path_request_conditions = True path_request_conditions = True
blocked_if = link_entry[IDX_LT_RCVD_IF] blocked_if = link_entry[IDX_LT_RCVD_IF]
@@ -757,7 +754,7 @@ class Transport:
# changed. In that case, we try to discover a new path, # changed. In that case, we try to discover a new path,
# and mark the old one as potentially unresponsive. # and mark the old one as potentially unresponsive.
elif not path_request_throttle and lr_taken_hops == 1: elif not path_request_throttle and lr_taken_hops == 1:
RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and link initiator is local to an interface on this instance", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Trying to rediscover path for "+RNS.prettyhexrep(link_entry[IDX_LT_DSTHASH])+" since an attempted link was never established, and link initiator is local to an interface on this instance", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
path_request_conditions = True path_request_conditions = True
blocked_if = link_entry[IDX_LT_RCVD_IF] blocked_if = link_entry[IDX_LT_RCVD_IF]
@@ -793,11 +790,11 @@ class Transport:
if time.time() > destination_expiry: if time.time() > destination_expiry:
stale_paths.append(destination_hash) stale_paths.append(destination_hash)
should_collect = True should_collect = True
RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
elif not attached_interface in Transport.interfaces: elif not attached_interface in Transport.interfaces:
stale_paths.append(destination_hash) stale_paths.append(destination_hash)
should_collect = True should_collect = True
RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" was removed since the attached interface no longer exists", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" was removed since the attached interface no longer exists", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
# Cull the pending path requests table # Cull the pending path requests table
stale_path_requests = [] stale_path_requests = []
@@ -1206,7 +1203,7 @@ class Transport:
if packet.packet_type == RNS.Packet.ANNOUNCE: if packet.packet_type == RNS.Packet.ANNOUNCE:
if packet.attached_interface == None: if packet.attached_interface == None:
ac_loglevel = RNS.LOG_EXTREME ac_loglevel = RNS.LOG_PATHING
from_interface = Transport.next_hop_interface(packet.destination_hash) from_interface = Transport.next_hop_interface(packet.destination_hash)
local_destination = None local_destination = None
with Transport.destinations_map_lock: with Transport.destinations_map_lock:
@@ -1644,11 +1641,11 @@ class Transport:
nh_mtu = outbound_interface.HW_MTU nh_mtu = outbound_interface.HW_MTU
if path_mtu: if path_mtu:
if outbound_interface.HW_MTU == None: if outbound_interface.HW_MTU == None:
RNS.log(f"No next-hop HW MTU, disabling link MTU upgrade", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log(f"No next-hop HW MTU, disabling link MTU upgrade", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
path_mtu = None path_mtu = None
new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE] new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]
elif not outbound_interface.AUTOCONFIGURE_MTU and not outbound_interface.FIXED_MTU: elif not outbound_interface.AUTOCONFIGURE_MTU and not outbound_interface.FIXED_MTU:
RNS.log(f"Outbound interface doesn't support MTU autoconfiguration, disabling link MTU upgrade", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log(f"Outbound interface doesn't support MTU autoconfiguration, disabling link MTU upgrade", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
path_mtu = None path_mtu = None
new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE] new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]
else: else:
@@ -1656,7 +1653,7 @@ class Transport:
try: try:
path_mtu = min(nh_mtu, ph_mtu) path_mtu = min(nh_mtu, ph_mtu)
clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode) clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode)
RNS.log(f"Clamping link MTU to {RNS.prettysize(path_mtu)}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log(f"Clamping link MTU to {RNS.prettysize(path_mtu)}", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu
except Exception as e: except Exception as e:
RNS.log(f"Dropping link request packet. The contained exception was: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log(f"Dropping link request packet. The contained exception was: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
@@ -1849,7 +1846,7 @@ class Transport:
if not random_blob in random_blobs: if not random_blob in random_blobs:
# TODO: Check that this ^ approach actually # TODO: Check that this ^ approach actually
# works under all circumstances # works under all circumstances
RNS.log("Replacing destination table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce due to expired path", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Replacing path table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce due to expired path", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
Transport.mark_path_unknown_state(packet.destination_hash) Transport.mark_path_unknown_state(packet.destination_hash)
should_add = True should_add = True
else: else:
@@ -1860,7 +1857,7 @@ class Transport:
# this announce before, update the path table. # this announce before, update the path table.
if (announce_emitted > path_announce_emitted): if (announce_emitted > path_announce_emitted):
if not random_blob in random_blobs: if not random_blob in random_blobs:
RNS.log("Replacing destination table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce, since it was more recently emitted", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Replacing path table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce, since it was more recently emitted", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
Transport.mark_path_unknown_state(packet.destination_hash) Transport.mark_path_unknown_state(packet.destination_hash)
should_add = True should_add = True
else: else:
@@ -1872,7 +1869,7 @@ class Transport:
# allow updating the path table to this one. # allow updating the path table to this one.
elif announce_emitted == path_announce_emitted: elif announce_emitted == path_announce_emitted:
if Transport.path_is_unresponsive(packet.destination_hash): if Transport.path_is_unresponsive(packet.destination_hash):
RNS.log("Replacing destination table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce, since previously tried path was unresponsive", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Replacing path table entry for "+str(RNS.prettyhexrep(packet.destination_hash))+" with new announce, since previously tried path was unresponsive", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
should_add = True should_add = True
else: else:
should_add = False should_add = False
@@ -1938,7 +1935,7 @@ class Transport:
if (RNS.Reticulum.transport_enabled() or is_from_local_client) and packet.context != RNS.Packet.PATH_RESPONSE: if (RNS.Reticulum.transport_enabled() or is_from_local_client) and packet.context != RNS.Packet.PATH_RESPONSE:
# Insert announce into announce table for retransmission # Insert announce into announce table for retransmission
if rate_blocked: RNS.log("Blocking rebroadcast of announce from "+RNS.prettyhexrep(packet.destination_hash)+" due to excessive announce rate", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None if rate_blocked: RNS.log("Blocking rebroadcast of announce from "+RNS.prettyhexrep(packet.destination_hash)+" due to excessive announce rate", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
else: else:
if is_from_local_client: if is_from_local_client:
# If the announce is from a local client, # If the announce is from a local client,
@@ -2024,7 +2021,7 @@ class Transport:
interface_str = " on "+str(attached_interface) interface_str = " on "+str(attached_interface)
RNS.log("Got matching announce, answering waiting discovery path request for "+RNS.prettyhexrep(packet.destination_hash)+interface_str, Transport.PR_LOGLEVEL) if RNS.sl(Transport.PR_LOGLEVEL) else None RNS.log("Got matching announce, answering waiting discovery path request for "+RNS.prettyhexrep(packet.destination_hash)+interface_str, RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=False) announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=False)
announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown"); announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown");
announce_destination.hash = packet.destination_hash announce_destination.hash = packet.destination_hash
@@ -2044,7 +2041,7 @@ class Transport:
path_table_entry = [now, received_from, announce_hops, expires, random_blobs, packet.receiving_interface, packet.packet_hash] path_table_entry = [now, received_from, announce_hops, expires, random_blobs, packet.receiving_interface, packet.packet_hash]
with Transport.path_table_lock: Transport.path_table[packet.destination_hash] = path_table_entry with Transport.path_table_lock: Transport.path_table[packet.destination_hash] = path_table_entry
Transport.mark_path_unknown_state(packet.destination_hash) Transport.mark_path_unknown_state(packet.destination_hash)
RNS.log("Destination "+RNS.prettyhexrep(packet.destination_hash)+" is now "+str(announce_hops)+" hops away via "+RNS.prettyhexrep(received_from)+" on "+str(packet.receiving_interface), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Destination "+RNS.prettyhexrep(packet.destination_hash)+" is now "+str(announce_hops)+" hops away via "+RNS.prettyhexrep(received_from)+" on "+str(packet.receiving_interface), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
if packet.destination_hash in Transport.path_requests: if packet.destination_hash in Transport.path_requests:
RNS.Reticulum.get_instance()._used_destination_data(packet.destination_hash) RNS.Reticulum.get_instance()._used_destination_data(packet.destination_hash)
@@ -2059,7 +2056,7 @@ class Transport:
paths[packet.destination_hash] = [now, received_from, announce_hops, expires, random_blobs, None, packet.packet_hash] paths[packet.destination_hash] = [now, received_from, announce_hops, expires, random_blobs, None, packet.packet_hash]
expires = time.time() + Transport.TUNNEL_TIMEOUT expires = time.time() + Transport.TUNNEL_TIMEOUT
tunnel_entry[IDX_TT_EXPIRES] = expires tunnel_entry[IDX_TT_EXPIRES] = expires
RNS.log("Path to "+RNS.prettyhexrep(packet.destination_hash)+" associated with tunnel "+RNS.prettyhexrep(packet.receiving_interface.tunnel_id), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Path to "+RNS.prettyhexrep(packet.destination_hash)+" associated with tunnel "+RNS.prettyhexrep(packet.receiving_interface.tunnel_id), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
# Call externally registered callbacks from apps # Call externally registered callbacks from apps
# wanting to know when an announce arrives # wanting to know when an announce arrives
@@ -2229,7 +2226,7 @@ class Transport:
else: RNS.log("Invalid link request proof in transport for link "+RNS.prettyhexrep(packet.destination_hash)+", dropping proof.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None else: RNS.log("Invalid link request proof in transport for link "+RNS.prettyhexrep(packet.destination_hash)+", dropping proof.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
except Exception as e: RNS.log("Could not transport link request proof. The contained exception was: "+str(e), RNS.LOG_DEBUG) if RNS.sl(LOG_DEBUG) else None except Exception as e: RNS.log("Could not transport link request proof. The contained exception was: "+str(e), RNS.LOG_DEBUG) if RNS.sl(LOG_DEBUG) else None
else: RNS.log("Link request proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None else: RNS.log("Link request proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
else: RNS.log("Received link request proof with hop mismatch, not transporting it", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None else: RNS.log(f"Received link request proof with hop mismatch ({packet.hops}/{link_entry[IDX_LT_NH_IF]}), not transporting it", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
else: else:
# Check if we can deliver it to a local # Check if we can deliver it to a local
@@ -2365,14 +2362,14 @@ class Transport:
def handle_tunnel(tunnel_id, interface): def handle_tunnel(tunnel_id, interface):
expires = time.time() + Transport.TUNNEL_TIMEOUT expires = time.time() + Transport.TUNNEL_TIMEOUT
if not tunnel_id in Transport.tunnels: if not tunnel_id in Transport.tunnels:
RNS.log("Tunnel endpoint "+RNS.prettyhexrep(tunnel_id)+" established.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Tunnel endpoint "+RNS.prettyhexrep(tunnel_id)+" established.", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
paths = {} paths = {}
with Transport.tunnels_lock: with Transport.tunnels_lock:
tunnel_entry = [tunnel_id, interface, paths, expires] tunnel_entry = [tunnel_id, interface, paths, expires]
interface.tunnel_id = tunnel_id interface.tunnel_id = tunnel_id
Transport.tunnels[tunnel_id] = tunnel_entry Transport.tunnels[tunnel_id] = tunnel_entry
else: else:
RNS.log("Tunnel endpoint "+RNS.prettyhexrep(tunnel_id)+" reappeared. Restoring paths...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Tunnel endpoint "+RNS.prettyhexrep(tunnel_id)+" reappeared. Restoring paths...", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
tunnel_entry = Transport.tunnels[tunnel_id] tunnel_entry = Transport.tunnels[tunnel_id]
tunnel_entry[IDX_TT_IF] = interface tunnel_entry[IDX_TT_IF] = interface
tunnel_entry[IDX_TT_EXPIRES] = expires tunnel_entry[IDX_TT_EXPIRES] = expires
@@ -2401,21 +2398,21 @@ class Transport:
current_path_timebase = Transport.timebase_from_random_blobs(current_random_blobs) current_path_timebase = Transport.timebase_from_random_blobs(current_random_blobs)
tunnel_announce_timebase = Transport.timebase_from_random_blobs(random_blobs) tunnel_announce_timebase = Transport.timebase_from_random_blobs(random_blobs)
if tunnel_announce_timebase >= current_path_timebase: should_add = True if tunnel_announce_timebase >= current_path_timebase: should_add = True
else: RNS.log("Did not restore path to "+RNS.prettyhexrep(destination_hash)+" because existing path is more recent", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None else: RNS.log("Did not restore path to "+RNS.prettyhexrep(destination_hash)+" because existing path is more recent", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
else: RNS.log("Did not restore path to "+RNS.prettyhexrep(destination_hash)+" because a newer path with fewer hops exist", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None else: RNS.log("Did not restore path to "+RNS.prettyhexrep(destination_hash)+" because a newer path with fewer hops exist", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
else: else:
if time.time() < expires: should_add = True if time.time() < expires: should_add = True
else: RNS.log("Did not restore path to "+RNS.prettyhexrep(destination_hash)+" because it has expired", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None else: RNS.log("Did not restore path to "+RNS.prettyhexrep(destination_hash)+" because it has expired", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
if should_add: if should_add:
with Transport.path_table_lock: Transport.path_table[destination_hash] = new_entry with Transport.path_table_lock: Transport.path_table[destination_hash] = new_entry
RNS.log("Restored path to "+RNS.prettyhexrep(destination_hash)+" is now "+str(announce_hops)+" hops away via "+RNS.prettyhexrep(received_from)+" on "+str(receiving_interface), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Restored path to "+RNS.prettyhexrep(destination_hash)+" is now "+str(announce_hops)+" hops away via "+RNS.prettyhexrep(received_from)+" on "+str(receiving_interface), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
else: deprecated_paths.append(destination_hash) else: deprecated_paths.append(destination_hash)
for deprecated_path in deprecated_paths: for deprecated_path in deprecated_paths:
RNS.log("Removing path to "+RNS.prettyhexrep(deprecated_path)+" from tunnel "+RNS.prettyhexrep(tunnel_id), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Removing path to "+RNS.prettyhexrep(deprecated_path)+" from tunnel "+RNS.prettyhexrep(tunnel_id), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
with Transport.tunnels_lock: paths.pop(deprecated_path) with Transport.tunnels_lock: paths.pop(deprecated_path)
@staticmethod @staticmethod
@@ -2932,7 +2929,7 @@ class Transport:
tag=tag_bytes) tag=tag_bytes)
else: RNS.log("Ignoring duplicate path request for "+RNS.prettyhexrep(destination_hash)+" with tag "+RNS.prettyhexrep(unique_tag), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None else: RNS.log("Ignoring duplicate path request for "+RNS.prettyhexrep(destination_hash)+" with tag "+RNS.prettyhexrep(unique_tag), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
else: RNS.log("Ignoring tagless path request for "+RNS.prettyhexrep(destination_hash), Transport.PR_LOGLEVEL) if RNS.sl(Transport.PR_LOGLEVEL) else None else: RNS.log("Ignoring tagless path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
except Exception as e: RNS.log(f"Error while handling path request. The contained exception was: {e}", RNS.LOG_ERROR) except Exception as e: RNS.log(f"Error while handling path request. The contained exception was: {e}", RNS.LOG_ERROR)
@staticmethod @staticmethod
@@ -2946,9 +2943,9 @@ class Transport:
if attached_interface.recursive_prs: should_search_for_unknown = True if attached_interface.recursive_prs: should_search_for_unknown = True
elif attached_interface.mode in RNS.Interfaces.Interface.Interface.DISCOVER_PATHS_FOR: should_search_for_unknown = True elif attached_interface.mode in RNS.Interfaces.Interface.Interface.DISCOVER_PATHS_FOR: should_search_for_unknown = True
if RNS.sl(RNS.LOG_DEBUG): if RNS.sl(RNS.LOG_PATHING):
interface_str = f" on {attached_interface}" interface_str = f" on {attached_interface}"
RNS.log(f"Path request for {RNS.prettyhexrep(destination_hash)}{interface_str}", Transport.PR_LOGLEVEL) RNS.log(f"Path request for {RNS.prettyhexrep(destination_hash)}{interface_str}", RNS.LOG_PATHING)
destination_exists_on_local_client = False destination_exists_on_local_client = False
if len(Transport.local_client_interfaces) > 0: if len(Transport.local_client_interfaces) > 0:
@@ -2967,7 +2964,7 @@ class Transport:
if local_destination != None: if local_destination != None:
local_destination.announce(path_response=True, tag=tag, attached_interface=attached_interface) local_destination.announce(path_response=True, tag=tag, attached_interface=attached_interface)
RNS.log("Answering path request for "+RNS.prettyhexrep(destination_hash)+interface_str+", destination is local to this system", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Answering path request for "+RNS.prettyhexrep(destination_hash)+interface_str+", destination is local to this system", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
elif (RNS.Reticulum.transport_enabled() or is_from_local_client) and (destination_hash in Transport.path_table): elif (RNS.Reticulum.transport_enabled() or is_from_local_client) and (destination_hash in Transport.path_table):
packet = Transport.get_cached_packet(Transport.path_table[destination_hash][IDX_PT_PACKET], packet_type="announce") packet = Transport.get_cached_packet(Transport.path_table[destination_hash][IDX_PT_PACKET], packet_type="announce")
@@ -2978,7 +2975,7 @@ class Transport:
RNS.log(f"Could not retrieve announce packet from cache while answering path request for {RNS.prettyhexrep(destination_hash)}, ignoring path request", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log(f"Could not retrieve announce packet from cache while answering path request for {RNS.prettyhexrep(destination_hash)}, ignoring path request", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
elif attached_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ROAMING and attached_interface == received_from: elif attached_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ROAMING and attached_interface == received_from:
RNS.log("Not answering path request on roaming-mode interface, since next hop is on same roaming-mode interface", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Not answering path request on roaming-mode interface, since next hop is on same roaming-mode interface", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
else: else:
if not packet.unpack(): return if not packet.unpack(): return
@@ -2991,9 +2988,9 @@ class Transport:
# inefficient. There is probably a better way. Doing # inefficient. There is probably a better way. Doing
# path invalidation here would decrease the network # path invalidation here would decrease the network
# convergence time. Maybe just drop it? # convergence time. Maybe just drop it?
RNS.log("Not answering path request for "+RNS.prettyhexrep(destination_hash)+interface_str+", since next hop is the requestor", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Not answering path request for "+RNS.prettyhexrep(destination_hash)+interface_str+", since next hop is the requestor", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
else: else:
RNS.log("Answering path request for "+RNS.prettyhexrep(destination_hash)+interface_str+", path is known", Transport.PR_LOGLEVEL) if RNS.sl(Transport.PR_LOGLEVEL) else None RNS.log("Answering path request for "+RNS.prettyhexrep(destination_hash)+interface_str+", path is known", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
now = time.time() now = time.time()
retries = Transport.PATHFINDER_R retries = Transport.PATHFINDER_R
@@ -3035,7 +3032,7 @@ class Transport:
elif is_from_local_client: elif is_from_local_client:
# Forward path request on all interfaces # Forward path request on all interfaces
# except the local client # except the local client
RNS.log("Forwarding path request from local client for "+RNS.prettyhexrep(destination_hash)+interface_str+" to all other interfaces", Transport.PR_LOGLEVEL) if RNS.sl(Transport.PR_LOGLEVEL) else None RNS.log("Forwarding path request from local client for "+RNS.prettyhexrep(destination_hash)+interface_str+" to all other interfaces", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
request_tag = RNS.Identity.get_random_hash() request_tag = RNS.Identity.get_random_hash()
for interface in Transport.interfaces: for interface in Transport.interfaces:
if not interface == attached_interface: if not interface == attached_interface:
@@ -3043,20 +3040,20 @@ class Transport:
elif should_search_for_unknown: elif should_search_for_unknown:
if destination_hash in Transport.discovery_path_requests: if destination_hash in Transport.discovery_path_requests:
RNS.log("There is already a waiting path request for "+RNS.prettyhexrep(destination_hash)+" on behalf of path request"+interface_str, RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("There is already a waiting path request for "+RNS.prettyhexrep(destination_hash)+" on behalf of path request"+interface_str, RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
else: else:
# Abort recursive path request if receiving # Abort recursive path request if receiving
# interface has PR burst active, or should # interface has PR burst active, or should
# otherwise ingress limit path requests. # otherwise ingress limit path requests.
if should_ingress_limit: if should_ingress_limit:
if RNS.sl(RNS.LOG_DEBUG): if RNS.sl(RNS.LOG_PATHING):
interface_str = f" for {attached_interface}" if attached_interface else "" interface_str = f" for {attached_interface}" if attached_interface else ""
RNS.log(f"Not sending recursive path request{interface_str} due to active ingress limiting", RNS.LOG_DEBUG) RNS.log(f"Not sending recursive path request{interface_str} due to active ingress limiting", RNS.LOG_PATHING)
return return
# Forward path request on all interfaces # Forward path request on all interfaces
# except the requestor interface # except the requestor interface
RNS.log("Attempting to discover unknown path to "+RNS.prettyhexrep(destination_hash)+" on behalf of path request"+interface_str, RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log("Attempting to discover unknown path to "+RNS.prettyhexrep(destination_hash)+" on behalf of path request"+interface_str, RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
pr_entry = { "destination_hash": destination_hash, "timeout": time.time()+Transport.PATH_REQUEST_TIMEOUT, "requesting_interface": attached_interface } pr_entry = { "destination_hash": destination_hash, "timeout": time.time()+Transport.PATH_REQUEST_TIMEOUT, "requesting_interface": attached_interface }
with Transport.discovery_pr_lock: Transport.discovery_path_requests[destination_hash] = pr_entry with Transport.discovery_pr_lock: Transport.discovery_path_requests[destination_hash] = pr_entry
@@ -3072,12 +3069,12 @@ class Transport:
elif not is_from_local_client and len(Transport.local_client_interfaces) > 0: elif not is_from_local_client and len(Transport.local_client_interfaces) > 0:
# Forward the path request on all local # Forward the path request on all local
# client interfaces # client interfaces
RNS.log("Forwarding path request for "+RNS.prettyhexrep(destination_hash)+interface_str+" to local clients", Transport.PR_LOGLEVEL) if RNS.sl(Transport.PR_LOGLEVEL) else None RNS.log("Forwarding path request for "+RNS.prettyhexrep(destination_hash)+interface_str+" to local clients", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
for interface in Transport.local_client_interfaces: for interface in Transport.local_client_interfaces:
Transport.request_path(destination_hash, on_interface=interface) Transport.request_path(destination_hash, on_interface=interface)
else: else:
RNS.log("Ignoring path request for "+RNS.prettyhexrep(destination_hash)+interface_str+", no path known", Transport.PR_LOGLEVEL) if RNS.sl(Transport.PR_LOGLEVEL) else None RNS.log("Ignoring path request for "+RNS.prettyhexrep(destination_hash)+interface_str+", no path known", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
@staticmethod @staticmethod
def from_local_client(packet): def from_local_client(packet):
@@ -3291,7 +3288,7 @@ class Transport:
if interface != None: if interface != None:
# Get the destination entry from the destination table # Get the destination entry from the destination table
if not destination_hash in path_table: if not destination_hash in path_table:
RNS.log(f"Skipping persist for path table entry {RNS.prettyhexrep(destination_hash)}, no longer in table", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None RNS.log(f"Skipping persist for path table entry {RNS.prettyhexrep(destination_hash)}, no longer in table", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
de = path_table[destination_hash] de = path_table[destination_hash]
timestamp = de[IDX_PT_TIMESTAMP] timestamp = de[IDX_PT_TIMESTAMP]
+3 -2
View File
@@ -265,7 +265,7 @@ instance_name = default
[logging] [logging]
# Valid log levels are 0 through 7: # Valid log levels are 0 through 8:
# 0: Log only critical information # 0: Log only critical information
# 1: Log errors and lower log levels # 1: Log errors and lower log levels
# 2: Log warnings and lower log levels # 2: Log warnings and lower log levels
@@ -273,7 +273,8 @@ instance_name = default
# 4: Log info and lower (this is the default) # 4: Log info and lower (this is the default)
# 5: Verbose logging # 5: Verbose logging
# 6: Debug logging # 6: Debug logging
# 7: Extreme logging # 7: Path logging
# 8: Extreme logging
loglevel = 4 loglevel = 4
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.2.0" __version__ = "0.3.0"
+2 -1
View File
@@ -44,7 +44,8 @@ def setup_argument_parser():
parser = argparse.ArgumentParser(description="Reticulum Remote Shell Utility", epilog="When specifying a command to execute, separate rnsh\noptions from the command and its arguments with --\n\nFor example:\n rnsh -l -- /bin/bash --login\n rnsh <destination> -- ls -la /tmp", formatter_class=argparse.RawDescriptionHelpFormatter) parser = argparse.ArgumentParser(description="Reticulum Remote Shell Utility", epilog="When specifying a command to execute, separate rnsh\noptions from the command and its arguments with --\n\nFor example:\n rnsh -l -- /bin/bash --login\n rnsh <destination> -- ls -la /tmp", formatter_class=argparse.RawDescriptionHelpFormatter)
# Common options # Common options
parser.add_argument("--config", "-c", action="store", default=None, help="path to alternative Reticulum config directory", type=str) parser.add_argument("--config", "-c", action="store", default=None, help="path to config directory", type=str)
parser.add_argument("--rnsconfig", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
parser.add_argument("--identity", "-i", action="store", default=None, help="path to identity file to use", type=str) parser.add_argument("--identity", "-i", action="store", default=None, help="path to identity file to use", type=str)
parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity") parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity")
parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity") parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity")
-2
View File
@@ -53,8 +53,6 @@ class permit(AbstractContextManager):
""" """
def __init__(self, *exceptions): self._exceptions = exceptions def __init__(self, *exceptions): self._exceptions = exceptions
def __enter__(self): pass def __enter__(self): pass
def __exit__(self, exctype, excinst, exctb): def __exit__(self, exctype, excinst, exctb):
return exctype is not None and not issubclass(exctype, self._exceptions) return exctype is not None and not issubclass(exctype, self._exceptions)
-1
View File
@@ -55,5 +55,4 @@ class SleepRate:
return sleep_for if sleep_for > 0 else 0 return sleep_for if sleep_for > 0 else 0
async def sleep_async(self): await asyncio.sleep(self.next_sleep_time()) async def sleep_async(self): await asyncio.sleep(self.next_sleep_time())
def sleep_block(self): time.sleep(self.next_sleep_time()) def sleep_block(self): time.sleep(self.next_sleep_time())
+7 -14
View File
@@ -144,7 +144,7 @@ class RemoteExecutionError(Exception):
def __init__(self, msg): self.msg = msg def __init__(self, msg): self.msg = msg
async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0, quietness=0, noid=False, destination=None, async def _initiate_link(configdir, rnsconfigdir, identitypath=None, logfile=None, verbosity=0, quietness=0, noid=False, destination=None,
timeout=RNS.Transport.PATH_REQUEST_TIMEOUT): timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
global _identity, _reticulum, _link, _destination, _remote_exec_grace global _identity, _reticulum, _link, _destination, _remote_exec_grace
@@ -160,11 +160,11 @@ async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0
if _reticulum is None: if _reticulum is None:
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_ERROR) targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_ERROR)
RNS.logfile = os.path.join(configdir, "logfile") RNS.logfile = logfile
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel, logdest=RNS.LOG_FILE) _reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel, logdest=RNS.LOG_FILE)
if _identity is None: if _identity is None:
_identity = rnsh.prepare_identity(identitypath) _identity = rnsh.prepare_identity(identity_path=identitypath, service_name=None, configdir=configdir)
if not RNS.Transport.has_path(destination_hash): if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash) RNS.Transport.request_path(destination_hash)
@@ -211,8 +211,9 @@ async def _handle_error(errmsg: RNS.MessageBase):
raise RemoteExecutionError(f"Remote error: {errmsg.msg}") raise RemoteExecutionError(f"Remote error: {errmsg.msg}")
async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosity: int, quietness: int, noid: bool, destination: str, async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, logfile:str, verbosity: int, quietness: int, noid: bool, destination: str,
timeout: float, command: [str] | None = None): timeout: float, command: [str] | None = None):
global _finished, _link global _finished, _link
if timeout is None: if timeout is None:
timeout = RNS.Transport.PATH_REQUEST_TIMEOUT timeout = RNS.Transport.PATH_REQUEST_TIMEOUT
@@ -239,12 +240,6 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
channel = _link.get_channel() channel = _link.get_channel()
protocol.register_message_types(channel) protocol.register_message_types(channel)
channel.add_message_handler(_client_message_handler) channel.add_message_handler(_client_message_handler)
# Next step after linking and identifying: send version
# if not await _spin(lambda: messenger.is_outlet_ready(outlet), timeout=5, quiet=quietness > 0):
# print("Error bringing up link")
# return 253
channel.send(protocol.VersionInfoMessage()) channel.send(protocol.VersionInfoMessage())
try: try:
vm = _pq.get(timeout=max(outlet.rtt * 20, 5)) vm = _pq.get(timeout=max(outlet.rtt * 20, 5))
@@ -283,10 +278,8 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
return None return None
elif b == "L": elif b == "L":
line_mode = not line_mode line_mode = not line_mode
if line_mode: if line_mode: os.write(1, "\n\rLine-interactive mode enabled\n\r".encode("utf-8"))
os.write(1, "\n\rLine-interactive mode enabled\n\r".encode("utf-8")) else: os.write(1, "\n\rLine-interactive mode disabled\n\r".encode("utf-8"))
else:
os.write(1, "\n\rLine-interactive mode disabled\n\r".encode("utf-8"))
return None return None
return b return b
+12 -5
View File
@@ -118,25 +118,32 @@ def compute_target_rns_loglevel(verbosity: int, quietness: int, base_level: int
except Exception: return base_level except Exception: return base_level
async def listen(configdir, rnsconfigdir, command, identitypath=None, service_name=None, verbosity=0, quietness=0, allowed=None, async def listen(configdir, rnsconfigdir, command, identitypath=None, logfile=None, service_name=None, verbosity=0, quietness=0,
allowed_file=None, disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False, allowed=None, allowed_file=None, disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False,
loop: asyncio.AbstractEventLoop = None): loop: asyncio.AbstractEventLoop = None):
global _identity, _allow_all, _allowed_identity_hashes, _allowed_file, _allowed_file_identity_hashes global _identity, _allow_all, _allowed_identity_hashes, _allowed_file, _allowed_file_identity_hashes
global _reticulum, _cmd, _destination, _no_remote_command, _remote_cmd_as_args, _finished global _reticulum, _cmd, _destination, _no_remote_command, _remote_cmd_as_args, _finished
if not loop: loop = asyncio.get_running_loop() if not loop: loop = asyncio.get_running_loop()
if service_name is None or len(service_name) == 0: if service_name is None or len(service_name) == 0: service_name = "default"
service_name = "default"
RNS.log(f"Using service name {service_name}", RNS.LOG_INFO) RNS.log(f"Using service name {service_name}", RNS.LOG_INFO)
if logfile:
RNS.log(f"Logging to {logfile}", RNS.LOG_NOTICE)
logdest = RNS.LOG_FILE
RNS.logfile = logfile
else:
RNS.log(f"Logging to console", RNS.LOG_NOTICE)
logdest = RNS.LOG_STDOUT
# More -v should increase verbosity (higher RNS.loglevel); -q should decrease it # More -v should increase verbosity (higher RNS.loglevel); -q should decrease it
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_INFO) targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_INFO)
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel) _reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel)
_identity = rnsh.prepare_identity(identitypath, service_name) _identity = rnsh.prepare_identity(identity_path=identitypath, service_name=service_name, configdir=configdir)
_destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.APP_NAME) _destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.APP_NAME)
RNS.log(f"rnsh listening for commands on {RNS.prettyhexrep(_destination.hash)}", RNS.LOG_NOTICE) RNS.log(f"rnsh listening for commands on {RNS.prettyhexrep(_destination.hash)}", RNS.LOG_NOTICE)
RNS.logdest = logdest
_cmd = command _cmd = command
if _cmd is None or len(_cmd) == 0: if _cmd is None or len(_cmd) == 0:
+4 -10
View File
@@ -88,8 +88,7 @@ class RetryThread(AbstractContextManager):
self._thread = threading.Thread(name=name, target=self._thread_run, daemon=True) self._thread = threading.Thread(name=name, target=self._thread_run, daemon=True)
self._thread.start() self._thread.start()
def is_alive(self): def is_alive(self): return self._thread.is_alive()
return self._thread.is_alive()
def close(self, loop: asyncio.AbstractEventLoop = None) -> asyncio.Future: def close(self, loop: asyncio.AbstractEventLoop = None) -> asyncio.Future:
RNS.log("Stopping timer thread", RNS.LOG_DEBUG) RNS.log("Stopping timer thread", RNS.LOG_DEBUG)
@@ -102,17 +101,12 @@ class RetryThread(AbstractContextManager):
return self._finished return self._finished
def wait(self, timeout: float = None): def wait(self, timeout: float = None):
if timeout: if timeout: timeout = timeout + time.time()
timeout = timeout + time.time()
while timeout is None or time.time() < timeout: while timeout is None or time.time() < timeout:
with self._lock: with self._lock: task_count = len(self._statuses)
task_count = len(self._statuses) if task_count == 0: return
if task_count == 0:
return
time.sleep(0.1) time.sleep(0.1)
def _thread_run(self): def _thread_run(self):
while self._run and self._finished is None: while self._run and self._finished is None:
time.sleep(self._loop_period) time.sleep(self._loop_period)
+40 -25
View File
@@ -57,43 +57,54 @@ loop: asyncio.AbstractEventLoop | None = None
def _sanitize_service_name(service_name:str) -> str: return re.sub(r'\W+', '', service_name) def _sanitize_service_name(service_name:str) -> str: return re.sub(r'\W+', '', service_name)
def prepare_identity(identity_path, service_name: str = None) -> tuple[RNS.Identity]: def prepare_identity(identity_path=None, service_name=None, configdir=None):
service_name = _sanitize_service_name(service_name or "") service_name = _sanitize_service_name(service_name or "")
if identity_path is None: if not identity_path:
identity_path = RNS.Reticulum.identitypath + "/" + APP_NAME + \ identity_path = f"{configdir}/identity" + (f".{service_name}" if service_name and len(service_name) > 0 else "")
(f".{service_name}" if service_name and len(service_name) > 0 else "")
identity = None identity = None
if os.path.isfile(identity_path): if os.path.isfile(identity_path):
RNS.log(f"Loading identity from {identity_path}", RNS.LOG_VERBOSE)
identity = RNS.Identity.from_file(identity_path) identity = RNS.Identity.from_file(identity_path)
if identity is None: if identity is None:
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO) RNS.log(f"Creating identity {identity_path}", RNS.LOG_NOTICE)
identity = RNS.Identity() identity = RNS.Identity()
identity.to_file(identity_path) identity.to_file(identity_path)
return identity return identity
def print_identity(configdir, identitypath, service_name, include_destination: bool): def print_identity(configdir, rnsconfigdir, identitypath, service_name, include_destination: bool):
reticulum = RNS.Reticulum(configdir=configdir, loglevel=RNS.LOG_INFO) reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=RNS.LOG_INFO)
if service_name and len(service_name) > 0: if service_name and len(service_name) > 0: print(f"Using service name \"{service_name}\"")
print(f"Using service name \"{service_name}\"") identity = prepare_identity(identity_path=identitypath, service_name=service_name, configdir=configdir)
identity = prepare_identity(identitypath, service_name)
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME) destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME)
print("Identity : " + str(identity)) print("Identity : " + str(identity))
if include_destination: if include_destination: print("Listening on : " + RNS.prettyhexrep(destination.hash))
print("Listening on : " + RNS.prettyhexrep(destination.hash))
exit(0) exit(0)
verbose_set = False verbose_set = False
def ensure_config_directory(): def ensure_config_directory(configdir=None):
if os.path.isdir(os.path.expanduser("~/.config/rnsh")): return os.path.expanduser("~/.config/rnsh") if configdir:
elif os.path.isdir(os.path.expanduser("~/.rnsh")): return os.path.expanduser("~/.rnsh") if os.path.isdir(os.path.expanduser(configdir)): return os.path.expanduser(configdir)
else:
try:
RNS.log(f"Creating configuration directory: {os.path.expanduser(configdir)}")
os.makedirs(os.path.expanduser(configdir))
return os.path.expanduser(configdir)
except Exception as e:
RNS.log(f"Could not get or create rnsh configuration directory, aborting", RNS.LOG_CRITICAL)
os._exit(1)
elif os.path.isdir(os.path.expanduser("~/.config/rnsh")): return os.path.expanduser("~/.config/rnsh")
elif os.path.isdir(os.path.expanduser("~/.rnsh")): return os.path.expanduser("~/.rnsh")
else: else:
try: try:
RNS.log(f"Creating configuration directory: {os.path.expanduser('~/.rnsh')}")
os.makedirs(os.path.expanduser("~/.rnsh")) os.makedirs(os.path.expanduser("~/.rnsh"))
return os.path.expanduser("~/.rnsh") return os.path.expanduser("~/.rnsh")
@@ -105,26 +116,30 @@ def ensure_config_directory():
async def _rnsh_cli_main(): async def _rnsh_cli_main():
global verbose_set global verbose_set
args, parser = parse_arguments() args, parser = parse_arguments()
verbose_set = args.verbose > 0 verbose_set = args.verbose > 0
configdir = ensure_config_directory(args.config)
configdir = ensure_config_directory() if not configdir:
RNS.log(f"Could not resolve rnsh configuration directory", RNS.LOG_CRITICAL)
os._exit(1)
if args.print_identity: if args.print_identity:
print_identity(args.config, args.identity, args.service, args.listen) print_identity(configdir, args.rnsconfig, args.identity, args.service, args.listen)
return 0 return 0
logfile = f"{configdir}/logfile"
if args.listen: if args.listen:
allowed_file = None allowed_file = None
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
if os.path.isfile(os.path.expanduser("~/.config/rnsh/allowed_identities")): if os.path.isfile(os.path.expanduser(f"{configdir}/allowed_identities")):
allowed_file = os.path.expanduser("~/.config/rnsh/allowed_identities") allowed_file = os.path.expanduser(f"{configdir}/allowed_identities")
elif os.path.isfile(os.path.expanduser("~/.rnsh/allowed_identities")):
allowed_file = os.path.expanduser("~/.rnsh/allowed_identities")
await listener.listen(configdir=configdir, await listener.listen(configdir=configdir,
rnsconfigdir=args.config, rnsconfigdir=args.rnsconfig,
command=args.command, command=args.command,
identitypath=args.identity, identitypath=args.identity,
logfile=logfile,
service_name=args.service, service_name=args.service,
verbosity=args.verbose, verbosity=args.verbose,
quietness=args.quiet, quietness=args.quiet,
@@ -138,8 +153,9 @@ async def _rnsh_cli_main():
if args.destination is not None: if args.destination is not None:
return_code = await initiator.initiate(configdir=configdir, return_code = await initiator.initiate(configdir=configdir,
rnsconfigdir=args.config, rnsconfigdir=args.rnsconfig,
identitypath=args.identity, identitypath=args.identity,
logfile=logfile,
verbosity=args.verbose, verbosity=args.verbose,
quietness=args.quiet, quietness=args.quiet,
noid=args.no_id, noid=args.no_id,
@@ -170,5 +186,4 @@ def main():
if verbose_set and exc: raise exc if verbose_set and exc: raise exc
sys.exit(return_code if return_code is not None else 255) sys.exit(return_code if return_code is not None else 255)
if __name__ == "__main__": main() if __name__ == "__main__": main()
+71 -71
View File
@@ -103,6 +103,8 @@ class ListenerSession:
self.outlet.set_link_closed_callback(self._link_closed) self.outlet.set_link_closed_callback(self._link_closed)
self.loop = loop self.loop = loop
self.state: LSState = None self.state: LSState = None
self.terminated = False
self.authenticated = False
self.remote_identity = None self.remote_identity = None
self.term: str | None = None self.term: str | None = None
self.stdin_is_pipe: bool = False self.stdin_is_pipe: bool = False
@@ -122,8 +124,10 @@ class ListenerSession:
self.return_code_sent = False self.return_code_sent = False
self.process: process.CallbackSubprocess | None = None self.process: process.CallbackSubprocess | None = None
if self.allow_all: self._set_state(LSState.LSSTATE_WAIT_VERS) if not self.allow_all: self._set_state(LSState.LSSTATE_WAIT_IDENT)
else: self._set_state(LSState.LSSTATE_WAIT_IDENT) else:
self._set_state(LSState.LSSTATE_WAIT_VERS)
self.authenticated = True
self.sessions.append(self) self.sessions.append(self)
protocol.register_message_types(self.channel) protocol.register_message_types(self.channel)
@@ -143,37 +147,31 @@ class ListenerSession:
def _call(self, func: callable, delay: float = 0): def _call(self, func: callable, delay: float = 0):
def call_inner(): def call_inner():
if delay == 0: func() if delay == 0: func()
else: self.loop.call_later(delay, func) else: self.loop.call_later(delay, func)
self.loop.call_soon_threadsafe(call_inner) self.loop.call_soon_threadsafe(call_inner)
def send(self, message: RNS.MessageBase): def send(self, message: RNS.MessageBase): self.channel.send(message)
self.channel.send(message) def _protocol_error(self, name: str): self.terminate(f"Protocol error ({name})")
def _protocol_timeout_error(self, name: str): self.terminate(f"Protocol timeout error: {name}")
def _protocol_error(self, name: str):
self.terminate(f"Protocol error ({name})")
def _protocol_timeout_error(self, name: str):
self.terminate(f"Protocol timeout error: {name}")
def terminate(self, error: str = None): def terminate(self, error: str = None):
self.terminated = True
self.state = LSState.LSSTATE_ERROR
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
RNS.log("Terminating session" + (f": {error}" if error else ""), RNS.LOG_DEBUG) RNS.log("Terminating session" + (f": {error}" if error else ""), RNS.LOG_DEBUG)
if error and self.state != LSState.LSSTATE_TEARDOWN: if error and self.state != LSState.LSSTATE_TEARDOWN:
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
self.send(protocol.ErrorMessage(error, True)) self.send(protocol.ErrorMessage(error, True))
self.state = LSState.LSSTATE_ERROR
self._terminate_process() self._terminate_process()
self._call(self._prune, max(self.outlet.rtt * 3, process.CallbackSubprocess.PROCESS_PIPE_TIME+5)) self._call(self._prune, max(self.outlet.rtt * 3, process.CallbackSubprocess.PROCESS_PIPE_TIME+5))
def _prune(self): def _prune(self):
self.state = LSState.LSSTATE_TEARDOWN self.state = LSState.LSSTATE_TEARDOWN
RNS.log("Pruning session", RNS.LOG_DEBUG) RNS.log("Pruning session", RNS.LOG_DEBUG)
with contextlib.suppress(ValueError): with contextlib.suppress(ValueError): self.sessions.remove(self)
self.sessions.remove(self) with contextlib.suppress(Exception): self.outlet.teardown()
with contextlib.suppress(Exception):
self.outlet.teardown()
def _check_protocol_timeout(self, fail_condition: Callable[[], bool], name: str): def _check_protocol_timeout(self, fail_condition: Callable[[], bool], name: str):
timeout = True timeout = True
@@ -183,7 +181,6 @@ class ListenerSession:
def _link_closed(self, outlet: LSOutletBase): def _link_closed(self, outlet: LSOutletBase):
outlet.unset_link_closed_callback() outlet.unset_link_closed_callback()
if outlet != self.outlet: if outlet != self.outlet:
RNS.log("Link closed received from incorrect outlet", RNS.LOG_DEBUG) RNS.log("Link closed received from incorrect outlet", RNS.LOG_DEBUG)
return return
@@ -200,11 +197,14 @@ class ListenerSession:
if self.state not in [LSState.LSSTATE_WAIT_IDENT, LSState.LSSTATE_WAIT_VERS]: if self.state not in [LSState.LSSTATE_WAIT_IDENT, LSState.LSSTATE_WAIT_VERS]:
self._protocol_error(LSState.LSSTATE_WAIT_IDENT.name) self._protocol_error(LSState.LSSTATE_WAIT_IDENT.name)
if not self.allow_all and identity.hash not in self.allowed_identity_hashes and identity.hash not in self.allowed_file_identity_hashes: if self.allow_all or identity.hash in self.allowed_identity_hashes or identity.hash in self.allowed_file_identity_hashes:
self.terminate("Identity is not allowed.") self.authenticated = True
self.remote_identity = identity
self._set_state(LSState.LSSTATE_WAIT_VERS)
self.remote_identity = identity else:
self._set_state(LSState.LSSTATE_WAIT_VERS) self.authenticated = False
self.terminate("Identity not allowed")
@classmethod @classmethod
async def pump_all(cls) -> True: async def pump_all(cls) -> True:
@@ -241,8 +241,7 @@ class ListenerSession:
if compressed_length < max_data_len and compressed_length < chunk_segment_length: if compressed_length < max_data_len and compressed_length < chunk_segment_length:
comp_success = True comp_success = True
break break
else: else: comp_try += 1
comp_try += 1
if comp_success: if comp_success:
diff = max_data_len - len(compressed_chunk) diff = max_data_len - len(compressed_chunk)
@@ -255,10 +254,8 @@ class ListenerSession:
return comp_success, processed_length, chunk return comp_success, processed_length, chunk
try: try:
if self.state != LSState.LSSTATE_RUNNING: if self.state != LSState.LSSTATE_RUNNING: return False
return False elif not self.channel.is_ready_to_send(): return False
elif not self.channel.is_ready_to_send():
return False
elif len(self.stderr_buf) > 0: elif len(self.stderr_buf) > 0:
comp_success, processed_length, data = compress_adaptive(self.stderr_buf) comp_success, processed_length, data = compress_adaptive(self.stderr_buf)
self.stderr_buf = self.stderr_buf[processed_length:] self.stderr_buf = self.stderr_buf[processed_length:]
@@ -267,8 +264,7 @@ class ListenerSession:
msg = protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDERR, msg = protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDERR,
data, send_eof, comp_success) data, send_eof, comp_success)
self.send(msg) self.send(msg)
if send_eof: if send_eof: self.stderr_eof_sent = True
self.stderr_eof_sent = True
return True return True
elif len(self.stdout_buf) > 0: elif len(self.stdout_buf) > 0:
comp_success, processed_length, data = compress_adaptive(self.stdout_buf) comp_success, processed_length, data = compress_adaptive(self.stdout_buf)
@@ -278,8 +274,7 @@ class ListenerSession:
msg = protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDOUT, msg = protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDOUT,
data, send_eof, comp_success) data, send_eof, comp_success)
self.send(msg) self.send(msg)
if send_eof: if send_eof: self.stdout_eof_sent = True
self.stdout_eof_sent = True
return True return True
elif self.return_code is not None and not self.return_code_sent: elif self.return_code is not None and not self.return_code_sent:
msg = protocol.CommandExitedMessage(self.return_code) msg = protocol.CommandExitedMessage(self.return_code)
@@ -295,22 +290,32 @@ class ListenerSession:
def _terminate_process(self): def _terminate_process(self):
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
if self.process and self.process.running: if self.process and self.process.running: self.process.terminate()
self.process.terminate()
def _start_cmd(self, cmdline: [str], pipe_stdin: bool, pipe_stdout: bool, pipe_stderr: bool, tcflags: [any], def _start_cmd(self, cmdline: [str], pipe_stdin: bool, pipe_stdout: bool, pipe_stderr: bool, tcflags: [any],
term: str | None, rows: int, cols: int, hpix: int, vpix: int): term: str | None, rows: int, cols: int, hpix: int, vpix: int):
if self.terminated:
RNS.log(f"Attempt to start command on terminated session: {cmdline}", RNS.LOG_WARNING)
return
if not self.authenticated:
RNS.log(f"Attempt to start command on unauthenticated session: {cmdline}", RNS.LOG_WARNING)
self.terminate("Invalid state")
return
if self.state != LSState.LSSTATE_RUNNING:
RNS.log(f"Attempt to start command on session not in running state: {cmdline}", RNS.LOG_WARNING)
self.terminate("Invalid state")
return
self.cmdline = self.default_command self.cmdline = self.default_command
if not self.allow_remote_command and cmdline and len(cmdline) > 0: if not self.allow_remote_command and cmdline and len(cmdline) > 0:
self.terminate("Remote command line not allowed by listener") self.terminate("Remote command line not allowed by listener")
return return
if self.remote_cmd_as_args and cmdline and len(cmdline) > 0: if self.remote_cmd_as_args and cmdline and len(cmdline) > 0: self.cmdline.extend(cmdline)
self.cmdline.extend(cmdline) elif cmdline and len(cmdline) > 0: self.cmdline = cmdline
elif cmdline and len(cmdline) > 0:
self.cmdline = cmdline
self.stdin_is_pipe = pipe_stdin self.stdin_is_pipe = pipe_stdin
self.stdout_is_pipe = pipe_stdout self.stdout_is_pipe = pipe_stdout
@@ -318,11 +323,8 @@ class ListenerSession:
self.tcflags = tcflags self.tcflags = tcflags
self.term = term self.term = term
def stdout(data: bytes): def stdout(data: bytes): self.stdout_buf.extend(data)
self.stdout_buf.extend(data) def stderr(data: bytes): self.stderr_buf.extend(data)
def stderr(data: bytes):
self.stderr_buf.extend(data)
try: try:
self.process = process.CallbackSubprocess(argv=self.cmdline, self.process = process.CallbackSubprocess(argv=self.cmdline,
@@ -347,22 +349,28 @@ class ListenerSession:
self.cols = cols self.cols = cols
self.hpix = hpix self.hpix = hpix
self.vpix = vpix self.vpix = vpix
with contextlib.suppress(Exception): with contextlib.suppress(Exception): self.process.set_winsize(rows, cols, hpix, vpix)
self.process.set_winsize(rows, cols, hpix, vpix)
def _received_stdin(self, data: bytes, eof: bool): def _received_stdin(self, data: bytes, eof: bool):
if data and len(data) > 0: if data and len(data) > 0: self.process.write(data)
self.process.write(data) if eof: self.process.close_stdin()
if eof:
self.process.close_stdin()
def _handle_message(self, message: RNS.MessageBase): def _handle_message(self, message: RNS.MessageBase):
if self.terminated:
RNS.log(f"Received packet, but session was terminated", RNS.LOG_ERROR)
return
if self.state > LSState.LSSTATE_RUNNING:
RNS.log(f"Received packet, but in state {self.state.name}", RNS.LOG_ERROR)
return
if self.state == LSState.LSSTATE_WAIT_IDENT: if self.state == LSState.LSSTATE_WAIT_IDENT:
# Ignore any messages until the initiator has identified to avoid race conditions # Ignore any messages until the initiator has identified to avoid race conditions
# between identity announcement and early protocol messages. # between identity announcement and early protocol messages.
RNS.log("Ignoring message while waiting for identification", RNS.LOG_DEBUG) RNS.log("Ignoring message while waiting for identification", RNS.LOG_DEBUG)
return return
if self.state == LSState.LSSTATE_WAIT_VERS: elif self.state == LSState.LSSTATE_WAIT_VERS:
if not self.authenticated:
self.terminate("Invalid state")
return
if not isinstance(message, protocol.VersionInfoMessage): if not isinstance(message, protocol.VersionInfoMessage):
self._protocol_error(self.state.name) self._protocol_error(self.state.name)
return return
@@ -374,6 +382,9 @@ class ListenerSession:
self._set_state(LSState.LSSTATE_WAIT_CMD) self._set_state(LSState.LSSTATE_WAIT_CMD)
return return
elif self.state == LSState.LSSTATE_WAIT_CMD: elif self.state == LSState.LSSTATE_WAIT_CMD:
if not self.authenticated:
self.terminate("Invalid state")
return
if not isinstance(message, protocol.ExecuteCommandMesssage): if not isinstance(message, protocol.ExecuteCommandMesssage):
return self._protocol_error(self.state.name) return self._protocol_error(self.state.name)
RNS.log(f"Execute command message on link {self.outlet}: {message.cmdline}", RNS.LOG_VERBOSE) RNS.log(f"Execute command message on link {self.outlet}: {message.cmdline}", RNS.LOG_VERBOSE)
@@ -382,6 +393,9 @@ class ListenerSession:
message.tcflags, message.term, message.rows, message.cols, message.hpix, message.vpix) message.tcflags, message.term, message.rows, message.cols, message.hpix, message.vpix)
return return
elif self.state == LSState.LSSTATE_RUNNING: elif self.state == LSState.LSSTATE_RUNNING:
if not self.authenticated:
self.terminate("Invalid state")
return
if isinstance(message, protocol.WindowSizeMessage): if isinstance(message, protocol.WindowSizeMessage):
self._set_window_size(message.rows, message.cols, message.hpix, message.vpix) self._set_window_size(message.rows, message.cols, message.hpix, message.vpix)
elif isinstance(message, protocol.StreamDataMessage): elif isinstance(message, protocol.StreamDataMessage):
@@ -394,9 +408,6 @@ class ListenerSession:
# echo noop only on listener--used for keepalive/connectivity check # echo noop only on listener--used for keepalive/connectivity check
self.send(message) self.send(message)
return return
elif self.state in [LSState.LSSTATE_ERROR, LSState.LSSTATE_TEARDOWN]:
RNS.log(f"Received packet, but in state {self.state.name}", RNS.LOG_ERROR)
return
else: else:
self._protocol_error("unexpected message") self._protocol_error("unexpected message")
return return
@@ -405,29 +416,20 @@ class ListenerSession:
class RNSOutlet(LSOutletBase): class RNSOutlet(LSOutletBase):
def set_initiator_identified_callback(self, cb: Callable[[LSOutletBase, _TIdentity], None]): def set_initiator_identified_callback(self, cb: Callable[[LSOutletBase, _TIdentity], None]):
def inner_cb(link, identity: _TIdentity): def inner_cb(link, identity: _TIdentity): cb(self, identity)
cb(self, identity)
self.link.set_remote_identified_callback(inner_cb) self.link.set_remote_identified_callback(inner_cb)
def set_link_closed_callback(self, cb: Callable[[LSOutletBase], None]): def set_link_closed_callback(self, cb: Callable[[LSOutletBase], None]):
def inner_cb(link): def inner_cb(link): cb(self)
cb(self)
self.link.set_link_closed_callback(inner_cb) self.link.set_link_closed_callback(inner_cb)
def unset_link_closed_callback(self): def unset_link_closed_callback(self): self.link.set_link_closed_callback(None)
self.link.set_link_closed_callback(None) def teardown(self): self.link.teardown()
def teardown(self):
self.link.teardown()
@property @property
def rtt(self) -> float: def rtt(self) -> float: return self.link.rtt
return self.link.rtt
def __str__(self): def __str__(self): return f"Outlet on {self.link}"
return f"Outlet RNS Link {self.link}"
def __init__(self, link: RNS.Link): def __init__(self, link: RNS.Link):
self.link = link self.link = link
@@ -435,7 +437,5 @@ class RNSOutlet(LSOutletBase):
@staticmethod @staticmethod
def get_outlet(link: RNS.Link): def get_outlet(link: RNS.Link):
if hasattr(link, "lsoutlet"): if hasattr(link, "lsoutlet"): return link.lsoutlet
return link.lsoutlet
return RNSOutlet(link) return RNSOutlet(link)
+3
View File
@@ -450,6 +450,9 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
clients_string = "" clients_string = ""
else: else:
clients_string = "Clients : "+str(clients) clients_string = "Clients : "+str(clients)
if "blocked_ips" in ifstat:
p = ifstat["blocked_ips"] > 0
if p: clients_string += "\n Blocked : "+str(ifstat["blocked_ips"])+" IP"+"s" if p else ""
else: else:
clients = None clients = None
+3 -1
View File
@@ -70,7 +70,8 @@ LOG_NOTICE = 3
LOG_INFO = 4 LOG_INFO = 4
LOG_VERBOSE = 5 LOG_VERBOSE = 5
LOG_DEBUG = 6 LOG_DEBUG = 6
LOG_EXTREME = 7 LOG_PATHING = 7
LOG_EXTREME = 8
LOG_STDOUT = 0x91 LOG_STDOUT = 0x91
LOG_FILE = 0x92 LOG_FILE = 0x92
@@ -102,6 +103,7 @@ def loglevelname(level):
if (level == LOG_INFO): return "[Info] " if (level == LOG_INFO): return "[Info] "
if (level == LOG_VERBOSE): return "[Verbose] " if (level == LOG_VERBOSE): return "[Verbose] "
if (level == LOG_DEBUG): return "[Debug] " if (level == LOG_DEBUG): return "[Debug] "
if (level == LOG_PATHING): return "[Pathing] "
if (level == LOG_EXTREME): return "[Extra] " if (level == LOG_EXTREME): return "[Extra] "
return "Unknown" return "Unknown"
+1 -1
View File
@@ -1 +1 @@
__version__ = "1.3.8" __version__ = "1.3.9"
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
# Sphinx build info version 1 # Sphinx build info version 1
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. # This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
config: 5f1bb7356498498f06570848af4171af config: b7a9e97674355dfa0f0218a058ae09df
tags: 645f666f9bcd5a90fca523b33c5a78b7 tags: 645f666f9bcd5a90fca523b33c5a78b7
+52 -1
View File
@@ -230,6 +230,51 @@ specify the target Yggdrasil IPv6 address and port, like so:
target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5 target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5
target_port = 4343 target_port = 4343
Automated Blocking
------------------
Listener instances of ``BackboneInterface`` will automatically block fast-flapping clients,
that repeatedly connect for a short amount of time, and then disconnect. Such behavior usually
occurs from clients trying to spam the network with announces or path requests, by connecting, dumping
a massive amount of requests, and then disconnecting in an attempt to bypass rate limits.
While such bypass attempts only have very limited effect on the amount of spam actually dumped (ingress limits
trigger immediately once a client exceeds rate limits), the behavior is often seen anyways, and
causes log noise and needless interface rotation. The fast-flapping block ensures such clients
are never allocated an interface on the transport instance.
Another common cause can simply be clients using implementations that are broken, or automated
scanning tools attempting to connect to the instance.
.. code:: ini
[[Backbone Listener]]
type = BackboneInterface
enabled = yes
listen_on = 0.0.0.0
port = 4242
# Whether to enable blocking
block_fast_flapping = yes
# How long an IP address stays
# blocked, in minutes. Set to
# 12 hours by default.
fast_flapping_block_time = 720
# The minimum time, in seconds,
# an interface must stay connected
# to be considered not fast-flapping.
fast_flapping_threshold = 20
# Amount of fast flaps a remote
# IP can perform before having
# blocking triggered.
fast_flapping_grace = 5
The configuration options listed in the example above are the *default values*, and you do
not need to add them for automated blocking to work, but you can use them to change the
behavior from the defaults, if necessary.
.. _interfaces-tcps: .. _interfaces-tcps:
TCP Server Interface TCP Server Interface
@@ -998,7 +1043,13 @@ On a real system, you should make the script robust enough to deal with intermit
**Physical Location** **Physical Location**
``latitude``, ``longitude``, ``height`` ``latitude``, ``longitude``, ``height``
Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters. Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters above mean sea level.
``location_cmd``
Optional path to executable or script that returns the physical coordinates for the interface. This can be used instead of manually setting ``latitude``, ``longitude`` and ``height``. Reticulum expects the script to output the location data to ``stdout`` on a single line, separated by commas, with values as floating point numbers in the format ``LAT, LON, HEIGHT``. Coordinates should be in decimal degrees, height in meters above mean sea level.
.. note::
The height value for interface discovery is specified in *height above mean sea level*! This is the geoid-corrected height value, and is distinct from GPS altitude. If you manually specify height, it will typically be set to what you find on a topographical map. If you are using a script to output the location data, make sure that you are using the geoid-corrected altitude, not height above the GPS ellipsoid. Most GPS systems will make both figures available.
**Radio Parameters** **Radio Parameters**
+28 -32
View File
@@ -1002,39 +1002,35 @@ available. These are only recognised immediately after a newline character:
destination hexadecimal hash of the destination to connect to destination hexadecimal hash of the destination to connect to
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
--config, -c CONFIG path to alternative Reticulum config directory --config, -c PATH path to config directory
--identity, -i IDENTITY --rnsconfig, -c PATH path to alternative Reticulum config directory
path to identity file to use --identity, -i IDENTITY path to identity file to use
-v, --verbose increase verbosity -v, --verbose increase verbosity
-q, --quiet decrease verbosity -q, --quiet decrease verbosity
-p, --print-identity print identity and destination info and exit -p, --print-identity print identity and destination info and exit
--version show program's version number and exit --version show program's version number and exit
-l, --listen listen (server) mode; any command specified after -- -l, --listen listen (server) mode; any command specified after --
will be used as the default command when the initiator will be used as the default command when the initiator
does not provide one or when remote command execution does not provide one or when remote command execution
is disabled; if no command is specified, the default is disabled; if no command is specified, the default
shell of the user running rnsh will be used shell of the user running rnsh will be used
-s, --service SERVICE -s, --service SERVICE service name for identity file if not the default
service name for identity file if not the default -b, --announce PERIOD announce on startup and every PERIOD seconds; specify
-b, --announce PERIOD 0 to announce on startup only
announce on startup and every PERIOD seconds; specify -a, --allowed HASH allow this identity to connect (may be specified
0 to announce on startup only multiple times); allowed identities can also be
-a, --allowed HASH allow this identity to connect (may be specified specified in ~/.rnsh/allowed_identities or
multiple times); allowed identities can also be ~/.config/rnsh/allowed_identities, one hash per line
specified in ~/.rnsh/allowed_identities or -n, --no-auth disable authentication (allow any identity to connect)
~/.config/rnsh/allowed_identities, one hash per line
-n, --no-auth disable authentication (allow any identity to connect)
-A, --remote-command-as-args -A, --remote-command-as-args
concatenate remote command to the argument list of the concatenate remote command to the argument list of the
default program or shell default program or shell
-C, --no-remote-command -C, --no-remote-command disable executing command lines received from the
disable executing command lines received from the remote initiator
remote initiator -N, --no-id disable identity announcement on connect
-N, --no-id disable identity announcement on connect -m, --mirror return with the exit code of the remote process
-m, --mirror return with the exit code of the remote process -w, --timeout SECONDS connect and request timeout in seconds
-w, --timeout SECONDS
connect and request timeout in seconds
When specifying a command to execute, separate rnsh options from the command When specifying a command to execute, separate rnsh options from the command
and its arguments with --. For example: and its arguments with --. For example:
+1 -1
View File
@@ -1,5 +1,5 @@
const DOCUMENTATION_OPTIONS = { const DOCUMENTATION_OPTIONS = {
VERSION: '1.3.8', VERSION: '1.3.9',
LANGUAGE: 'en', LANGUAGE: 'en',
COLLAPSE_INDEX: false, COLLAPSE_INDEX: false,
BUILDER: 'html', BUILDER: 'html',
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Distributed Development - Reticulum Network Stack 1.3.8 documentation</title> <title>Distributed Development - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -431,7 +431,7 @@
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Code Examples - Reticulum Network Stack 1.3.8 documentation</title> <title>Code Examples - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -3648,7 +3648,7 @@
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.3.8 documentation</title> <title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -296,7 +296,7 @@
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -5,7 +5,7 @@
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#"><link rel="search" title="Search" href="search.html"> <meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#"><link rel="search" title="Search" href="search.html">
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.3.8 documentation</title> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -178,7 +178,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -202,7 +202,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -846,7 +846,7 @@
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Getting Started Fast - Reticulum Network Stack 1.3.8 documentation</title> <title>Getting Started Fast - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -776,7 +776,7 @@ section of this manual.</p>
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Git Over Reticulum - Reticulum Network Stack 1.3.8 documentation</title> <title>Git Over Reticulum - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -1594,7 +1594,7 @@ done
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Communications Hardware - Reticulum Network Stack 1.3.8 documentation</title> <title>Communications Hardware - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -676,7 +676,7 @@ can be used with Reticulum. This includes virtual software modems such as
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+5 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Reticulum Network Stack 1.3.8 documentation</title> <title>Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="#"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="#"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -486,6 +486,7 @@ to participate in the development of Reticulum itself.</p>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#backbone-interface">Backbone Interface</a><ul> <li class="toctree-l2"><a class="reference internal" href="interfaces.html#backbone-interface">Backbone Interface</a><ul>
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#listeners">Listeners</a></li> <li class="toctree-l3"><a class="reference internal" href="interfaces.html#listeners">Listeners</a></li>
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#connecting-remotes">Connecting Remotes</a></li> <li class="toctree-l3"><a class="reference internal" href="interfaces.html#connecting-remotes">Connecting Remotes</a></li>
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#automated-blocking">Automated Blocking</a></li>
</ul> </ul>
</li> </li>
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#tcp-server-interface">TCP Server Interface</a></li> <li class="toctree-l2"><a class="reference internal" href="interfaces.html#tcp-server-interface">TCP Server Interface</a></li>
@@ -719,7 +720,7 @@ to participate in the development of Reticulum itself.</p>
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+53 -5
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Configuring Interfaces - Reticulum Network Stack 1.3.8 documentation</title> <title>Configuring Interfaces - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -460,6 +460,47 @@ specify the target Yggdrasil IPv6 address and port, like so:</p>
</pre></div> </pre></div>
</div> </div>
</section> </section>
<section id="automated-blocking">
<h3>Automated Blocking<a class="headerlink" href="#automated-blocking" title="Link to this heading"></a></h3>
<p>Listener instances of <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code> will automatically block fast-flapping clients,
that repeatedly connect for a short amount of time, and then disconnect. Such behavior usually
occurs from clients trying to spam the network with announces or path requests, by connecting, dumping
a massive amount of requests, and then disconnecting in an attempt to bypass rate limits.</p>
<p>While such bypass attempts only have very limited effect on the amount of spam actually dumped (ingress limits
trigger immediately once a client exceeds rate limits), the behavior is often seen anyways, and
causes log noise and needless interface rotation. The fast-flapping block ensures such clients
are never allocated an interface on the transport instance.</p>
<p>Another common cause can simply be clients using implementations that are broken, or automated
scanning tools attempting to connect to the instance.</p>
<div class="highlight-ini notranslate"><div class="highlight"><pre><span></span><span class="k">[[Backbone Listener]]</span>
<span class="w"> </span><span class="na">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">BackboneInterface</span>
<span class="w"> </span><span class="na">enabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">yes</span>
<span class="w"> </span><span class="na">listen_on</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">0.0.0.0</span>
<span class="w"> </span><span class="na">port</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">4242</span>
<span class="w"> </span><span class="c1"># Whether to enable blocking</span>
<span class="w"> </span><span class="na">block_fast_flapping</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">yes</span>
<span class="w"> </span><span class="c1"># How long an IP address stays</span>
<span class="w"> </span><span class="c1"># blocked, in minutes. Set to</span>
<span class="w"> </span><span class="c1"># 12 hours by default.</span>
<span class="w"> </span><span class="na">fast_flapping_block_time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">720</span>
<span class="w"> </span><span class="c1"># The minimum time, in seconds,</span>
<span class="w"> </span><span class="c1"># an interface must stay connected</span>
<span class="w"> </span><span class="c1"># to be considered not fast-flapping.</span>
<span class="w"> </span><span class="na">fast_flapping_threshold</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">20</span>
<span class="w"> </span><span class="c1"># Amount of fast flaps a remote</span>
<span class="w"> </span><span class="c1"># IP can perform before having</span>
<span class="w"> </span><span class="c1"># blocking triggered.</span>
<span class="w"> </span><span class="na">fast_flapping_grace</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">5</span>
</pre></div>
</div>
<p>The configuration options listed in the example above are the <em>default values</em>, and you do
not need to add them for automated blocking to work, but you can use them to change the
behavior from the defaults, if necessary.</p>
</section>
</section> </section>
<section id="tcp-server-interface"> <section id="tcp-server-interface">
<span id="interfaces-tcps"></span><h2>TCP Server Interface<a class="headerlink" href="#tcp-server-interface" title="Link to this heading"></a></h2> <span id="interfaces-tcps"></span><h2>TCP Server Interface<a class="headerlink" href="#tcp-server-interface" title="Link to this heading"></a></h2>
@@ -1154,9 +1195,15 @@ curl<span class="w"> </span>-s<span class="w"> </span>ip.me
</dl> </dl>
<p><strong>Physical Location</strong></p> <p><strong>Physical Location</strong></p>
<dl class="simple"> <dl class="simple">
<dt><code class="docutils literal notranslate"><span class="pre">latitude</span></code>, <code class="docutils literal notranslate"><span class="pre">longitude</span></code>, <code class="docutils literal notranslate"><span class="pre">height</span></code></dt><dd><p>Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters.</p> <dt><code class="docutils literal notranslate"><span class="pre">latitude</span></code>, <code class="docutils literal notranslate"><span class="pre">longitude</span></code>, <code class="docutils literal notranslate"><span class="pre">height</span></code></dt><dd><p>Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters above mean sea level.</p>
</dd>
<dt><code class="docutils literal notranslate"><span class="pre">location_cmd</span></code></dt><dd><p>Optional path to executable or script that returns the physical coordinates for the interface. This can be used instead of manually setting <code class="docutils literal notranslate"><span class="pre">latitude</span></code>, <code class="docutils literal notranslate"><span class="pre">longitude</span></code> and <code class="docutils literal notranslate"><span class="pre">height</span></code>. Reticulum expects the script to output the location data to <code class="docutils literal notranslate"><span class="pre">stdout</span></code> on a single line, separated by commas, with values as floating point numbers in the format <code class="docutils literal notranslate"><span class="pre">LAT,</span> <span class="pre">LON,</span> <span class="pre">HEIGHT</span></code>. Coordinates should be in decimal degrees, height in meters above mean sea level.</p>
</dd> </dd>
</dl> </dl>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>The height value for interface discovery is specified in <em>height above mean sea level</em>! This is the geoid-corrected height value, and is distinct from GPS altitude. If you manually specify height, it will typically be set to what you find on a topographical map. If you are using a script to output the location data, make sure that you are using the geoid-corrected altitude, not height above the GPS ellipsoid. Most GPS systems will make both figures available.</p>
</div>
<p><strong>Radio Parameters</strong></p> <p><strong>Radio Parameters</strong></p>
<p>For physical radio interfaces like <code class="docutils literal notranslate"><span class="pre">RNodeInterface</span></code> or <code class="docutils literal notranslate"><span class="pre">KISSInterface</span></code>, the following optional parameters allow you to broadcast the operating frequency and characteristics, allowing clients to verify compatibility before connecting:</p> <p>For physical radio interfaces like <code class="docutils literal notranslate"><span class="pre">RNodeInterface</span></code> or <code class="docutils literal notranslate"><span class="pre">KISSInterface</span></code>, the following optional parameters allow you to broadcast the operating frequency and characteristics, allowing clients to verify compatibility before connecting:</p>
<dl class="simple"> <dl class="simple">
@@ -1767,6 +1814,7 @@ interface basis under the relevant interface configuration section.</p>
<li><a class="reference internal" href="#backbone-interface">Backbone Interface</a><ul> <li><a class="reference internal" href="#backbone-interface">Backbone Interface</a><ul>
<li><a class="reference internal" href="#listeners">Listeners</a></li> <li><a class="reference internal" href="#listeners">Listeners</a></li>
<li><a class="reference internal" href="#connecting-remotes">Connecting Remotes</a></li> <li><a class="reference internal" href="#connecting-remotes">Connecting Remotes</a></li>
<li><a class="reference internal" href="#automated-blocking">Automated Blocking</a></li>
</ul> </ul>
</li> </li>
<li><a class="reference internal" href="#tcp-server-interface">TCP Server Interface</a></li> <li><a class="reference internal" href="#tcp-server-interface">TCP Server Interface</a></li>
@@ -1803,7 +1851,7 @@ interface basis under the relevant interface configuration section.</p>
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Reticulum License - Reticulum Network Stack 1.3.8 documentation</title> <title>Reticulum License - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -345,7 +345,7 @@ SOFTWARE.
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Building Networks - Reticulum Network Stack 1.3.8 documentation</title> <title>Building Networks - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -664,7 +664,7 @@ differently than a mobile device roaming between radio cells.</p>
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
Binary file not shown.
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>API Reference - Reticulum Network Stack 1.3.8 documentation</title> <title>API Reference - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -2519,7 +2519,7 @@ will announce it.</p>
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -8,7 +8,7 @@
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<meta name="robots" content="noindex" /> <meta name="robots" content="noindex" />
<title>Search - Reticulum Network Stack 1.3.8 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <title>Search - Reticulum Network Stack 1.3.9 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="#" role="search"> </a><form class="sidebar-search-container" method="get" action="#" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -304,7 +304,7 @@
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Programs Using Reticulum - Reticulum Network Stack 1.3.8 documentation</title> <title>Programs Using Reticulum - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -513,7 +513,7 @@ plugin system for expandability.</p>
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Support Reticulum - Reticulum Network Stack 1.3.8 documentation</title> <title>Support Reticulum - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -383,7 +383,7 @@ circumstances, so we rely on old-fashioned human feedback.</p>
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Understanding Reticulum - Reticulum Network Stack 1.3.8 documentation</title> <title>Understanding Reticulum - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -1118,7 +1118,7 @@ those risks are acceptable to you.</p>
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+32 -36
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Using Reticulum on Your System - Reticulum Network Stack 1.3.8 documentation</title> <title>Using Reticulum on Your System - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -1106,39 +1106,35 @@ positional arguments:
destination hexadecimal hash of the destination to connect to destination hexadecimal hash of the destination to connect to
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
--config, -c CONFIG path to alternative Reticulum config directory --config, -c PATH path to config directory
--identity, -i IDENTITY --rnsconfig, -c PATH path to alternative Reticulum config directory
path to identity file to use --identity, -i IDENTITY path to identity file to use
-v, --verbose increase verbosity -v, --verbose increase verbosity
-q, --quiet decrease verbosity -q, --quiet decrease verbosity
-p, --print-identity print identity and destination info and exit -p, --print-identity print identity and destination info and exit
--version show program&#39;s version number and exit --version show program&#39;s version number and exit
-l, --listen listen (server) mode; any command specified after -- -l, --listen listen (server) mode; any command specified after --
will be used as the default command when the initiator will be used as the default command when the initiator
does not provide one or when remote command execution does not provide one or when remote command execution
is disabled; if no command is specified, the default is disabled; if no command is specified, the default
shell of the user running rnsh will be used shell of the user running rnsh will be used
-s, --service SERVICE -s, --service SERVICE service name for identity file if not the default
service name for identity file if not the default -b, --announce PERIOD announce on startup and every PERIOD seconds; specify
-b, --announce PERIOD 0 to announce on startup only
announce on startup and every PERIOD seconds; specify -a, --allowed HASH allow this identity to connect (may be specified
0 to announce on startup only multiple times); allowed identities can also be
-a, --allowed HASH allow this identity to connect (may be specified specified in ~/.rnsh/allowed_identities or
multiple times); allowed identities can also be ~/.config/rnsh/allowed_identities, one hash per line
specified in ~/.rnsh/allowed_identities or -n, --no-auth disable authentication (allow any identity to connect)
~/.config/rnsh/allowed_identities, one hash per line
-n, --no-auth disable authentication (allow any identity to connect)
-A, --remote-command-as-args -A, --remote-command-as-args
concatenate remote command to the argument list of the concatenate remote command to the argument list of the
default program or shell default program or shell
-C, --no-remote-command -C, --no-remote-command disable executing command lines received from the
disable executing command lines received from the remote initiator
remote initiator -N, --no-id disable identity announcement on connect
-N, --no-id disable identity announcement on connect -m, --mirror return with the exit code of the remote process
-m, --mirror return with the exit code of the remote process -w, --timeout SECONDS connect and request timeout in seconds
-w, --timeout SECONDS
connect and request timeout in seconds
When specifying a command to execute, separate rnsh options from the command When specifying a command to execute, separate rnsh options from the command
and its arguments with --. For example: and its arguments with --. For example:
@@ -1639,7 +1635,7 @@ systemctl --user enable rnsd.service
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>What is Reticulum? - Reticulum Network Stack 1.3.8 documentation</title> <title>What is Reticulum? - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -505,7 +505,7 @@ network, and vice versa.</p>
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image"> <link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --> <!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Zen of Reticulum - Reticulum Network Stack 1.3.8 documentation</title> <title>Zen of Reticulum - Reticulum Network Stack 1.3.9 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" /> <link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" /> <link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" /> <link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label> </label>
</div> </div>
<div class="header-center"> <div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.8 documentation</div></a> <a href="index.html"><div class="brand">Reticulum Network Stack 1.3.9 documentation</div></a>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle-container theme-toggle-header"> <div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/> <img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div> </div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.8 documentation</span> <span class="sidebar-brand-text">Reticulum Network Stack 1.3.9 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search"> </a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search"> <input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -677,7 +677,7 @@ Imagine a messaging app. You write it once. It works on a laptop connected to fi
</aside> </aside>
</div> </div>
</div><script src="_static/documentation_options.js?v=3cbadadc"></script> </div><script src="_static/documentation_options.js?v=98c25ec5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script> <script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script> <script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script> <script src="_static/scripts/furo.js?v=46bd48cc"></script>
+1
View File
@@ -161,6 +161,7 @@ to participate in the development of Reticulum itself.
* [Backbone Interface](interfaces.md#backbone-interface) * [Backbone Interface](interfaces.md#backbone-interface)
* [Listeners](interfaces.md#listeners) * [Listeners](interfaces.md#listeners)
* [Connecting Remotes](interfaces.md#connecting-remotes) * [Connecting Remotes](interfaces.md#connecting-remotes)
* [Automated Blocking](interfaces.md#automated-blocking)
* [TCP Server Interface](interfaces.md#tcp-server-interface) * [TCP Server Interface](interfaces.md#tcp-server-interface)
* [TCP Client Interface](interfaces.md#tcp-client-interface) * [TCP Client Interface](interfaces.md#tcp-client-interface)
* [UDP Interface](interfaces.md#udp-interface) * [UDP Interface](interfaces.md#udp-interface)
+52 -1
View File
@@ -212,6 +212,51 @@ specify the target Yggdrasil IPv6 address and port, like so:
target_port = 4343 target_port = 4343
``` ```
### Automated Blocking
Listener instances of `BackboneInterface` will automatically block fast-flapping clients,
that repeatedly connect for a short amount of time, and then disconnect. Such behavior usually
occurs from clients trying to spam the network with announces or path requests, by connecting, dumping
a massive amount of requests, and then disconnecting in an attempt to bypass rate limits.
While such bypass attempts only have very limited effect on the amount of spam actually dumped (ingress limits
trigger immediately once a client exceeds rate limits), the behavior is often seen anyways, and
causes log noise and needless interface rotation. The fast-flapping block ensures such clients
are never allocated an interface on the transport instance.
Another common cause can simply be clients using implementations that are broken, or automated
scanning tools attempting to connect to the instance.
```ini
[[Backbone Listener]]
type = BackboneInterface
enabled = yes
listen_on = 0.0.0.0
port = 4242
# Whether to enable blocking
block_fast_flapping = yes
# How long an IP address stays
# blocked, in minutes. Set to
# 12 hours by default.
fast_flapping_block_time = 720
# The minimum time, in seconds,
# an interface must stay connected
# to be considered not fast-flapping.
fast_flapping_threshold = 20
# Amount of fast flaps a remote
# IP can perform before having
# blocking triggered.
fast_flapping_grace = 5
```
The configuration options listed in the example above are the *default values*, and you do
not need to add them for automated blocking to work, but you can use them to change the
behavior from the defaults, if necessary.
## TCP Server Interface ## TCP Server Interface
The TCP Server interface is suitable for allowing other peers to connect over The TCP Server interface is suitable for allowing other peers to connect over
@@ -939,7 +984,13 @@ If you enable `discovery_encrypt` but do not configure a valid `network_identity
**Physical Location** **Physical Location**
`latitude`, `longitude`, `height` `latitude`, `longitude`, `height`
: Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters. : Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters above mean sea level.
`location_cmd`
: Optional path to executable or script that returns the physical coordinates for the interface. This can be used instead of manually setting `latitude`, `longitude` and `height`. Reticulum expects the script to output the location data to `stdout` on a single line, separated by commas, with values as floating point numbers in the format `LAT, LON, HEIGHT`. Coordinates should be in decimal degrees, height in meters above mean sea level.
#### NOTE
The height value for interface discovery is specified in *height above mean sea level*! This is the geoid-corrected height value, and is distinct from GPS altitude. If you manually specify height, it will typically be set to what you find on a topographical map. If you are using a script to output the location data, make sure that you are using the geoid-corrected altitude, not height above the GPS ellipsoid. Most GPS systems will make both figures available.
**Radio Parameters** **Radio Parameters**
+28 -32
View File
@@ -965,39 +965,35 @@ positional arguments:
destination hexadecimal hash of the destination to connect to destination hexadecimal hash of the destination to connect to
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
--config, -c CONFIG path to alternative Reticulum config directory --config, -c PATH path to config directory
--identity, -i IDENTITY --rnsconfig, -c PATH path to alternative Reticulum config directory
path to identity file to use --identity, -i IDENTITY path to identity file to use
-v, --verbose increase verbosity -v, --verbose increase verbosity
-q, --quiet decrease verbosity -q, --quiet decrease verbosity
-p, --print-identity print identity and destination info and exit -p, --print-identity print identity and destination info and exit
--version show program's version number and exit --version show program's version number and exit
-l, --listen listen (server) mode; any command specified after -- -l, --listen listen (server) mode; any command specified after --
will be used as the default command when the initiator will be used as the default command when the initiator
does not provide one or when remote command execution does not provide one or when remote command execution
is disabled; if no command is specified, the default is disabled; if no command is specified, the default
shell of the user running rnsh will be used shell of the user running rnsh will be used
-s, --service SERVICE -s, --service SERVICE service name for identity file if not the default
service name for identity file if not the default -b, --announce PERIOD announce on startup and every PERIOD seconds; specify
-b, --announce PERIOD 0 to announce on startup only
announce on startup and every PERIOD seconds; specify -a, --allowed HASH allow this identity to connect (may be specified
0 to announce on startup only multiple times); allowed identities can also be
-a, --allowed HASH allow this identity to connect (may be specified specified in ~/.rnsh/allowed_identities or
multiple times); allowed identities can also be ~/.config/rnsh/allowed_identities, one hash per line
specified in ~/.rnsh/allowed_identities or -n, --no-auth disable authentication (allow any identity to connect)
~/.config/rnsh/allowed_identities, one hash per line
-n, --no-auth disable authentication (allow any identity to connect)
-A, --remote-command-as-args -A, --remote-command-as-args
concatenate remote command to the argument list of the concatenate remote command to the argument list of the
default program or shell default program or shell
-C, --no-remote-command -C, --no-remote-command disable executing command lines received from the
disable executing command lines received from the remote initiator
remote initiator -N, --no-id disable identity announcement on connect
-N, --no-id disable identity announcement on connect -m, --mirror return with the exit code of the remote process
-m, --mirror return with the exit code of the remote process -w, --timeout SECONDS connect and request timeout in seconds
-w, --timeout SECONDS
connect and request timeout in seconds
When specifying a command to execute, separate rnsh options from the command When specifying a command to execute, separate rnsh options from the command
and its arguments with --. For example: and its arguments with --. For example:
+52 -1
View File
@@ -230,6 +230,51 @@ specify the target Yggdrasil IPv6 address and port, like so:
target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5 target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5
target_port = 4343 target_port = 4343
Automated Blocking
------------------
Listener instances of ``BackboneInterface`` will automatically block fast-flapping clients,
that repeatedly connect for a short amount of time, and then disconnect. Such behavior usually
occurs from clients trying to spam the network with announces or path requests, by connecting, dumping
a massive amount of requests, and then disconnecting in an attempt to bypass rate limits.
While such bypass attempts only have very limited effect on the amount of spam actually dumped (ingress limits
trigger immediately once a client exceeds rate limits), the behavior is often seen anyways, and
causes log noise and needless interface rotation. The fast-flapping block ensures such clients
are never allocated an interface on the transport instance.
Another common cause can simply be clients using implementations that are broken, or automated
scanning tools attempting to connect to the instance.
.. code:: ini
[[Backbone Listener]]
type = BackboneInterface
enabled = yes
listen_on = 0.0.0.0
port = 4242
# Whether to enable blocking
block_fast_flapping = yes
# How long an IP address stays
# blocked, in minutes. Set to
# 12 hours by default.
fast_flapping_block_time = 720
# The minimum time, in seconds,
# an interface must stay connected
# to be considered not fast-flapping.
fast_flapping_threshold = 20
# Amount of fast flaps a remote
# IP can perform before having
# blocking triggered.
fast_flapping_grace = 5
The configuration options listed in the example above are the *default values*, and you do
not need to add them for automated blocking to work, but you can use them to change the
behavior from the defaults, if necessary.
.. _interfaces-tcps: .. _interfaces-tcps:
TCP Server Interface TCP Server Interface
@@ -998,7 +1043,13 @@ On a real system, you should make the script robust enough to deal with intermit
**Physical Location** **Physical Location**
``latitude``, ``longitude``, ``height`` ``latitude``, ``longitude``, ``height``
Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters. Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters above mean sea level.
``location_cmd``
Optional path to executable or script that returns the physical coordinates for the interface. This can be used instead of manually setting ``latitude``, ``longitude`` and ``height``. Reticulum expects the script to output the location data to ``stdout`` on a single line, separated by commas, with values as floating point numbers in the format ``LAT, LON, HEIGHT``. Coordinates should be in decimal degrees, height in meters above mean sea level.
.. note::
The height value for interface discovery is specified in *height above mean sea level*! This is the geoid-corrected height value, and is distinct from GPS altitude. If you manually specify height, it will typically be set to what you find on a topographical map. If you are using a script to output the location data, make sure that you are using the geoid-corrected altitude, not height above the GPS ellipsoid. Most GPS systems will make both figures available.
**Radio Parameters** **Radio Parameters**
+28 -32
View File
@@ -1002,39 +1002,35 @@ available. These are only recognised immediately after a newline character:
destination hexadecimal hash of the destination to connect to destination hexadecimal hash of the destination to connect to
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
--config, -c CONFIG path to alternative Reticulum config directory --config, -c PATH path to config directory
--identity, -i IDENTITY --rnsconfig, -c PATH path to alternative Reticulum config directory
path to identity file to use --identity, -i IDENTITY path to identity file to use
-v, --verbose increase verbosity -v, --verbose increase verbosity
-q, --quiet decrease verbosity -q, --quiet decrease verbosity
-p, --print-identity print identity and destination info and exit -p, --print-identity print identity and destination info and exit
--version show program's version number and exit --version show program's version number and exit
-l, --listen listen (server) mode; any command specified after -- -l, --listen listen (server) mode; any command specified after --
will be used as the default command when the initiator will be used as the default command when the initiator
does not provide one or when remote command execution does not provide one or when remote command execution
is disabled; if no command is specified, the default is disabled; if no command is specified, the default
shell of the user running rnsh will be used shell of the user running rnsh will be used
-s, --service SERVICE -s, --service SERVICE service name for identity file if not the default
service name for identity file if not the default -b, --announce PERIOD announce on startup and every PERIOD seconds; specify
-b, --announce PERIOD 0 to announce on startup only
announce on startup and every PERIOD seconds; specify -a, --allowed HASH allow this identity to connect (may be specified
0 to announce on startup only multiple times); allowed identities can also be
-a, --allowed HASH allow this identity to connect (may be specified specified in ~/.rnsh/allowed_identities or
multiple times); allowed identities can also be ~/.config/rnsh/allowed_identities, one hash per line
specified in ~/.rnsh/allowed_identities or -n, --no-auth disable authentication (allow any identity to connect)
~/.config/rnsh/allowed_identities, one hash per line
-n, --no-auth disable authentication (allow any identity to connect)
-A, --remote-command-as-args -A, --remote-command-as-args
concatenate remote command to the argument list of the concatenate remote command to the argument list of the
default program or shell default program or shell
-C, --no-remote-command -C, --no-remote-command disable executing command lines received from the
disable executing command lines received from the remote initiator
remote initiator -N, --no-id disable identity announcement on connect
-N, --no-id disable identity announcement on connect -m, --mirror return with the exit code of the remote process
-m, --mirror return with the exit code of the remote process -w, --timeout SECONDS connect and request timeout in seconds
-w, --timeout SECONDS
connect and request timeout in seconds
When specifying a command to execute, separate rnsh options from the command When specifying a command to execute, separate rnsh options from the command
and its arguments with --. For example: and its arguments with --. For example: