Compare commits

...

15 Commits

Author SHA1 Message Date
Mark Qvist 7fdac2118b Prepare release 2026-04-18 16:07:38 +02:00
Mark Qvist 1dbf78ed71 Updated changelog 2026-04-18 16:06:14 +02:00
Mark Qvist c9101a0c21 Ensure loop-originating closures have variables captured at iteration-time. Thanks @taprootmx! 2026-04-18 15:36:33 +02:00
Mark Qvist 2e6264c04b Updated changelog 2026-04-18 15:24:29 +02:00
Mark Qvist e0aa46ba22 Improved gracious transport data persist handling 2026-04-18 14:50:45 +02:00
Mark Qvist 8093c3cd2c Added local destinations lookup map 2026-04-17 11:39:14 +02:00
Mark Qvist c6778e4e29 Improved transport tunnel handling. Improved memory consumption. Fixed disk I/O bound thread execution time starvation on cache management jobs. 2026-04-17 00:07:07 +02:00
Mark Qvist c77548d299 Updated docs 2026-04-15 18:54:54 +02:00
Mark Qvist 26d435ea64 Updated version 2026-04-15 18:48:59 +02:00
Mark Qvist c3f0d98e41 Refactoring work for free-threaded transport I/O. Added ingress control bypass on pending path requests. 2026-04-15 18:48:17 +02:00
Mark Qvist 3c50f4aee9 Updated logging 2026-04-15 12:06:15 +02:00
Mark Qvist 4a930ba82a Fixed invalid EPOLL modification error handler 2026-04-15 12:04:26 +02:00
Mark Qvist 866e63f0fe Apply patch from K8: Fix IFAC for autoconnected, discovered interfaces. 2026-04-15 10:37:41 +02:00
Mark Qvist d461cfa8ce Updated manual 2026-04-15 10:32:41 +02:00
Mark Qvist 18708636fb Updated manual 2026-04-13 20:38:55 +02:00
33 changed files with 482 additions and 334 deletions
+28
View File
@@ -1,3 +1,29 @@
### 2026-04-18: RNS 1.1.6
**Changes**
- Improved transport memory consumption.
- Improved transport tunnel handling.
- Improved gracious transport data persist handling.
- Added ingress control bypass for pending path requests.
- Added local destinations lookup map for better transport efficiency to local destinations.
- Fixed disk I/O bound thread execution time starvation on cache management jobs.
- Fixed invalid EPOLL modification error handler.
- Fixed incorrect default IFAC size for autoconnected, discovered interfaces. Thanks @taprootmx!
- Ensure loop-originating closures have variables captured at iteration-time. Thanks @taprootmx!
**Release Hashes**
```
2ce4451668f8c464295cc269188c232e7805ddd618ec0135550a5e6809df5de0 rns-1.1.6-py3-none-any.whl
ba3e541e69a2f4892177383c8ec4e7d172d298546317e08270928c0163865aa3 rnspure-1.1.6-py3-none-any.wh
```
**Release Signatures**
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`:
```sh
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.6-py3-none-any.whl.rsg
```
### 2026-04-13: RNS 1.1.5
**Changes**
@@ -21,6 +47,8 @@ Release artifacts include `rsg` signature files that can be validated against th
rnid -i bc7291552be7a58f361522990465165c -V rns-1.1.5-py3-none-any.whl.rsg
```
#
### 2026-03-12: RNS 1.1.4
**Changes**
+15 -23
View File
@@ -295,33 +295,26 @@ class Destination:
app_data = returned_app_data
signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash+ratchet
if app_data != None:
signed_data += app_data
if app_data != None: signed_data += app_data
signature = self.identity.sign(signed_data)
announce_data = self.identity.get_public_key()+self.name_hash+random_hash+ratchet+signature
if app_data != None:
announce_data += app_data
if app_data != None: announce_data += app_data
self.path_responses[tag] = [time.time(), announce_data]
if path_response:
announce_context = RNS.Packet.PATH_RESPONSE
else:
announce_context = RNS.Packet.NONE
if path_response: announce_context = RNS.Packet.PATH_RESPONSE
else: announce_context = RNS.Packet.NONE
if ratchet:
context_flag = RNS.Packet.FLAG_SET
else:
context_flag = RNS.Packet.FLAG_UNSET
if ratchet: context_flag = RNS.Packet.FLAG_SET
else: context_flag = RNS.Packet.FLAG_UNSET
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context,
attached_interface = attached_interface, context_flag=context_flag)
if send:
announce_packet.send()
else:
return announce_packet
if send: announce_packet.send()
else: return announce_packet
def accepts_links(self, accepts = None):
"""
@@ -330,13 +323,10 @@ class Destination:
:param accepts: If ``True`` or ``False``, this method sets whether the destination accepts incoming link requests. If not provided or ``None``, the method returns whether the destination currently accepts link requests.
:returns: ``True`` or ``False`` depending on whether the destination accepts incoming link requests, if the *accepts* parameter is not provided or ``None``.
"""
if accepts == None:
return self.accept_link_requests
if accepts == None: return self.accept_link_requests
if accepts:
self.accept_link_requests = True
else:
self.accept_link_requests = False
if accepts: self.accept_link_requests = True
else: self.accept_link_requests = False
def set_link_established_callback(self, callback):
"""
@@ -421,7 +411,9 @@ class Destination:
else:
if packet.packet_type == RNS.Packet.DATA:
if self.callbacks.packet != None:
try: self.callbacks.packet(plaintext, packet)
try:
def job(): self.callbacks.packet(plaintext, packet)
threading.Thread(target=job, daemon=True).start()
except Exception as e:
RNS.log("Error while executing receive callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
+3 -3
View File
@@ -160,7 +160,7 @@ class Identity:
return None
@staticmethod
def save_known_destinations():
def save_known_destinations(background=False):
# TODO: Improve the storage method so we don't have to
# deserialize and serialize the entire table on every
# save, but the only changes. It might be possible to
@@ -491,9 +491,9 @@ class Identity:
return False
@staticmethod
def persist_data():
def persist_data(background=False):
if not RNS.Transport.owner.is_connected_to_shared_instance:
Identity.save_known_destinations()
Identity.save_known_destinations(background=background)
@staticmethod
def exit_handler():
+4 -5
View File
@@ -228,10 +228,10 @@ class BackboneInterface(Interface):
if interface.socket:
fileno = interface.socket.fileno()
if fileno in BackboneInterface.spawned_interface_filenos:
try:
BackboneInterface.epoll.modify(interface.socket.fileno(), select.EPOLLOUT)
try: BackboneInterface.epoll.modify(fileno, select.EPOLLOUT)
except Exception as e:
RNS.trace_exception(e)
RNS.log(f"Error occurred on {interface} while modifying socket EPOLL state: {e}", RNS.LOG_WARNING)
raise e
@staticmethod
def __job():
@@ -270,8 +270,7 @@ class BackboneInterface(Interface):
spawned_interface.receive(received_bytes)
elif client_socket and fileno == client_socket.fileno() and (event & select.EPOLLOUT):
try:
written = client_socket.send(spawned_interface.transmit_buffer)
try: written = client_socket.send(spawned_interface.transmit_buffer)
except Exception as e:
written = 0
if not spawned_interface.detached: RNS.log(f"Error while writing to {spawned_interface}: {e}", RNS.LOG_DEBUG)
+10 -9
View File
@@ -55,8 +55,8 @@ class Interface:
# How many samples to use for announce
# frequency calculations
IA_FREQ_SAMPLES = 12
OA_FREQ_SAMPLES = 12
IA_FREQ_SAMPLES = 128
OA_FREQ_SAMPLES = 128
# Maximum amount of ingress limited announces
# to hold at any given time.
@@ -66,12 +66,12 @@ class Interface:
# considered to be newly created. Two
# hours by default.
IC_NEW_TIME = 2*60*60
IC_BURST_FREQ_NEW = 3.5
IC_BURST_FREQ = 12
IC_BURST_FREQ_NEW = 6
IC_BURST_FREQ = 35
IC_BURST_HOLD = 1*60
IC_BURST_PENALTY = 5*60
IC_HELD_RELEASE_INTERVAL = 30
IC_DEQUE_MIN_SAMPLE = 8
IC_BURST_PENALTY = 15
IC_HELD_RELEASE_INTERVAL = 2
IC_DEQUE_MIN_SAMPLE = 32
AUTOCONFIGURE_MTU = False
FIXED_MTU = False
@@ -123,13 +123,14 @@ class Interface:
if self.ic_burst_active:
if ia_freq < freq_threshold and time.time() > self.ic_burst_activated+self.ic_burst_hold:
self.ic_burst_active = False
self.ic_held_release = time.time() + self.ic_burst_penalty
return True
else:
if ia_freq > freq_threshold:
self.ic_burst_active = True
self.ic_burst_activated = time.time()
self.ic_held_release = time.time() + self.ic_burst_penalty
return True
else: return False
@@ -174,7 +175,7 @@ class Interface:
def process_held_announces(self):
try:
if not self.should_ingress_limit() and len(self.held_announces) > 0 and time.time() > self.ic_held_release:
if len(self.held_announces) > 0 and time.time() > self.ic_held_release:
freq_threshold = self.ic_burst_freq_new if self.age() < self.ic_new_time else self.ic_burst_freq
ia_freq = self.incoming_announce_frequency()
if ia_freq < freq_threshold:
+2 -1
View File
@@ -328,7 +328,8 @@ class LocalClientInterface(Interface):
if hasattr(self, "parent_interface") and self.parent_interface != None:
self.parent_interface.clients -= 1
if hasattr(RNS.Transport, "owner") and RNS.Transport.owner != None:
RNS.Transport.owner._should_persist_data()
background = not self.detached
RNS.Transport.owner._should_persist_data(background=background)
if nowarning == False:
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)
+5 -9
View File
@@ -722,12 +722,9 @@ class Link:
pass
def link_closed(self):
for resource in self.incoming_resources:
resource.cancel()
for resource in self.outgoing_resources:
resource.cancel()
if self._channel:
self._channel._shutdown()
for resource in self.incoming_resources: resource.cancel()
for resource in self.outgoing_resources: resource.cancel()
if self._channel: self._channel._shutdown()
self.prv = None
self.pub = None
@@ -741,8 +738,7 @@ class Link:
self.destination.links.remove(self)
if self.callbacks.link_closed != None:
try:
self.callbacks.link_closed(self)
try: self.callbacks.link_closed(self)
except Exception as e:
RNS.log("Error while executing link closed callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
@@ -1181,7 +1177,7 @@ class Link:
resource_hash = packet.data[0:RNS.Identity.HASHLENGTH//8]
for resource in self.outgoing_resources:
if resource_hash == resource.hash:
def job(): resource.validate_proof(packet.data)
def job(resource=resource): resource.validate_proof(packet.data)
threading.Thread(target=job, daemon=True).start()
self.__update_phy_stats(packet, query_shared=True)
+2 -2
View File
@@ -293,7 +293,7 @@ class Packet:
if RNS.Transport.outbound(self): return self.receipt
else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
RNS.log("No interfaces could process the outbound packet", RNS.LOG_DEBUG)
self.sent = False
self.receipt = None
return False
@@ -315,7 +315,7 @@ class Packet:
if RNS.Transport.outbound(self):
return self.receipt
else:
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
RNS.log("Re-send failed. No interfaces could process the outbound packet", RNS.LOG_WARNING)
self.sent = False
self.receipt = None
return False
+20 -14
View File
@@ -47,6 +47,7 @@ else:
from RNS.Interfaces import *
from RNS.vendor.configobj import ConfigObj
from threading import Lock
import configparser
import multiprocessing.connection
import importlib.util
@@ -171,6 +172,8 @@ class Reticulum:
cachepath = ""
interfacepath = ""
gracious_persist_lock = Lock()
__instance = None
__interface_detach_ran = False
@@ -361,11 +364,11 @@ class Reticulum:
now = time.time()
if now > self.last_cache_clean+Reticulum.CLEAN_INTERVAL:
self.__clean_caches()
self.__clean_caches(background=True)
self.last_cache_clean = time.time()
if now > self.last_data_persist+Reticulum.PERSIST_INTERVAL:
self.__persist_data()
self.__persist_data(background=True)
time.sleep(Reticulum.JOB_INTERVAL)
@@ -960,7 +963,7 @@ class Reticulum:
interface.optimise_mtu()
if ifac_size != None: interface.ifac_size = ifac_size
else: interface.ifac_size = 8
else: interface.ifac_size = interface.DEFAULT_IFAC_SIZE
interface.announce_cap = announce_cap if announce_cap != None else Reticulum.ANNOUNCE_CAP/100.0
interface.announce_rate_target = announce_rate_target
@@ -993,16 +996,19 @@ class Reticulum:
RNS.Transport.interfaces.append(interface)
interface.final_init()
def _should_persist_data(self):
def _should_persist_data(self, background=False):
if time.time() > self.last_data_persist+Reticulum.GRACIOUS_PERSIST_INTERVAL:
self.__persist_data()
def job(): self.__persist_data(background=background)
threading.Thread(target=job, daemon=True).start()
def __persist_data(self):
RNS.Transport.persist_data()
RNS.Identity.persist_data()
self.last_data_persist = time.time()
def __persist_data(self, background=False):
if Reticulum.gracious_persist_lock.locked(): return
with Reticulum.gracious_persist_lock:
RNS.Transport.persist_data(background=background)
RNS.Identity.persist_data(background=background)
self.last_data_persist = time.time()
def __clean_caches(self):
def __clean_caches(self, background=False):
RNS.log("Cleaning resource and packet caches...", RNS.LOG_EXTREME)
now = time.time()
@@ -1013,8 +1019,8 @@ class Reticulum:
filepath = self.resourcepath + "/" + filename
mtime = os.path.getmtime(filepath)
age = now - mtime
if age > Reticulum.RESOURCE_CACHE:
os.unlink(filepath)
if age > Reticulum.RESOURCE_CACHE: os.unlink(filepath)
if background: time.sleep(0.001)
except Exception as e:
RNS.log("Error while cleaning resources cache, the contained exception was: "+str(e), RNS.LOG_ERROR)
@@ -1026,8 +1032,8 @@ class Reticulum:
filepath = self.cachepath + "/" + filename
mtime = os.path.getmtime(filepath)
age = now - mtime
if age > RNS.Transport.DESTINATION_TIMEOUT:
os.unlink(filepath)
if age > RNS.Transport.DESTINATION_TIMEOUT: os.unlink(filepath)
if background: time.sleep(0.001)
except Exception as e:
RNS.log("Error while cleaning resources cache, the contained exception was: "+str(e), RNS.LOG_ERROR)
+322 -197
View File
@@ -87,6 +87,8 @@ class Transport:
LINK_TIMEOUT = RNS.Link.STALE_TIME * 1.25
REVERSE_TIMEOUT = 8*60 # Reverse table entries are removed after 8 minutes
DESTINATION_TIMEOUT = 60*60*24*7 # Destination table entries are removed if unused for one week
TUNNEL_TIMEOUT = 60*60*8 # Tunnel table entries are removed if unused for eight hours
TUNNEL_PATH_TIMEOUT = 60*60*8 # Tunnel path table entries are removed if unused for eight hours
MAX_RECEIPTS = 1024 # Maximum number of receipts to keep track of
MAX_RATE_TIMESTAMPS = 16 # Maximum number of announce timestamps to keep per destination
PERSIST_RANDOM_BLOBS = 32 # Maximum number of random blobs per destination to persist to disk
@@ -94,6 +96,7 @@ class Transport:
interfaces = [] # All active interfaces
destinations = [] # All active destinations
destinations_map = {} # Destination hash map of active destinations
pending_links = [] # Links that are being established
active_links = [] # Links that are active
packet_hashlist = set() # A list of packet hashes for duplicate detection
@@ -119,7 +122,9 @@ class Transport:
discovery_pr_tags = [] # A table for keeping track of tagged path requests
max_pr_tags = 32000 # Maximum amount of unique path request tags to remember
interfaces_lock = Lock()
destinations_lock = Lock()
destinations_map_lock = Lock()
inbound_announce_lock = Lock()
announce_table_lock = Lock()
announce_rate_table_lock = Lock()
@@ -137,6 +142,7 @@ class Transport:
pending_local_prs_lock = Lock()
path_states_lock = Lock()
jobs_lock = Lock()
cache_clean_lock = Lock()
# Transport control destinations are used
# for control purposes like path requests
@@ -302,7 +308,8 @@ class Transport:
# over an interface. It is cached with it's non-
# increased hop-count.
announce_packet.hops += 1
Transport.path_table[destination_hash] = [timestamp, received_from, hops, expires, random_blobs, receiving_interface, announce_packet.packet_hash]
with Transport.path_table_lock:
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)
else:
RNS.log("Could not reconstruct path table entry from storage for "+RNS.prettyhexrep(destination_hash), RNS.LOG_DEBUG)
@@ -360,10 +367,10 @@ class Transport:
if len(tunnel_paths) > 0:
tunnel = [tunnel_id, None, tunnel_paths, expires]
Transport.tunnels[tunnel_id] = tunnel
with Transport.tunnels_lock: Transport.tunnels[tunnel_id] = tunnel
if len(Transport.path_table) == 1: specifier = "entry"
else: specifier = "entries"
if len(Transport.tunnels) == 1: specifier = "entry"
else: specifier = "entries"
RNS.log("Loaded "+str(len(Transport.tunnels))+" tunnel table "+specifier+" from storage", RNS.LOG_VERBOSE)
gc.collect()
@@ -407,8 +414,9 @@ class Transport:
@staticmethod
def prioritize_interfaces():
try: Transport.interfaces.sort(key=lambda interface: interface.bitrate, reverse=True)
except Exception as e: RNS.log(f"Could not prioritize interfaces according to bitrate. The contained exception was: {e}", RNS.LOG_ERROR)
with Transport.interfaces_lock:
try: Transport.interfaces.sort(key=lambda interface: interface.bitrate, reverse=True)
except Exception as e: RNS.log(f"Could not prioritize interfaces according to bitrate. The contained exception was: {e}", RNS.LOG_ERROR)
@staticmethod
def enable_discovery():
@@ -607,10 +615,14 @@ class Transport:
# Cull invalidated path requests
if time.time() > Transport.pending_prs_last_checked+Transport.pending_prs_check_interval:
stale_local_prs = []
with Transport.pending_local_prs_lock:
for destination_hash in Transport.pending_local_path_requests:
if not Transport.pending_local_path_requests[destination_hash] in Transport.interfaces:
Transport.pending_local_path_requests.pop(destination_hash)
stale_local_prs.append(destination_hash)
for destination_hash in stale_local_prs:
Transport.pending_local_path_requests.pop(destination_hash)
Transport.pending_prs_last_checked = time.time()
@@ -762,9 +774,12 @@ class Transport:
tunnel_entry = Transport.tunnels[tunnel_id]
expires = tunnel_entry[IDX_TT_EXPIRES]
if time.time() > expires:
stale_tunnels.append(tunnel_id)
should_collect = True
if expires > time.time() + Transport.TUNNEL_TIMEOUT*2:
stale_tunnels.append(tunnel_id); should_collect = True
RNS.log("Tunnel "+RNS.prettyhexrep(tunnel_id)+" with excessive expiry was removed", RNS.LOG_EXTREME)
elif time.time() > expires:
stale_tunnels.append(tunnel_id); should_collect = True
RNS.log("Tunnel "+RNS.prettyhexrep(tunnel_id)+" timed out and was removed", RNS.LOG_EXTREME)
else:
@@ -777,11 +792,25 @@ class Transport:
for tunnel_path in tunnel_paths:
tunnel_path_entry = tunnel_paths[tunnel_path]
if time.time() > tunnel_path_entry[0] + Transport.DESTINATION_TIMEOUT:
stale_tunnel_paths.append(tunnel_path)
should_collect = True
if time.time() > tunnel_path_entry[0] + Transport.TUNNEL_PATH_TIMEOUT:
stale_tunnel_paths.append(tunnel_path); should_collect = True
RNS.log("Tunnel path to "+RNS.prettyhexrep(tunnel_path)+" timed out and was removed", RNS.LOG_EXTREME)
else:
active_path = None
with Transport.path_table_lock:
if tunnel_path in Transport.path_table: active_path = Transport.path_table[tunnel_path]
if active_path:
random_blobs = tunnel_path_entry[4]
current_random_blobs = active_path[IDX_PT_RANDBLOBS]
current_path_timebase = Transport.timebase_from_random_blobs(current_random_blobs)
tunnel_announce_timebase = Transport.timebase_from_random_blobs(random_blobs)
if current_path_timebase > tunnel_announce_timebase:
stale_tunnel_paths.append(tunnel_path); should_collect = True
RNS.log("Tunnel path to "+RNS.prettyhexrep(tunnel_path)+" was removed due to more recent active path", RNS.LOG_EXTREME)
for tunnel_path in stale_tunnel_paths:
tunnel_paths.pop(tunnel_path)
ti += 1
@@ -864,7 +893,9 @@ class Transport:
# Clean packet caches
if time.time() > Transport.cache_last_cleaned+Transport.cache_clean_interval:
Transport.clean_cache()
Transport.cache_last_cleaned = time.time()
def job(): Transport.clean_cache()
threading.Thread(target=job, daemon=True).start()
# Send announces for management destinations
if time.time() > Transport.last_mgmt_announce+Transport.mgmt_announce_interval:
@@ -908,6 +939,7 @@ class Transport:
except Exception as e:
RNS.log("An exception occurred while running Transport jobs.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
RNS.trace_exception(e) # TODO: Remove
for packet in outgoing: packet.send()
@@ -1068,8 +1100,10 @@ class Transport:
should_transmit = False
elif interface.mode == RNS.Interfaces.Interface.Interface.MODE_ROAMING:
with Transport.destinations_lock:
local_destination = next((d for d in Transport.destinations if d.hash == packet.destination_hash), None)
local_destination = None
with Transport.destinations_map_lock:
if packet.destination_hash in Transport.destinations_map:
local_destination = Transport.destinations_map[packet.destination_hash]
if local_destination != None:
# RNS.log("Allowing announce broadcast on roaming-mode interface from instance-local destination", RNS.LOG_EXTREME)
@@ -1091,8 +1125,11 @@ class Transport:
should_transmit = False
elif interface.mode == RNS.Interfaces.Interface.Interface.MODE_BOUNDARY:
with Transport.destinations_lock:
local_destination = next((d for d in Transport.destinations if d.hash == packet.destination_hash), None)
local_destination = None
with Transport.destinations_map_lock:
if packet.destination_hash in Transport.destinations_map:
local_destination = Transport.destinations_map[packet.destination_hash]
if local_destination != None:
# RNS.log("Allowing announce broadcast on boundary-mode interface from instance-local destination", RNS.LOG_EXTREME)
pass
@@ -1553,12 +1590,19 @@ class Transport:
# potential ingress limiting. Already known
# destinations will have re-announces controlled
# by normal announce rate limiting.
if interface.should_ingress_limit():
if packet.destination_hash in Transport.path_requests or packet.destination_hash in Transport.discovery_path_requests:
# RNS.log(f"Skipping ingress limit check for {RNS.prettyhexrep(packet.destination_hash)} due to waiting path requests", RNS.LOG_DEBUG)
pass
elif interface.should_ingress_limit():
interface.hold_announce(packet)
return
with Transport.destinations_lock:
local_destination = next((d for d in Transport.destinations if d.hash == packet.destination_hash), None)
local_destination = None
with Transport.destinations_map_lock:
if packet.destination_hash in Transport.destinations_map:
local_destination = Transport.destinations_map[packet.destination_hash]
if local_destination == None and RNS.Identity.validate_announce(packet):
if packet.transport_id != None:
received_from = packet.transport_id
@@ -1575,14 +1619,15 @@ class Transport:
if announce_entry[IDX_AT_RETRIES] > 0:
if announce_entry[IDX_AT_LCL_RBRD] >= Transport.LOCAL_REBROADCASTS_MAX:
RNS.log("Completed announce processing for "+RNS.prettyhexrep(packet.destination_hash)+", local rebroadcast limit reached", RNS.LOG_EXTREME)
if packet.destination_hash in Transport.announce_table: Transport.announce_table.pop(packet.destination_hash)
with Transport.announce_table_lock:
if packet.destination_hash in Transport.announce_table: Transport.announce_table.pop(packet.destination_hash)
if packet.hops-1 == announce_entry[IDX_AT_HOPS]+1 and announce_entry[IDX_AT_RETRIES] > 0:
now = time.time()
if now < announce_entry[IDX_AT_RTRNS_TMO]:
RNS.log("Rebroadcasted announce for "+RNS.prettyhexrep(packet.destination_hash)+" has been passed on to another node, no further tries needed", RNS.LOG_EXTREME)
if packet.destination_hash in Transport.announce_table:
Transport.announce_table.pop(packet.destination_hash)
with Transport.announce_table_lock:
if packet.destination_hash in Transport.announce_table: Transport.announce_table.pop(packet.destination_hash)
else:
received_from = packet.destination_hash
@@ -1593,7 +1638,9 @@ class Transport:
# First, check that the announce is not for a destination
# local to this system, and that hops are less than the max
with Transport.destinations_lock: local_and_hops_condition = (not any(packet.destination_hash == d.hash for d in Transport.destinations) and packet.hops < Transport.PATHFINDER_M+1)
with Transport.destinations_map_lock:
local_and_hops_condition = (packet.hops < Transport.PATHFINDER_M+1) and (not packet.destination_hash in Transport.destinations_map)
if local_and_hops_condition:
announce_emitted = Transport.announce_emitted(packet)
@@ -1706,9 +1753,7 @@ class Transport:
else:
rate_entry["last"] = now
else:
rate_blocked = True
else: rate_blocked = True
retries = 0
announce_hops = packet.hops
@@ -1864,12 +1909,15 @@ class Transport:
# announce to the tunnels table
if hasattr(packet.receiving_interface, "tunnel_id") and packet.receiving_interface.tunnel_id != None:
with Transport.tunnels_lock:
tunnel_entry = Transport.tunnels[packet.receiving_interface.tunnel_id]
paths = tunnel_entry[IDX_TT_PATHS]
paths[packet.destination_hash] = [now, received_from, announce_hops, expires, random_blobs, None, packet.packet_hash]
expires = time.time() + Transport.DESTINATION_TIMEOUT
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 not packet.receiving_interface.tunnel_id in Transport.tunnels:
RNS.log(f"Tunnel ID for {packet.receiving_interface} was not found in tunnel table", RNS.LOG_WARNING)
else:
tunnel_entry = Transport.tunnels[packet.receiving_interface.tunnel_id]
paths = tunnel_entry[IDX_TT_PATHS]
paths[packet.destination_hash] = [now, received_from, announce_hops, expires, random_blobs, None, packet.packet_hash]
expires = time.time() + Transport.TUNNEL_TIMEOUT
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)
# Call externally registered callbacks from apps
# wanting to know when an announce arrives
@@ -1896,14 +1944,14 @@ class Transport:
if execute_callback:
if len(inspect.signature(handler.received_announce).parameters) == 3:
def job():
def job(handler=handler, packet=packet, announce_identity=announce_identity):
handler.received_announce(destination_hash=packet.destination_hash,
announced_identity=announce_identity,
app_data=RNS.Identity.recall_app_data(packet.destination_hash))
threading.Thread(target=job, daemon=True).start()
elif len(inspect.signature(handler.received_announce).parameters) == 4:
def job():
def job(handler=handler, packet=packet, announce_identity=announce_identity):
handler.received_announce(destination_hash=packet.destination_hash,
announced_identity=announce_identity,
app_data=RNS.Identity.recall_app_data(packet.destination_hash),
@@ -1911,7 +1959,7 @@ class Transport:
threading.Thread(target=job, daemon=True).start()
elif len(inspect.signature(handler.received_announce).parameters) == 5:
def job():
def job(handler=handler, packet=packet, announce_identity=announce_identity):
handler.received_announce(destination_hash=packet.destination_hash,
announced_identity=announce_identity,
app_data=RNS.Identity.recall_app_data(packet.destination_hash),
@@ -1930,73 +1978,81 @@ class Transport:
# Handling for link requests to local destinations
elif packet.packet_type == RNS.Packet.LINKREQUEST:
if packet.transport_id == None or packet.transport_id == Transport.identity.hash:
for destination in Transport.destinations:
if destination.hash == packet.destination_hash and destination.type == packet.destination_type:
path_mtu = RNS.Link.mtu_from_lr_packet(packet)
mode = RNS.Link.mode_from_lr_packet(packet)
if packet.receiving_interface.AUTOCONFIGURE_MTU or packet.receiving_interface.FIXED_MTU:
nh_mtu = packet.receiving_interface.HW_MTU
destination = None
with Transport.destinations_map_lock:
if packet.destination_hash in Transport.destinations_map:
destination = Transport.destinations_map[packet.destination_hash]
if destination and destination.type == packet.destination_type:
path_mtu = RNS.Link.mtu_from_lr_packet(packet)
mode = RNS.Link.mode_from_lr_packet(packet)
if packet.receiving_interface.AUTOCONFIGURE_MTU or packet.receiving_interface.FIXED_MTU:
nh_mtu = packet.receiving_interface.HW_MTU
else:
nh_mtu = RNS.Reticulum.MTU
if path_mtu:
if packet.receiving_interface.HW_MTU == None:
path_mtu = None
packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE]
else:
nh_mtu = RNS.Reticulum.MTU
if nh_mtu < path_mtu:
try:
path_mtu = nh_mtu
clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode)
packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu
except Exception as e:
RNS.log(f"Dropping link request packet to local destination. The contained exception was: {e}", RNS.LOG_WARNING)
return
if path_mtu:
if packet.receiving_interface.HW_MTU == None:
RNS.log(f"No next-hop HW MTU, disabling link MTU upgrade", RNS.LOG_DEBUG) # TODO: Remove debug
path_mtu = None
packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE]
else:
if nh_mtu < path_mtu:
try:
path_mtu = nh_mtu
clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode)
RNS.log(f"Clamping link MTU to {RNS.prettysize(nh_mtu)}", RNS.LOG_DEBUG) # TODO: Remove debug
packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu
except Exception as e:
RNS.log(f"Dropping link request packet to local destination. The contained exception was: {e}", RNS.LOG_WARNING)
return
packet.destination = destination
destination.receive(packet)
packet.destination = destination
destination.receive(packet)
# Handling for local data packets
elif packet.packet_type == RNS.Packet.DATA:
if packet.destination_type == RNS.Destination.LINK:
for link in Transport.active_links:
if link.link_id == packet.destination_hash:
if link.attached_interface == packet.receiving_interface:
packet.link = link
if packet.context == RNS.Packet.CACHE_REQUEST:
cached_packet = Transport.get_cached_packet(packet.data)
if cached_packet != None:
cached_packet.unpack()
RNS.Packet(destination=link, data=cached_packet.data,
packet_type=cached_packet.packet_type, context=cached_packet.context).send()
with Transport.active_links_lock:
for link in Transport.active_links:
if link.link_id == packet.destination_hash:
if link.attached_interface == packet.receiving_interface:
packet.link = link
if packet.context == RNS.Packet.CACHE_REQUEST:
cached_packet = Transport.get_cached_packet(packet.data)
if cached_packet != None:
cached_packet.unpack()
RNS.Packet(destination=link, data=cached_packet.data,
packet_type=cached_packet.packet_type, context=cached_packet.context).send()
else: link.receive(packet)
break
else: link.receive(packet)
else:
# In the strange and rare case that an interface
# is partly malfunctioning, and a link-associated
# packet is being received on an interface that
# has failed sending, and transport has failed over
# to another path, we remove this packet hash from
# the filter hashlist so the link can receive the
# packet when it finally arrives over another path.
while packet.packet_hash in Transport.packet_hashlist:
Transport.packet_hashlist.remove(packet.packet_hash)
else:
# In the strange and rare case that an interface
# is partly malfunctioning, and a link-associated
# packet is being received on an interface that
# has failed sending, and transport has failed over
# to another path, we remove this packet hash from
# the filter hashlist so the link can receive the
# packet when it finally arrives over another path.
while packet.packet_hash in Transport.packet_hashlist:
Transport.packet_hashlist.remove(packet.packet_hash)
else:
for destination in Transport.destinations:
if destination.hash == packet.destination_hash and destination.type == packet.destination_type:
packet.destination = destination
if destination.receive(packet):
if destination.proof_strategy == RNS.Destination.PROVE_ALL: packet.prove()
destination = None
with Transport.destinations_map_lock:
if packet.destination_hash in Transport.destinations_map:
destination = Transport.destinations_map[packet.destination_hash]
elif destination.proof_strategy == RNS.Destination.PROVE_APP:
if destination.callbacks.proof_requested:
try:
if destination.callbacks.proof_requested(packet): packet.prove()
except Exception as e:
RNS.log("Error while executing proof request callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
if destination and destination.type == packet.destination_type:
packet.destination = destination
if destination.receive(packet):
if destination.proof_strategy == RNS.Destination.PROVE_ALL: packet.prove()
elif destination.proof_strategy == RNS.Destination.PROVE_APP:
if destination.callbacks.proof_requested:
try:
if destination.callbacks.proof_requested(packet): packet.prove()
except Exception as e:
RNS.log("Error while executing proof request callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
# Handling for proofs and link-request proofs
elif packet.packet_type == RNS.Packet.PROOF:
@@ -2038,48 +2094,57 @@ class Transport:
RNS.log("Link request proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG)
else:
RNS.log("Received link request proof with hop mismatch, not transporting it", RNS.LOG_DEBUG)
else:
# Check if we can deliver it to a local
# pending link
for link in Transport.pending_links:
if link.link_id == packet.destination_hash:
# We need to also allow an expected hops value of
# PATHFINDER_M, since in some cases, the number of hops
# to the destination will be unknown at link creation
# time. The real chance of this occuring is likely to be
# extremely small, and this allowance could probably
# be discarded without major issues, but it is kept
# for now to ensure backwards compatibility.
# TODO: Probably reset check back to
# if packet.hops == link.expected_hops:
# within one of the next releases
pending_link = None
with Transport.pending_links_lock:
for link in Transport.pending_links:
if link.link_id == packet.destination_hash:
# We need to also allow an expected hops value of
# PATHFINDER_M, since in some cases, the number of hops
# to the destination will be unknown at link creation
# time. The real chance of this occuring is likely to be
# extremely small, and this allowance could probably
# be discarded without major issues, but it is kept
# for now to ensure backwards compatibility.
if packet.hops == link.expected_hops or link.expected_hops == RNS.Transport.PATHFINDER_M:
# Add this packet to the filter hashlist if we
# have determined that it's actually destined
# for this system, and then validate the proof
Transport.add_packet_hash(packet.packet_hash)
link.validate_proof(packet)
# TODO: Probably reset check back to
# if packet.hops == link.expected_hops:
# within one of the next releases
if packet.hops == link.expected_hops or link.expected_hops == RNS.Transport.PATHFINDER_M:
# Add this packet to the filter hashlist if we
# have determined that it's actually destined
# for this system, and then validate the proof
Transport.add_packet_hash(packet.packet_hash)
pending_link = link
break
if pending_link: pending_link.validate_proof(packet)
elif packet.context == RNS.Packet.RESOURCE_PRF:
for link in Transport.active_links:
if link.link_id == packet.destination_hash:
link.receive(packet)
else:
if packet.destination_type == RNS.Destination.LINK:
with Transport.active_links_lock:
for link in Transport.active_links:
if link.link_id == packet.destination_hash:
packet.link = link
link.receive(packet)
break
else:
if packet.destination_type == RNS.Destination.LINK:
with Transport.active_links_lock:
for link in Transport.active_links:
if link.link_id == packet.destination_hash:
packet.link = link
break
if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH:
proof_hash = packet.data[:RNS.Identity.HASHLENGTH//8]
else:
proof_hash = None
if len(packet.data) == RNS.PacketReceipt.EXPL_LENGTH: proof_hash = packet.data[:RNS.Identity.HASHLENGTH//8]
else: proof_hash = None
# Check if this proof needs to be transported
if (RNS.Reticulum.transport_enabled() or from_local_client or proof_for_local_client) and packet.destination_hash in Transport.reverse_table:
reverse_entry = Transport.reverse_table.pop(packet.destination_hash)
with Transport.reverse_table_lock: reverse_entry = Transport.reverse_table.pop(packet.destination_hash)
if packet.receiving_interface == reverse_entry[IDX_RT_OUTB_IF]:
RNS.log("Proof received on correct interface, transporting it via "+str(reverse_entry[IDX_RT_RCVD_IF]), RNS.LOG_EXTREME)
new_raw = packet.raw[0:1]
@@ -2089,20 +2154,21 @@ class Transport:
else:
RNS.log("Proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG)
for receipt in Transport.receipts:
receipt_validated = False
if proof_hash != None:
# Only test validation if hash matches
if receipt.hash == proof_hash:
with Transport.receipts_lock:
for receipt in Transport.receipts:
receipt_validated = False
if proof_hash != None:
# Only test validation if hash matches
if receipt.hash == proof_hash:
receipt_validated = receipt.validate_proof_packet(packet)
else:
# In case of an implicit proof, we have
# to check every single outstanding receipt
receipt_validated = receipt.validate_proof_packet(packet)
else:
# In case of an implicit proof, we have
# to check every single outstanding receipt
receipt_validated = receipt.validate_proof_packet(packet)
if receipt_validated:
if receipt in Transport.receipts:
Transport.receipts.remove(receipt)
if receipt_validated:
if receipt in Transport.receipts:
Transport.receipts.remove(receipt)
@staticmethod
def synthesize_tunnel(interface):
@@ -2151,19 +2217,21 @@ class Transport:
@staticmethod
def void_tunnel_interface(tunnel_id):
if tunnel_id in Transport.tunnels:
RNS.log(f"Voiding tunnel interface {Transport.tunnels[tunnel_id][IDX_TT_IF]}", RNS.LOG_EXTREME)
Transport.tunnels[tunnel_id][IDX_TT_IF] = None
with Transport.tunnels_lock:
if tunnel_id in Transport.tunnels:
RNS.log(f"Voiding tunnel interface {Transport.tunnels[tunnel_id][IDX_TT_IF]}", RNS.LOG_EXTREME)
Transport.tunnels[tunnel_id][IDX_TT_IF] = None
@staticmethod
def handle_tunnel(tunnel_id, interface):
expires = time.time() + Transport.DESTINATION_TIMEOUT
expires = time.time() + Transport.TUNNEL_TIMEOUT
if not tunnel_id in Transport.tunnels:
RNS.log("Tunnel endpoint "+RNS.prettyhexrep(tunnel_id)+" established.", RNS.LOG_DEBUG)
paths = {}
tunnel_entry = [tunnel_id, interface, paths, expires]
interface.tunnel_id = tunnel_id
Transport.tunnels[tunnel_id] = tunnel_entry
with Transport.tunnels_lock:
tunnel_entry = [tunnel_id, interface, paths, expires]
interface.tunnel_id = tunnel_id
Transport.tunnels[tunnel_id] = tunnel_entry
else:
RNS.log("Tunnel endpoint "+RNS.prettyhexrep(tunnel_id)+" reappeared. Restoring paths...", RNS.LOG_DEBUG)
tunnel_entry = Transport.tunnels[tunnel_id]
@@ -2173,36 +2241,64 @@ class Transport:
paths = tunnel_entry[IDX_TT_PATHS]
deprecated_paths = []
for destination_hash, path_entry in paths.items():
received_from = path_entry[1]
announce_hops = path_entry[2]
expires = path_entry[3]
random_blobs = list(set(path_entry[4]))
receiving_interface = interface
packet_hash = path_entry[6]
new_entry = [time.time(), received_from, announce_hops, expires, random_blobs, receiving_interface, packet_hash]
with Transport.tunnels_lock:
for destination_hash, path_entry in paths.items():
received_from = path_entry[1]
announce_hops = path_entry[2]
expires = path_entry[3]
random_blobs = list(set(path_entry[4]))
receiving_interface = interface
packet_hash = path_entry[6]
new_entry = [time.time(), received_from, announce_hops, expires, random_blobs, receiving_interface, packet_hash]
should_add = False
if destination_hash in Transport.path_table:
old_entry = Transport.path_table[destination_hash]
old_hops = old_entry[IDX_PT_HOPS]
old_expires = old_entry[IDX_PT_EXPIRES]
if announce_hops <= old_hops or time.time() > old_expires: should_add = True
else: RNS.log("Did not restore path to "+RNS.prettyhexrep(destination_hash)+" because a newer path with fewer hops exist", RNS.LOG_DEBUG)
else:
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)
should_add = False
with Transport.path_table_lock:
if destination_hash in Transport.path_table:
old_entry = Transport.path_table[destination_hash]
old_hops = old_entry[IDX_PT_HOPS]
old_expires = old_entry[IDX_PT_EXPIRES]
if announce_hops <= old_hops or time.time() > old_expires:
current_random_blobs = Transport.path_table[destination_hash][IDX_PT_RANDBLOBS]
current_path_timebase = Transport.timebase_from_random_blobs(current_random_blobs)
tunnel_announce_timebase = Transport.timebase_from_random_blobs(random_blobs)
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)
else: RNS.log("Did not restore path to "+RNS.prettyhexrep(destination_hash)+" because a newer path with fewer hops exist", RNS.LOG_DEBUG)
else:
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 should_add:
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)
else:
deprecated_paths.append(destination_hash)
if should_add:
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)
else: deprecated_paths.append(destination_hash)
for deprecated_path in deprecated_paths:
RNS.log("Removing path to "+RNS.prettyhexrep(deprecated_path)+" from tunnel "+RNS.prettyhexrep(tunnel_id), RNS.LOG_DEBUG)
paths.pop(deprecated_path)
with Transport.tunnels_lock: paths.pop(deprecated_path)
@staticmethod
def clean_destinations_map():
with Transport.destinations_lock:
for destination in Transport.destinations:
with Transport.destinations_map_lock:
if not destination.hash in Transport.destinations_map:
Transport.destinations_map[destination.hash] = destination
with Transport.destinations_map_lock:
stale_destination_hashes = []
for destination_hash in Transport.destinations_map:
with Transport.destinations_lock:
found = False
for destination in Transport.destinations:
if destination.hash == destination_hash: found = True
if not found: stale_destination_hashes.append(destination_hash)
for destination_hash in stale_destination_hashes:
Transport.destinations_map.pop(destination_hash)
@staticmethod
def register_destination(destination):
@@ -2215,6 +2311,9 @@ class Transport:
Transport.destinations.append(destination)
with Transport.destinations_map_lock:
Transport.destinations_map[destination.hash] = destination
if Transport.owner.is_connected_to_shared_instance:
if destination.type == RNS.Destination.SINGLE:
def job():
@@ -2227,6 +2326,10 @@ class Transport:
with Transport.destinations_lock:
if destination in Transport.destinations: Transport.destinations.remove(destination)
with Transport.destinations_map_lock:
if destination.hash in Transport.destinations_map:
Transport.destinations_map.pop(destination.hash)
@staticmethod
def register_link(link):
RNS.log("Registering link "+str(link), RNS.LOG_EXTREME)
@@ -2295,16 +2398,30 @@ class Transport:
@staticmethod
def clean_cache():
if not Transport.owner.is_connected_to_shared_instance:
Transport.clean_announce_cache()
Transport.cache_last_cleaned = time.time()
if Transport.cache_clean_lock.locked():
RNS.log(f"Cache clean job still running, postponing until next scheduler interval", RNS.LOG_VERBOSE)
else:
try:
acquired_lock = Transport.cache_clean_lock.acquire(blocking=False)
if acquired_lock:
Transport.clean_announce_cache()
Transport.cache_last_cleaned = time.time()
except Exception as e:
RNS.log(f"An error occurred while launching the cache clean job. The contained exception was: {e}", RNS.LOG_ERROR)
RNS.trace_exception(e)
finally:
if acquired_lock: Transport.cache_clean_lock.release()
@staticmethod
def clean_announce_cache():
st = time.time()
target_path = os.path.join(RNS.Reticulum.cachepath, "announces")
active_paths = [Transport.path_table[dst_hash][6] for dst_hash in Transport.path_table]
tunnel_paths = list(set([path_dict[dst_hash][6] for path_dict in [Transport.tunnels[tunnel_id][2] for tunnel_id in Transport.tunnels] for dst_hash in path_dict]))
removed = 0
with Transport.path_table_lock: active_paths = [Transport.path_table[dst_hash][6] for dst_hash in Transport.path_table]
with Transport.tunnels_lock: tunnel_paths = list(set([path_dict[dst_hash][6] for path_dict in [Transport.tunnels[tunnel_id][2] for tunnel_id in Transport.tunnels] for dst_hash in path_dict]))
removed = 0; total = 0
for packet_hash in os.listdir(target_path):
remove = False
full_path = os.path.join(target_path, packet_hash)
@@ -2313,9 +2430,12 @@ class Transport:
except: remove = True
if (not target_hash in active_paths) and (not target_hash in tunnel_paths): remove = True
if remove: os.unlink(full_path); removed += 1
total += 1
if removed > 0:
RNS.log(f"Removed {removed} cached announces in {RNS.prettytime(time.time()-st)}", RNS.LOG_DEBUG)
# Low priority, yield thread
time.sleep(0.001)
if removed > 0: RNS.log(f"Removed {removed} cached announces in {RNS.prettytime(time.time()-st)}", RNS.LOG_DEBUG)
# When caching packets to storage, they are written
# exactly as they arrived over their interface. This
@@ -2447,8 +2567,8 @@ class Transport:
if next_hop_interface != None:
if next_hop_interface.AUTOCONFIGURE_MTU or next_hop_interface.FIXED_MTU: return next_hop_interface.HW_MTU
else: return None
else:
return None
else: return None
@staticmethod
def next_hop_per_bit_latency(destination_hash):
@@ -2511,7 +2631,8 @@ class Transport:
def path_is_unresponsive(destination_hash):
with Transport.path_states_lock:
if destination_hash in Transport.path_states:
if Transport.path_states[destination_hash] == Transport.STATE_UNRESPONSIVE: return True
if Transport.path_states[destination_hash] == Transport.STATE_UNRESPONSIVE:
return True
return False
@@ -2693,8 +2814,11 @@ class Transport:
destination_exists_on_local_client = True
with Transport.pending_local_prs_lock:
Transport.pending_local_path_requests[destination_hash] = attached_interface
local_destination = next((d for d in Transport.destinations if d.hash == destination_hash), None)
local_destination = None
with Transport.destinations_map_lock:
if destination_hash in Transport.destinations_map: local_destination = Transport.destinations_map[destination_hash]
if local_destination != None:
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)
@@ -2757,7 +2881,8 @@ class Transport:
held_entry = Transport.announce_table[packet.destination_hash]
Transport.held_announces[packet.destination_hash] = held_entry
Transport.announce_table[packet.destination_hash] = [now, retransmit_timeout, retries, received_from, announce_hops, packet, local_rebroadcasts, block_rebroadcasts, attached_interface]
with Transport.announce_table_lock:
Transport.announce_table[packet.destination_hash] = [now, retransmit_timeout, retries, received_from, announce_hops, packet, local_rebroadcasts, block_rebroadcasts, attached_interface]
elif is_from_local_client:
# Forward path request on all interfaces
@@ -2776,7 +2901,7 @@ class Transport:
# 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)
pr_entry = { "destination_hash": destination_hash, "timeout": time.time()+Transport.PATH_REQUEST_TIMEOUT, "requesting_interface": attached_interface }
Transport.discovery_path_requests[destination_hash] = pr_entry
with Transport.discovery_pr_lock: Transport.discovery_path_requests[destination_hash] = pr_entry
for interface in Transport.interfaces:
if not interface == attached_interface:
@@ -2851,7 +2976,7 @@ class Transport:
elif type(interface) == RNS.Interfaces.LocalInterface.LocalClientInterface:
local_interfaces.append(interface)
else:
def detach_job():
def detach_job(interface=interface):
RNS.log(f"Detaching {interface}", RNS.LOG_EXTREME)
interface.detach()
dt = threading.Thread(target=detach_job, daemon=False)
@@ -2876,11 +3001,11 @@ class Transport:
@staticmethod
def shared_connection_disappeared():
for link in Transport.active_links:
link.teardown()
with Transport.active_links_lock:
for link in Transport.active_links: link.teardown()
for link in Transport.pending_links:
link.teardown()
with Transport.pending_links_lock:
for link in Transport.pending_links: link.teardown()
Transport.announce_table = {}
Transport.path_table = {}
@@ -2892,11 +3017,10 @@ class Transport:
@staticmethod
def shared_connection_reappeared():
if Transport.owner.is_connected_to_shared_instance:
for registered_destination in Transport.destinations:
for registered_destination in Transport.destinations.copy():
if registered_destination.type == RNS.Destination.SINGLE:
registered_destination.announce(path_response=True)
@staticmethod
def drop_announce_queues():
for interface in Transport.interfaces:
@@ -2931,7 +3055,7 @@ class Transport:
return announce_emitted
@staticmethod
def save_packet_hashlist():
def save_packet_hashlist(background=False):
if not Transport.owner.is_connected_to_shared_instance:
if hasattr(Transport, "saving_packet_hashlist"):
wait_interval = 0.2
@@ -2968,7 +3092,7 @@ class Transport:
@staticmethod
def save_path_table():
def save_path_table(background=False):
if not Transport.owner.is_connected_to_shared_instance:
if hasattr(Transport, "saving_path_table"):
wait_interval = 0.2
@@ -3042,7 +3166,7 @@ class Transport:
@staticmethod
def save_tunnel_table():
def save_tunnel_table(background=False):
if not Transport.owner.is_connected_to_shared_instance:
if hasattr(Transport, "saving_tunnel_table"):
wait_interval = 0.2
@@ -3116,10 +3240,10 @@ class Transport:
gc.collect()
@staticmethod
def persist_data():
Transport.save_packet_hashlist()
Transport.save_path_table()
Transport.save_tunnel_table()
def persist_data(background=False):
Transport.save_packet_hashlist(background=background)
Transport.save_path_table(background=background)
Transport.save_tunnel_table(background=background)
@staticmethod
def exit_handler():
@@ -3210,7 +3334,8 @@ class Transport:
for destination_hash in drop_destinations:
try:
if destination_hash in Transport.path_table: Transport.path_table.pop(destination_hash)
with Transport.path_table_lock:
if destination_hash in Transport.path_table: Transport.path_table.pop(destination_hash)
except Exception as e:
RNS.log(f"Error while dropping blackhole-associated destination from path table: {e}", RNS.LOG_ERROR)
+1 -1
View File
@@ -1 +1 @@
__version__ = "1.1.5"
__version__ = "1.1.6"
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,4 +1,4 @@
# 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.
config: 0a02f7ea18b4c8fc12a9bb273be595c7
config: 76bb8ae3fb1a993b055966a4ef5900e1
tags: 645f666f9bcd5a90fca523b33c5a78b7
+1 -1
View File
@@ -1,5 +1,5 @@
const DOCUMENTATION_OPTIONS = {
VERSION: '1.1.5',
VERSION: '1.1.6',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
+4 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Code Examples - Reticulum Network Stack 1.1.5 documentation</title>
<title>Code Examples - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -3663,7 +3663,7 @@ will be fully on-par with natively included interfaces, including all supported
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.1.5 documentation</title>
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -294,7 +294,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<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.1.5 documentation</title>
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -178,7 +178,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -202,7 +202,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -836,7 +836,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Getting Started Fast - Reticulum Network Stack 1.1.5 documentation</title>
<title>Getting Started Fast - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -966,7 +966,7 @@ All other available modules will still be loaded when needed.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Communications Hardware - Reticulum Network Stack 1.1.5 documentation</title>
<title>Communications Hardware - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -674,7 +674,7 @@ can be used with Reticulum. This includes virtual software modems such as
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Reticulum Network Stack 1.1.5 documentation</title>
<title>Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="#"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="#"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -631,7 +631,7 @@ to participate in the development of Reticulum itself.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Configuring Interfaces - Reticulum Network Stack 1.1.5 documentation</title>
<title>Configuring Interfaces - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -1684,7 +1684,7 @@ to <code class="docutils literal notranslate"><span class="pre">30</span></code>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Reticulum License - Reticulum Network Stack 1.1.5 documentation</title>
<title>Reticulum License - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -343,7 +343,7 @@ SOFTWARE.
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Building Networks - Reticulum Network Stack 1.1.5 documentation</title>
<title>Building Networks - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -662,7 +662,7 @@ differently than a mobile device roaming between radio cells.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>API Reference - Reticulum Network Stack 1.1.5 documentation</title>
<title>API Reference - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -2472,7 +2472,7 @@ will announce it.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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 -->
<meta name="robots" content="noindex" />
<title>Search - Reticulum Network Stack 1.1.5 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<title>Search - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="#" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -302,7 +302,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Programs Using Reticulum - Reticulum Network Stack 1.1.5 documentation</title>
<title>Programs Using Reticulum - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -533,7 +533,7 @@ using LXMF.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Support Reticulum - Reticulum Network Stack 1.1.5 documentation</title>
<title>Support Reticulum - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -381,7 +381,7 @@ circumstances, so we rely on old-fashioned human feedback.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Understanding Reticulum - Reticulum Network Stack 1.1.5 documentation</title>
<title>Understanding Reticulum - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -1336,7 +1336,7 @@ those risks are acceptable to you.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Using Reticulum on Your System - Reticulum Network Stack 1.1.5 documentation</title>
<title>Using Reticulum on Your System - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -1395,7 +1395,7 @@ systemctl --user enable rnsd.service
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>What is Reticulum? - Reticulum Network Stack 1.1.5 documentation</title>
<title>What is Reticulum? - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -503,7 +503,7 @@ network, and vice versa.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></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">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>Zen of Reticulum - Reticulum Network Stack 1.1.5 documentation</title>
<title>Zen of Reticulum - Reticulum Network Stack 1.1.6 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/copybutton.css?v=76b2166b" />
@@ -180,7 +180,7 @@
</label>
</div>
<div class="header-center">
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.5 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.1.6 documentation</div></a>
</div>
<div class="header-right">
<div class="theme-toggle-container theme-toggle-header">
@@ -204,7 +204,7 @@
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
</div>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.5 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.1.6 documentation</span>
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -675,7 +675,7 @@ Imagine a messaging app. You write it once. It works on a laptop connected to fi
</aside>
</div>
</div><script src="_static/documentation_options.js?v=a48ae3df"></script>
</div><script src="_static/documentation_options.js?v=937269b5"></script>
<script src="_static/doctools.js?v=9bcbadda"></script>
<script src="_static/sphinx_highlight.js?v=dc90522c"></script>
<script src="_static/scripts/furo.js?v=46bd48cc"></script>