Compare commits

...

34 Commits

Author SHA1 Message Date
Mark Qvist 45e12cc668 Prepare release 2026-04-22 13:51:09 +02:00
Mark Qvist a21024a57e Prepare release 2026-04-22 13:48:02 +02:00
Mark Qvist c175491bb0 Updated version 2026-04-22 12:50:02 +02:00
Mark Qvist 09b0469faf Fixed bz2 decompression bomb vulnerability in Resource transfer assembly and Buffer StreamDataMessage unpacking. 2026-04-22 12:43:16 +02:00
Mark Qvist 3d63bbf4bf Fixed typo 2026-04-22 12:39:36 +02:00
Mark Qvist 56d5d01497 Updated changelog 2026-04-21 18:57:31 +02:00
Mark Qvist a70bd44426 Prepare release 2026-04-21 18:54:31 +02:00
Mark Qvist 8c082b2fcc Fixed path state potentially being applied before path table entry exists. 2026-04-21 18:49:03 +02:00
Mark Qvist 1732cac806 Updated makefile 2026-04-21 17:10:27 +02:00
Mark Qvist e1340e87eb Prepare release 2026-04-21 17:02:37 +02:00
Mark Qvist e9bfef2131 Cleanup 2026-04-21 16:55:59 +02:00
Mark Qvist b408699e65 Periodically clean known destinations data based on local relevance 2026-04-21 13:21:23 +02:00
Mark Qvist 3d1c508868 Improved BackboneInterface error handling 2026-04-21 00:24:00 +02:00
Mark Qvist 84e0746c9c Updated version 2026-04-20 23:49:24 +02:00
Mark Qvist b5658c4865 Keep track of which known destinations are actually in use, so irrelevant destination data can be cleaned 2026-04-20 23:48:57 +02:00
Mark Qvist d413a4bc53 Improved resource transfer timing calculations 2026-04-20 23:44:55 +02:00
Mark Qvist ce5ab902b6 Updated docs 2026-04-20 11:38:14 +02:00
Mark Qvist 294408b0bb Run non-background data persist synchronously 2026-04-19 01:32:12 +02:00
Mark Qvist 53372fbe4c Updated docs 2026-04-18 17:27:42 +02:00
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
38 changed files with 802 additions and 382 deletions
+91
View File
@@ -1,3 +1,92 @@
### 2026-04-22: RNS 1.1.9
This maintenance release fixes a critical security issue, that would allow an attacker to craft a BZ2 decompression bomb via Resource transfers or Buffer StreamDataMessage, causing an out-of-memory condition and crashing the receiving process via OOM killer.
Big thanks to @defidude (github.com/ratspeak) for discovering and reporting this vulnerability!
**Changes**
- Fixed bz2 decompression bomb vulnerability in Resource transfer assembly and Buffer StreamDataMessage unpacking.
**Release Hashes**
```
39a131aeb5d76fd73bfc67f68135f49ab0cf8628af154e04096a05c208ce77b6 rns-1.1.9-py3-none-any.whl
aab7bfc8c65514c9bdf4c22f00d288faf6c9e1777fc002dbe3eb29c286e67128 rnspure-1.1.9-py3-none-any.whl
```
**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.9-py3-none-any.whl.rsg
```
### 2026-04-21: RNS 1.1.8
This maintenance release fixes a critical bug in path state management, that could result in significant path convergence degradation under certain conditions.
**Changes**
- Fixed path state potentially being applied before path table entry exists, causing worse paths to be selected.
**Release Hashes**
```
9cf728e9e9a9fe113e4ac14e6b833f7ee65feedf8468e6ab94a261bf205f2632 rns-1.1.8-py3-none-any.whl
407dc3975335e9eabaaddb7ed1dc75fc3a1b8d24a7207e740797440c2ad0b3e5 rnspure-1.1.8-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.8-py3-none-any.whl.rsg
```
### 2026-04-21: RNS 1.1.7
**Changes**
- Added periodic known destination data cleaning based on local relevance.
- Improved resource transfer sequencing timing calculations and reliability.
- Improved BackboneInterface error handling on EPOLL errors.
- Ensured non-background data persist runs synchronously.
**Release Hashes**
```
4d9702c5d9bb8a3c8b94766cb51cccad5afd78d615af9a6b146730347044e6f0 rns-1.1.7-py3-none-any.whl
172dede7656b41b85e4319354ed04649b518e58c54586da7e443579c620a0a5b rnspure-1.1.7-py3-none-any.whl
```
**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.7-py3-none-any.whl.rsg
```
### 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 +110,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**
+1 -1
View File
@@ -222,7 +222,7 @@ def link_established(link):
# Inform the user that the server is
# connected
RNS.log("Link established with server, hit enter to sand a resource, or type in \"quit\" to quit")
RNS.log("Link established with server, hit enter to send a resource, or type in \"quit\" to quit")
# When a link is closed, we'll inform the
# user, and exit the program
+12 -3
View File
@@ -61,9 +61,18 @@ release: test remove_symlinks build_sdist build_wheel build_pure_wheel documenta
debug: remove_symlinks build_wheel build_pure_wheel create_symlinks
upload:
@echo Ready to publish release, hit enter to continue
upload: upload-rns upload-rnspure
upload-rns:
@echo Ready to publish rns release, hit enter to continue
@read VOID
@echo Uploading to PyPi...
twine upload dist/*
twine upload dist/rns-*.whl dist/rns-*.tar.gz
@echo Release published
upload-rnspure:
@echo Ready to publish rnspure release, hit enter to continue
@read VOID
@echo Uploading to PyPi...
twine upload dist/rnspure-*.whl
@echo Release published
+3 -1
View File
@@ -92,7 +92,9 @@ class StreamDataMessage(MessageBase):
self.data = raw[2:]
if self.compressed:
self.data = bz2.decompress(self.data)
decompressor = bz2.BZ2Decompressor()
self.data = decompressor.decompress(self.data, max_length=RawChannelWriter.MAX_CHUNK_LEN)
if not decompressor.eof: raise IOError("Decompressed buffer chunk exceeds maximum legitimate size")
class RawChannelReader(RawIOBase, AbstractContextManager):
+12 -22
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):
"""
+130 -30
View File
@@ -94,17 +94,25 @@ class Identity:
known_ratchets = {}
ratchet_persist_lock = threading.Lock()
known_destinations_lock = threading.Lock()
@staticmethod
def remember(packet_hash, destination_hash, public_key, app_data = None):
if len(public_key) != Identity.KEYSIZE//8:
raise TypeError("Can't remember "+RNS.prettyhexrep(destination_hash)+", the public key size of "+str(len(public_key))+" is not valid.", RNS.LOG_ERROR)
else:
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data]
with Identity.known_destinations_lock:
if not destination_hash in Identity.known_destinations:
Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data, 0]
else:
entry = Identity.known_destinations[destination_hash]
entry[0] = time.time()
entry[1] = packet_hash
entry[2] = public_key
entry[3] = app_data
@staticmethod
def recall(target_hash, from_identity_hash=False):
def recall(target_hash, from_identity_hash=False, _no_use=False):
"""
Recall identity for a destination or identity hash. By default, this function
will return the identity associated with a given *destination* hash. As an
@@ -120,6 +128,7 @@ class Identity:
if from_identity_hash:
for destination_hash in Identity.known_destinations:
if target_hash == Identity.truncated_hash(Identity.known_destinations[destination_hash][2]):
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(destination_hash)
identity_data = Identity.known_destinations[destination_hash]
identity = Identity(create_keys=False)
identity.load_public_key(identity_data[2])
@@ -130,6 +139,7 @@ class Identity:
else:
if target_hash in Identity.known_destinations:
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(target_hash)
identity_data = Identity.known_destinations[target_hash]
identity = Identity(create_keys=False)
identity.load_public_key(identity_data[2])
@@ -146,7 +156,7 @@ class Identity:
return None
@staticmethod
def recall_app_data(destination_hash):
def recall_app_data(destination_hash, _no_use=False):
"""
Recall last heard app_data for a destination hash.
@@ -154,13 +164,14 @@ class Identity:
:returns: *Bytes* containing app_data, or *None* if the destination is unknown.
"""
if destination_hash in Identity.known_destinations:
if not _no_use: RNS.Reticulum.get_instance()._used_destination_data(destination_hash)
app_data = Identity.known_destinations[destination_hash][3]
return app_data
else:
return None
else: return None
@staticmethod
def save_known_destinations():
def save_known_destinations(background=False, recombine=True):
# 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
@@ -181,32 +192,33 @@ class Identity:
Identity.saving_known_destinations = True
save_start = time.time()
storage_known_destinations = {}
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
if recombine:
storage_known_destinations = {}
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
try:
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
storage_known_destinations = umsgpack.load(file)
except: pass
try:
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
storage_known_destinations = umsgpack.load(file)
except: pass
for destination_hash in storage_known_destinations:
if not destination_hash in Identity.known_destinations:
with Identity.known_destinations_lock:
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
except Exception as e:
RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING)
try:
for destination_hash in storage_known_destinations:
if not destination_hash in Identity.known_destinations:
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
except Exception as e:
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_DEBUG)
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_VERBOSE)
with open(RNS.Reticulum.storagepath+"/known_destinations","wb") as file:
umsgpack.dump(Identity.known_destinations.copy(), file)
save_time = time.time() - save_start
if save_time < 1:
time_str = str(round(save_time*1000,2))+"ms"
else:
time_str = str(round(save_time,2))+"s"
if save_time < 1: time_str = str(round(save_time*1000,2))+"ms"
else: time_str = str(round(save_time,2))+"s"
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_DEBUG)
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_VERBOSE)
except Exception as e:
RNS.log("Error while saving known destinations to disk, the contained exception was: "+str(e), RNS.LOG_ERROR)
@@ -217,6 +229,7 @@ class Identity:
@staticmethod
def load_known_destinations():
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
st = time.time()
try:
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
loaded_known_destinations = umsgpack.load(file)
@@ -224,15 +237,102 @@ class Identity:
Identity.known_destinations = {}
for known_destination in loaded_known_destinations:
if len(known_destination) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
if len(loaded_known_destinations[known_destination]) < 5:
e = loaded_known_destinations[known_destination]
loaded_known_destinations[known_destination] = [e[0], e[1], e[2], e[3], 0]
RNS.log("Loaded "+str(len(Identity.known_destinations))+" known destination from storage", RNS.LOG_VERBOSE)
with Identity.known_destinations_lock:
Identity.known_destinations[known_destination] = loaded_known_destinations[known_destination]
RNS.log(f"Loaded {len(Identity.known_destinations)} known destination from storage in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_VERBOSE)
except Exception as e:
RNS.log("Error loading known destinations from disk, file will be recreated on exit", RNS.LOG_ERROR)
RNS.trace_exception(e)
else:
RNS.log("Destinations file does not exist, no known destinations loaded", RNS.LOG_VERBOSE)
@staticmethod
def _used_destination_data(destination_hash):
with Identity.known_destinations_lock:
if destination_hash in Identity.known_destinations:
if not Identity.known_destinations[destination_hash][4] < 0:
Identity.known_destinations[destination_hash][4] = time.time()
return True
return False
@staticmethod
def _retain_destination_data(destination_hash):
with Identity.known_destinations_lock:
if destination_hash in Identity.known_destinations:
Identity.known_destinations[destination_hash][4] = -1
return True
return False
@staticmethod
def _unretain_destination_data(destination_hash):
with Identity.known_destinations_lock:
if destination_hash in Identity.known_destinations:
Identity.known_destinations[destination_hash][4] = time.time()
return True
return False
@staticmethod
def clean_known_destinations():
now = time.time()
st = now
total = len(Identity.known_destinations)
stale = []
no_path = 0
retained = 0
never_used = 0
for destination_hash in Identity.known_destinations:
try:
if RNS.Transport.has_path(destination_hash): has_path = True
else:
has_path = False
no_path += 1
with Identity.known_destinations_lock:
if destination_hash in Identity.known_destinations:
last_announce = Identity.known_destinations[destination_hash][0]
last_use = 0
was_used = False
is_retained = False
if Identity.known_destinations[destination_hash][4] > 0:
was_used = True
last_use = Identity.known_destinations[destination_hash][4]
elif Identity.known_destinations[destination_hash][4] == 0:
was_used = False
never_used += 1
elif Identity.known_destinations[destination_hash][4] == -1:
is_retained = True
retained += 1
unused_for = time.time() - Identity.known_destinations[destination_hash][4]
if not is_retained and not has_path:
if not was_used and now - last_announce > RNS.Transport.UNUSED_DESTINATION_LINGER: stale.append(destination_hash)
elif unused_for > RNS.Transport.DESTINATION_TIMEOUT*1.25: stale.append(destination_hash)
except Exception as e: RNS.log(f"Faulty entry for {RNS.prettyhexrep(destination_hash)} while cleaning known destinations: {e}", RNS.LOG_DEBUG)
removed = 0
for destination_hash in stale:
with Identity.known_destinations_lock:
if destination_hash in Identity.known_destinations:
Identity.known_destinations.pop(destination_hash)
removed += 1
# RNS.log(f"Total destinations: {total}, stale: {len(stale)}, removed: {removed}, no path: {no_path}, never used: {never_used}, with path: {total-no_path}, used: {total-never_used}, retained: {retained}. Completed in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_WARNING) # TODO: Remove
if not RNS.Transport.owner.is_connected_to_shared_instance: Identity.save_known_destinations(recombine=False)
@staticmethod
def full_hash(data):
"""
@@ -491,9 +591,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():
+9 -6
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)
@@ -293,7 +292,11 @@ class BackboneInterface(Interface):
spawned_interface.receive(b"")
spawned_interface.transmit_buffer = spawned_interface.transmit_buffer[written:]
if len(spawned_interface.transmit_buffer) == 0: BackboneInterface.epoll.modify(fileno, select.EPOLLIN)
try:
if len(spawned_interface.transmit_buffer) == 0: BackboneInterface.epoll.modify(fileno, select.EPOLLIN)
except Exception as e:
RNS.log(f"Error while setting EPOLLIN on {spawned_interface}: {e}", RNS.LOG_ERROR)
spawned_interface.txb += written
if spawned_interface.parent_interface: spawned_interface.parent_interface.txb += written
+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
+24 -3
View File
@@ -126,6 +126,7 @@ class Resource:
PART_TIMEOUT_FACTOR = 4
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
PROOF_TIMEOUT_FACTOR = 3
HMU_WAIT_FACTOR = 3.5
MAX_RETRIES = 16
MAX_ADV_RETRIES = 4
SENDER_GRACE_TIME = 10.0
@@ -193,6 +194,7 @@ class Resource:
resource.window_flexibility = Resource.WINDOW_FLEXIBILITY
resource.last_activity = time.time()
resource.started_transferring = resource.last_activity
resource.advertisement_packet = advertisement_packet
resource.storagepath = RNS.Reticulum.resourcepath+"/"+resource.original_hash.hex()
resource.meta_storagepath = resource.storagepath+".meta"
@@ -359,6 +361,7 @@ class Resource:
self.request_id = request_id
self.started_transferring = None
self.is_response = is_response
self.max_decompressed_size = Resource.AUTO_COMPRESS_MAX_SIZE
self.auto_compress_limit = Resource.AUTO_COMPRESS_MAX_SIZE
self.auto_compress_option = auto_compress
@@ -594,15 +597,16 @@ class Resource:
extra_wait = retries_used * Resource.PER_RETRY_DELAY
self.update_eifr()
expected_hmu_wait_remaining = (self.sdu*8*self.HMU_WAIT_FACTOR)/self.eifr if self.waiting_for_hmu or self.outstanding_parts == 0 else 0
expected_tof_remaining = (self.outstanding_parts*self.sdu*8)/self.eifr
if self.req_resp_rtt_rate != 0:
sleep_time = self.last_activity + self.part_timeout_factor*expected_tof_remaining + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
sleep_time = self.last_activity + self.part_timeout_factor*expected_tof_remaining + expected_hmu_wait_remaining + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
else:
sleep_time = self.last_activity + self.part_timeout_factor*((3*self.sdu)/self.eifr) + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
# TODO: Remove debug at some point
# RNS.log(f"EIFR {RNS.prettyspeed(self.eifr)}, ETOF {RNS.prettyshorttime(expected_tof_remaining)} ", RNS.LOG_DEBUG, pt=True)
# RNS.log(f"EIFR {RNS.prettyspeed(self.eifr)}, ETOF {RNS.prettyshorttime(expected_tof_remaining)}, EHWR {RNS.prettyshorttime(expected_hmu_wait_remaining)}", RNS.LOG_DEBUG, pt=True)
# RNS.log(f"Resource ST {RNS.prettyshorttime(sleep_time)}, RTT {RNS.prettyshorttime(self.rtt or self.link.rtt)}, {self.outstanding_parts} left", RNS.LOG_DEBUG, pt=True)
if sleep_time < 0:
@@ -677,6 +681,16 @@ class Resource:
# Strip off random hash
data = data[Resource.RANDOM_HASH_SIZE:]
if not self.compressed: self.data = data
else:
decompressor = bz2.BZ2Decompressor()
self.data = decompressor.decompress(data, max_length=self.max_decompressed_size)
if not decompressor.eof:
self.status = Resource.CORRUPT
self.cancel()
RNS.log(f"Decompressed resource exceeded maximum decompressed size. The resource was rejected.", RNS.LOG_ERROR)
return
if self.compressed: self.data = bz2.decompress(data)
else: self.data = data
@@ -959,6 +973,7 @@ class Resource:
self.last_activity = time.time()
self.req_sent = self.last_activity
self.req_sent_bytes = len(request_packet.raw)
self.rtt_rxd_bytes_at_part_req = self.rtt_rxd_bytes
self.req_resp = None
except Exception as e:
@@ -1066,7 +1081,13 @@ class Resource:
Cancels transferring the resource.
"""
if self.next_segment: self.next_segment.cancel()
if self.status < Resource.COMPLETE:
if self.status == Resource.CORRUPT:
self.link.cancel_incoming_resource(self)
self.reject(self.advertisement_packet)
self.link.teardown()
elif self.status < Resource.COMPLETE:
self.status = Resource.FAILED
if self.initiator:
if self.link.status == RNS.Link.ACTIVE:
+55 -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,20 @@ 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)
if background: threading.Thread(target=job, daemon=True).start()
else: job()
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 +1020,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 +1033,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)
@@ -1080,6 +1087,13 @@ class Reticulum:
identity_hash = call["unblackhole_identity"]
rpc_connection.send(self.unblackhole_identity(identity_hash))
if "destination_data" in call:
operation = call["destination_data"]
destination_hash = call["destination_hash"]
if operation == "used": rpc_connection.send(self._used_destination_data(destination_hash))
elif operation == "retain": rpc_connection.send(self._retain_destination_data(destination_hash))
elif operation == "unretain": rpc_connection.send(self._unretain_destination_data(destination_hash))
rpc_connection.close()
except Exception as e:
@@ -1087,6 +1101,33 @@ class Reticulum:
def get_rpc_client(self): return multiprocessing.connection.Client(self.rpc_addr, family=self.rpc_type, authkey=self.rpc_key)
def _used_destination_data(self, destination_hash):
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
rpc_connection.send({"destination_data": "used", "destination_hash": destination_hash})
response = rpc_connection.recv()
return response
else: return RNS.Identity._used_destination_data(destination_hash)
def _retain_destination_data(self, destination_hash):
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
rpc_connection.send({"destination_data": "retain", "destination_hash": destination_hash})
response = rpc_connection.recv()
return response
else: return RNS.Identity._retain_destination_data(destination_hash)
def _unretain_destination_data(self, destination_hash):
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
rpc_connection.send({"destination_data": "unretain", "destination_hash": destination_hash})
response = rpc_connection.recv()
return response
else: return RNS.Identity._unretain_destination_data(destination_hash)
def get_interface_stats(self):
if self.is_connected_to_shared_instance:
rpc_connection = self.get_rpc_client()
+372 -207
View File
@@ -76,6 +76,7 @@ class Transport:
LOCAL_REBROADCASTS_MAX = 2 # How many local rebroadcasts of an announce is allowed
PATH_REQUEST_TIMEOUT = 15 # Default timeout for client path requests in seconds
PATH_REQUEST_GATE_TIMEOUT = 120 # Default timeout for client path request gate control in seconds
PATH_REQUEST_GRACE = 0.4 # Grace time before a path announcement is made, allows directly reachable peers to respond first
PATH_REQUEST_RG = 1.5 # Extra grace time for roaming-mode interfaces to allow more suitable peers to respond first
PATH_REQUEST_MI = 20 # Minimum interval in seconds for automated path requests
@@ -87,6 +88,9 @@ 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
UNUSED_DESTINATION_LINGER = 6*60 # Linger time for pathless and never used destinations
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 +98,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 +124,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 +144,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
@@ -171,6 +179,8 @@ class Transport:
pending_prs_check_interval = 30.0
cache_last_cleaned = 0.0
cache_clean_interval = 5*60
destinations_last_cleaned = 0.0
known_destinations_interval = 5*60
tables_last_culled = 0.0
tables_cull_interval = 5.0
interface_last_jobs = 0.0
@@ -257,6 +267,9 @@ class Transport:
# Defer cleaning packet cache for 60 seconds
Transport.cache_last_cleaned = time.time() + 60
# Defer cleaning known destinations
Transport.destinations_last_cleaned = time.time()
# Defer sending management announces for 15 seconds
Transport.last_mgmt_announce = time.time() - Transport.mgmt_announce_interval + 15
@@ -291,7 +304,7 @@ class Transport:
blackholed = False
if len(Transport.blackholed_identities) > 0:
path_identity = RNS.Identity.recall(destination_hash)
path_identity = RNS.Identity.recall(destination_hash, _no_use=True)
if path_identity in Transport.blackholed_identities: blackholed = True
del path_identity
@@ -302,7 +315,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 +374,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 +421,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():
@@ -558,7 +573,7 @@ class Transport:
announce_context = RNS.Packet.NONE
if block_rebroadcasts: announce_context = RNS.Packet.PATH_RESPONSE
announce_data = packet.data
announce_identity = RNS.Identity.recall(packet.destination_hash)
announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=True)
announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown");
announce_destination.hash = packet.destination_hash
announce_destination.hexhash = announce_destination.hash.hex()
@@ -607,10 +622,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()
@@ -744,6 +763,14 @@ class Transport:
should_collect = True
RNS.log("Path to "+RNS.prettyhexrep(destination_hash)+" was removed since the attached interface no longer exists", RNS.LOG_DEBUG)
# Cull the pending path requests table
stale_path_requests = []
with Transport.path_requests_lock:
for destination_hash in Transport.path_requests:
if time.time() > Transport.path_requests[destination_hash] + Transport.PATH_REQUEST_GATE_TIMEOUT:
stale_path_requests.append(destination_hash)
RNS.log("Path request entry for "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_EXTREME)
# Cull the pending discovery path requests table
stale_discovery_path_requests = []
with Transport.discovery_pr_lock:
@@ -762,9 +789,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 +807,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
@@ -820,6 +864,16 @@ class Transport:
if i == 1: RNS.log("Removed "+str(i)+" path", RNS.LOG_EXTREME)
else: RNS.log("Removed "+str(i)+" paths", RNS.LOG_EXTREME)
i = 0
with Transport.path_requests_lock:
for destination_hash in stale_path_requests:
Transport.path_requests.pop(destination_hash)
i += 1
if i > 0:
if i == 1: RNS.log("Removed "+str(i)+" path request entry", RNS.LOG_EXTREME)
else: RNS.log("Removed "+str(i)+" path request entries", RNS.LOG_EXTREME)
i = 0
with Transport.discovery_pr_lock:
for destination_hash in stale_discovery_path_requests:
@@ -864,7 +918,15 @@ 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()
# Clean known destinations
if time.time() > Transport.destinations_last_cleaned+Transport.known_destinations_interval:
Transport.destinations_last_cleaned = time.time()
def job(): RNS.Identity.clean_known_destinations()
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 +970,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 +1131,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 +1156,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 +1621,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 +1650,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 +1669,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)
@@ -1672,6 +1750,7 @@ class Transport:
else:
# If this destination is unknown in our table
# we should add it
Transport.mark_path_unknown_state(packet.destination_hash)
should_add = True
if should_add:
@@ -1706,9 +1785,7 @@ class Transport:
else:
rate_entry["last"] = now
else:
rate_blocked = True
else: rate_blocked = True
retries = 0
announce_hops = packet.hops
@@ -1779,7 +1856,7 @@ class Transport:
# If we have any local clients connected, we re-
# transmit the announce to them immediately
if (len(Transport.local_client_interfaces)):
announce_identity = RNS.Identity.recall(packet.destination_hash)
announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=True)
announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown");
announce_destination.hash = packet.destination_hash
announce_destination.hexhash = announce_destination.hash.hex()
@@ -1833,7 +1910,7 @@ class Transport:
interface_str = " on "+str(attached_interface)
RNS.log("Got matching announce, answering waiting discovery path request for "+RNS.prettyhexrep(packet.destination_hash)+interface_str, RNS.LOG_DEBUG)
announce_identity = RNS.Identity.recall(packet.destination_hash)
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.hash = packet.destination_hash
announce_destination.hexhash = announce_destination.hash.hex()
@@ -1858,18 +1935,24 @@ class Transport:
if not Transport.owner.is_connected_to_shared_instance: Transport.cache(packet, force_cache=True, packet_type="announce")
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
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 packet.destination_hash in Transport.path_requests:
RNS.Reticulum.get_instance()._used_destination_data(packet.destination_hash)
# If the receiving interface is a tunnel, we add the
# 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
@@ -1879,7 +1962,7 @@ class Transport:
# Check that the announced destination matches
# the handlers aspect filter
execute_callback = False
announce_identity = RNS.Identity.recall(packet.destination_hash)
announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=True)
if handler.aspect_filter == None:
# If the handlers aspect filter is set to
# None, we execute the callback in all cases
@@ -1896,25 +1979,25 @@ 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))
app_data=RNS.Identity.recall_app_data(packet.destination_hash, _no_use=True))
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),
app_data=RNS.Identity.recall_app_data(packet.destination_hash, _no_use=True),
announce_packet_hash = packet.packet_hash)
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),
app_data=RNS.Identity.recall_app_data(packet.destination_hash, _no_use=True),
announce_packet_hash = packet.packet_hash,
is_path_response = packet.context == RNS.Packet.PATH_RESPONSE)
threading.Thread(target=job, daemon=True).start()
@@ -1930,73 +2013,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:
@@ -2014,7 +2105,7 @@ class Transport:
signalling_bytes = RNS.Link.signalling_bytes(RNS.Link.mtu_from_lp_packet(packet), RNS.Link.mode_from_lp_packet(packet))
peer_pub_bytes = packet.data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2]
peer_identity = RNS.Identity.recall(link_entry[IDX_LT_DSTHASH])
peer_identity = RNS.Identity.recall(link_entry[IDX_LT_DSTHASH], _no_use=True)
peer_sig_pub_bytes = peer_identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE]
signed_data = packet.destination_hash+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes
@@ -2027,6 +2118,8 @@ class Transport:
new_raw += packet.raw[2:]
Transport.link_table[packet.destination_hash][IDX_LT_VALIDATED] = True
Transport.transmit(link_entry[IDX_LT_RCVD_IF], new_raw)
if not Transport.owner.is_connected_to_shared_instance:
RNS.Identity._used_destination_data(link_entry[IDX_LT_DSTHASH])
else:
RNS.log("Invalid link request proof in transport for link "+RNS.prettyhexrep(packet.destination_hash)+", dropping proof.", RNS.LOG_DEBUG)
@@ -2038,48 +2131,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 +2191,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 +2254,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 +2278,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 +2348,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 +2363,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 +2435,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 +2467,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 +2604,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 +2668,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 +2851,12 @@ 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)
RNS.Reticulum.get_instance()._used_destination_data(destination_hash)
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 +2919,10 @@ 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]
if not Transport.owner.is_connected_to_shared_instance: RNS.Identity._used_destination_data(packet.destination_hash)
elif is_from_local_client:
# Forward path request on all interfaces
@@ -2776,7 +2941,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 +3016,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 +3041,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 +3057,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 +3095,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 +3132,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 +3206,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 +3280,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():
@@ -3202,7 +3366,7 @@ class Transport:
drop_destinations = []
for destination_hash in Transport.path_table.copy():
try:
associated_identity = RNS.Identity.recall(destination_hash)
associated_identity = RNS.Identity.recall(destination_hash, _no_use=True)
if associated_identity and associated_identity.hash in Transport.blackholed_identities:
if not destination_hash in drop_destinations: drop_destinations.append(destination_hash)
except Exception as e:
@@ -3210,7 +3374,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.9"
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: b2a01d6b7bffdf2e55f3a50f8370e2af
tags: 645f666f9bcd5a90fca523b33c5a78b7
+1 -1
View File
@@ -1,5 +1,5 @@
const DOCUMENTATION_OPTIONS = {
VERSION: '1.1.5',
VERSION: '1.1.9',
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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.
+6 -6
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.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/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.9 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.9 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">
@@ -506,7 +506,7 @@ for addressable hashes and other purposes. Non-configurable.</p>
<dl class="py method">
<dt class="sig sig-object py" id="RNS.Identity.recall">
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">recall</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">target_hash</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">from_identity_hash</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.recall" title="Link to this definition"></a></dt>
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">recall</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">target_hash</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">from_identity_hash</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">_no_use</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.recall" title="Link to this definition"></a></dt>
<dd><p>Recall identity for a destination or identity hash. By default, this function
will return the identity associated with a given <em>destination</em> hash. As an
example, if you know the <code class="docutils literal notranslate"><span class="pre">lxmf.delivery</span></code> destination hash of an endpoint,
@@ -528,7 +528,7 @@ search for an identity from a known <em>identity hash</em>, by setting the
<dl class="py method">
<dt class="sig sig-object py" id="RNS.Identity.recall_app_data">
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">recall_app_data</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">destination_hash</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.recall_app_data" title="Link to this definition"></a></dt>
<em class="property"><span class="k"><span class="pre">static</span></span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">recall_app_data</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">destination_hash</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">_no_use</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.recall_app_data" title="Link to this definition"></a></dt>
<dd><p>Recall last heard app_data for a destination hash.</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
@@ -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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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>
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">
<!-- 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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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.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/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.9 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.9 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=7b68ca77"></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>