Compare commits

..

25 Commits

Author SHA1 Message Date
Mark Qvist 7cbce84cbd Prepare release 2026-05-21 17:50:18 +02:00
Mark Qvist fe334c0d7c Updated changelog 2026-05-21 17:41:27 +02:00
Mark Qvist 33d5a8e2a8 Cleanup 2026-05-21 17:38:29 +02:00
Mark Qvist e80bf471ec Slight robustification 2026-05-21 17:31:20 +02:00
Mark Qvist 7dfdea2395 Raise descriptive error if hashlib.file_digest is not available. 2026-05-21 17:16:31 +02:00
Mark Qvist 74b61aebd2 Updated docs 2026-05-21 17:06:47 +02:00
Mark Qvist ce9071e2d3 Added ability to use wildcards in artifact fetch specifications 2026-05-21 17:06:04 +02:00
Mark Qvist d6cf59dcc8 Fix error message when no specified artifacts were available for fetch 2026-05-21 16:34:34 +02:00
Mark Qvist de61652d37 Updated changelog 2026-05-21 16:31:48 +02:00
Mark Qvist 6181f62d93 Return not found instead of remote error on missing document 2026-05-21 15:31:22 +02:00
Mark Qvist ed66b4873e Updated version 2026-05-21 15:17:33 +02:00
Mark Qvist 26869941a4 Merge branch 'patch_fix_channel_outlet_race' 2026-05-21 15:16:14 +02:00
Mark Qvist ee7b4e7ae5 Consistency 2026-05-21 15:16:09 +02:00
Mark Qvist 7866484453 Merge branch 'fix_kd_iter' 2026-05-21 15:01:37 +02:00
Mark Qvist 817b3b1a12 Consistency 2026-05-21 15:01:17 +02:00
Mark Qvist 17f6968467 Added blackhole methods to API docs 2026-05-21 13:19:06 +02:00
Mark Qvist a96a1d6692 Adjusted timeouts 2026-05-20 01:08:11 +02:00
Mark Qvist c1081fa9a4 Consistency 2026-05-20 01:07:54 +02:00
Mark Qvist dd3104094b Fixed check for existing link at shutdown 2026-05-20 00:32:08 +02:00
Mark Qvist dc68eea313 Fix commit message rendering 2026-05-19 21:35:45 +02:00
Jeremy O'Brien 794e437f6d Channel: prevent sequence holes and ghost envelopes when sending on a dying outlet
RNSChannelOutlet.send() can return a packet that never reached the wire
(link not ACTIVE, no capable interface, etc). The old Channel.send()
queued the envelope in _tx_ring before calling outlet.send(), then
tried to rewind _next_sequence and remove the envelope if the outlet
returned a failed packet. Two problems:

- Between queueing and outlet.send() returning, _tx_ring held an
envelope with packet.raw=None. Any worker thread iterating the
ring (timeout fire, proof callback) crashed in get_packet_id's
packet.get_hash() with a TypeError on None.raw.

- The rewind was only safe for a single-threaded sender: it checked
"is _next_sequence one past mine?" and skipped the rewind otherwise.
Under concurrent senders, the rewind silently failed, leaving a
hole in the on-wire sequence stream. The receiver's contiguous
seqnum rule then stalled the channel permanently with no error.

This fix serializes the reservation-and-transmit pair with a per-channel
_send_lock so the rewind is always correct, and defers queueing until
outlet.send() returns a real packet so _tx_ring never contains a
packet-less envelope. _packet_tx_op() and get_packet_id() now also
defensively skip/return-None for packet-less envelopes.

Also handle the small race where a proof arrives between outlet.send()
registering the receipt and us installing the delivery callback: after
registration, re-read the receipt status and synthesize the
_packet_delivered() call if it's already DELIVERED.
2026-05-19 14:52:59 -04:00
Jeremy O'Brien ebf544d335 rnsh: don't wait forever for rns operations when timeout isn't set 2026-05-19 07:46:50 -04:00
Jeremy O'Brien 939f30fef2 Don't iterate known_destinations directly; it can change during iteration 2026-05-19 07:46:50 -04:00
Mark Qvist 4549bbfdb9 Docs formatting 2026-05-19 11:41:09 +02:00
Jeremy O'Brien 603f709139 Don't iterate known_destinations directly; it can change during iteration 2026-05-18 00:10:25 -04:00
42 changed files with 569 additions and 227 deletions
+36
View File
@@ -1,3 +1,39 @@
### 2026-05-21: RNS 1.3.0
This maintenance release fixes a number of bugs.
**Changes**
- Added ability to use wildcards and pattern matches in `rngit` artifact fetch targets
- Fixed channel outlet sequence holes and ghost envelopes on dying outlets by **neutral**
- Fixed known destination iteration races by **neutral**
- Fixed timeout deadlock in `rnsh` by **neutral**
- Fixed commit message rendering in `rngit`
- Fixed various minor bugs and output inconsistencies in `rngit`
- Adjusted timeouts for remote operations in `rngit`
- Updated documentation
**Verified Retrieval**
You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To download all artifacts, and the release manifest for future updates, you can use the following command:
```sh
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:all --signer bc7291552be7a58f361522990465165c
```
To retrieve only the `.whl` package for installation, you can use:
```sh
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:rns-1.3.0-py3-none-any.whl --signer bc7291552be7a58f361522990465165c
```
**Release Signatures**
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
```sh
rnid -i bc7291552be7a58f361522990465165c -V manifest.rsm *.rsg
```
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
### 2026-05-19: RNS 1.2.9
This release completes the operational functionality of the `rngit` system, which now has full release creation, fetch and verified update support using the `rngit release` command. Additionally, two chapters have been added to the manual should cover all the things that `rngit` is currently capable of.
+89 -56
View File
@@ -144,7 +144,7 @@ class MessageBase(abc.ABC):
MSGTYPE = None
"""
Defines a unique identifier for a message class.
* Must be unique within all classes registered with a ``Channel``
* Must be less than ``0xf000``. Values greater than or equal to ``0xf000`` are reserved.
"""
@@ -255,11 +255,11 @@ class Channel(contextlib.AbstractContextManager):
# The maximum window size for transfers on fast links
WINDOW_MAX_FAST = 48
# For calculating maps and guard segments, this
# must be set to the global maximum window.
WINDOW_MAX = WINDOW_MAX_FAST
# If the fast rate is sustained for this many request
# rounds, the fast link window size will be allowed.
FAST_RATE_THRESHOLD = 10
@@ -285,6 +285,7 @@ class Channel(contextlib.AbstractContextManager):
"""
self._outlet = outlet
self._lock = threading.RLock()
self._send_lock = threading.Lock()
self._tx_ring: collections.deque[Envelope] = collections.deque()
self._rx_ring: collections.deque[Envelope] = collections.deque()
self._message_callbacks: [MessageCallbackType] = []
@@ -382,27 +383,30 @@ class Channel(contextlib.AbstractContextManager):
if envelope.packet is not None:
self._outlet.set_packet_timeout_callback(envelope.packet, None)
self._outlet.set_packet_delivered_callback(envelope.packet, None)
envelope.tracked = False
for envelope in self._rx_ring:
envelope.tracked = False
self._tx_ring.clear()
self._rx_ring.clear()
def _emplace_envelope(self, envelope: Envelope, ring: collections.deque[Envelope]) -> bool:
with self._lock:
i = 0
for existing in ring:
if envelope.sequence == existing.sequence:
RNS.log(f"Envelope: Emplacement of duplicate envelope with sequence "+str(envelope.sequence), RNS.LOG_EXTREME)
return False
if envelope.sequence < existing.sequence and not (self._next_rx_sequence - envelope.sequence) > (Channel.SEQ_MAX//2):
ring.insert(i, envelope)
envelope.tracked = True
return True
i += 1
envelope.tracked = True
ring.append(envelope)
@@ -457,7 +461,7 @@ class Channel(contextlib.AbstractContextManager):
m = e.unpack(self._message_factories)
else:
m = e.message
self._rx_ring.remove(e)
self._run_callbacks(m)
@@ -476,7 +480,7 @@ class Channel(contextlib.AbstractContextManager):
with self._lock:
outstanding = 0
for envelope in self._tx_ring:
if envelope.outlet == self._outlet:
if envelope.outlet == self._outlet:
if not envelope.packet or not self._outlet.get_packet_state(envelope.packet) == MessageState.MSGSTATE_DELIVERED:
outstanding += 1
@@ -486,8 +490,10 @@ class Channel(contextlib.AbstractContextManager):
return True
def _packet_tx_op(self, packet: TPacket, op: Callable[[TPacket], bool]):
target_id = self._outlet.get_packet_id(packet)
with self._lock:
envelope = next(filter(lambda e: self._outlet.get_packet_id(e.packet) == self._outlet.get_packet_id(packet),
envelope = next(filter(lambda e: e.packet is not None
and self._outlet.get_packet_id(e.packet) == target_id,
self._tx_ring), None)
if envelope and op(envelope):
@@ -516,7 +522,7 @@ class Channel(contextlib.AbstractContextManager):
# TODO: Remove at some point
# RNS.log("Increased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
# RNS.log("Increased "+str(self)+" min window to "+str(self.window_min), RNS.LOG_DEBUG)
else:
self.fast_rate_rounds += 1
if self.window_max < Channel.WINDOW_MAX_FAST and self.fast_rate_rounds == Channel.FAST_RATE_THRESHOLD:
@@ -547,36 +553,48 @@ class Channel(contextlib.AbstractContextManager):
return to
def _packet_timeout(self, packet: TPacket):
def retry_envelope(envelope: Envelope) -> bool:
if self._outlet.get_packet_state(packet) == MessageState.MSGSTATE_DELIVERED:
return
target_id = self._outlet.get_packet_id(packet)
envelope_to_resend: Envelope | None = None
should_teardown = False
with self._lock:
envelope = next(filter(
lambda e: e.packet is not None and self._outlet.get_packet_id(e.packet) == target_id,
self._tx_ring), None)
if envelope is None:
return
if envelope.tries >= self._max_tries:
RNS.log("Retry count exceeded on "+str(self)+", tearing down Link.", RNS.LOG_ERROR)
self._shutdown() # start on separate thread?
self._outlet.timed_out()
return True
should_teardown = True
else:
envelope.tries += 1
envelope_to_resend = envelope
envelope.tries += 1
self._outlet.resend(envelope.packet)
self._outlet.set_packet_delivered_callback(envelope.packet, self._packet_delivered)
self._outlet.set_packet_timeout_callback(envelope.packet, self._packet_timeout, self._get_packet_timeout_time(envelope.tries))
self._update_packet_timeouts()
if self.window > self.window_min:
self.window -= 1
if self.window_max > (self.window_min+self.window_flexibility):
self.window_max -= 1
if self.window > self.window_min:
self.window -= 1
# TODO: Remove at some point
# RNS.log("Decreased "+str(self)+" window to "+str(self.window), RNS.LOG_DEBUG)
if should_teardown:
RNS.log("Retry count exceeded on "+str(self)+", tearing down Link.", RNS.LOG_ERROR)
self._shutdown()
self._outlet.timed_out()
return
if self.window_max > (self.window_min+self.window_flexibility):
self.window_max -= 1
# TODO: Remove at some point
# RNS.log("Decreased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
if envelope_to_resend is not None:
self._outlet.resend(envelope_to_resend.packet)
with self._lock:
self._outlet.set_packet_delivered_callback(envelope_to_resend.packet, self._packet_delivered)
self._outlet.set_packet_timeout_callback(
envelope_to_resend.packet, self._packet_timeout,
self._get_packet_timeout_time(envelope_to_resend.tries))
self._update_packet_timeouts()
already_delivered = (self._outlet.get_packet_state(envelope_to_resend.packet) == MessageState.MSGSTATE_DELIVERED)
# TODO: Remove at some point
# RNS.log("Decreased "+str(self)+" window to "+str(self.window), RNS.LOG_EXTREME)
return False
if self._outlet.get_packet_state(packet) != MessageState.MSGSTATE_DELIVERED:
self._packet_tx_op(packet, retry_envelope)
if already_delivered:
self._packet_delivered(envelope_to_resend.packet)
def send(self, message: MessageBase) -> Envelope:
"""
@@ -585,27 +603,39 @@ class Channel(contextlib.AbstractContextManager):
:param message: an instance of a ``MessageBase`` subclass
"""
envelope: Envelope | None = None
with self._lock:
if not self.is_ready_to_send():
raise ChannelException(CEType.ME_LINK_NOT_READY, f"Link is not ready")
envelope = Envelope(self._outlet, message=message, sequence=self._next_sequence)
self._next_sequence = (self._next_sequence + 1) % Channel.SEQ_MODULUS
self._emplace_envelope(envelope, self._tx_ring)
with self._send_lock:
with self._lock:
if not self.is_ready_to_send():
raise ChannelException(CEType.ME_LINK_NOT_READY, f"Link is not ready")
if envelope is None:
raise BlockingIOError()
reserved_sequence = self._next_sequence
envelope = Envelope(self._outlet, message=message, sequence=reserved_sequence)
envelope.pack()
if len(envelope.raw) > self._outlet.mdu:
raise ChannelException(CEType.ME_TOO_BIG,
f"Packed message too big for packet: {len(envelope.raw)} > {self._outlet.mdu}")
self._next_sequence = (reserved_sequence + 1) % Channel.SEQ_MODULUS
envelope.pack()
if len(envelope.raw) > self._outlet.mdu:
raise ChannelException(CEType.ME_TOO_BIG, f"Packed message too big for packet: {len(envelope.raw)} > {self._outlet.mdu}")
envelope.packet = self._outlet.send(envelope.raw)
envelope.tries += 1
self._outlet.set_packet_delivered_callback(envelope.packet, self._packet_delivered)
self._outlet.set_packet_timeout_callback(envelope.packet, self._packet_timeout, self._get_packet_timeout_time(envelope.tries))
self._update_packet_timeouts()
envelope.packet = self._outlet.send(envelope.raw)
if (envelope.packet is None
or getattr(envelope.packet, "raw", None) is None
or (hasattr(envelope.packet, "receipt") and envelope.packet.receipt is None)):
with self._lock:
self._next_sequence = reserved_sequence
raise ChannelException(CEType.ME_LINK_NOT_READY, "Outlet did not transmit packet")
with self._lock:
self._emplace_envelope(envelope, self._tx_ring)
envelope.tries += 1
self._outlet.set_packet_delivered_callback(envelope.packet, self._packet_delivered)
self._outlet.set_packet_timeout_callback(envelope.packet, self._packet_timeout, self._get_packet_timeout_time(envelope.tries))
self._update_packet_timeouts()
already_delivered = (self._outlet.get_packet_state(envelope.packet) == MessageState.MSGSTATE_DELIVERED)
# prevent _tx_ring envelope leak
if already_delivered:
self._packet_delivered(envelope.packet)
return envelope
@@ -699,7 +729,10 @@ class LinkChannelOutlet(ChannelOutletBase):
packet.receipt.set_delivery_callback(inner if callback else None)
def get_packet_id(self, packet: RNS.Packet) -> any:
if packet and hasattr(packet, "get_hash") and callable(packet.get_hash):
if (packet
and getattr(packet, "raw", None) is not None
and hasattr(packet, "get_hash")
and callable(packet.get_hash)):
return packet.get_hash()
else:
return None
+2
View File
@@ -65,4 +65,6 @@ def sha512(data):
def file_sha256(file):
if not hashlib: raise SystemError("The hashlib module is not available on this system")
# TODO: Could implement fallback for old snakes here
if not hasattr(hashlib, "file_digest"): raise SystemError("The file_digest method is not available on this system. This functionality requires Python 3.11 or later.")
else: return hashlib.file_digest(file, "sha256").digest()
+15 -8
View File
@@ -127,13 +127,15 @@ class Identity:
:returns: An :ref:`RNS.Identity<api-identity>` instance that can be used to create an outgoing :ref:`RNS.Destination<api-destination>`, or *None* if the destination is unknown.
"""
if from_identity_hash:
for destination_hash in Identity.known_destinations:
if target_hash == Identity.truncated_hash(Identity.known_destinations[destination_hash][2]):
with Identity.known_destinations_lock: destination_hashes = list(Identity.known_destinations.keys())
for destination_hash in destination_hashes:
entry = Identity.known_destinations.get(destination_hash)
if not entry: continue
if target_hash == Identity.truncated_hash(entry[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])
identity.app_data = identity_data[3]
identity.load_public_key(entry[2])
identity.app_data = entry[3]
return identity
return None
@@ -294,8 +296,11 @@ class Identity:
def _retain_identity(identity_hash):
try:
retained = False
for destination_hash in Identity.known_destinations:
if identity_hash == Identity.truncated_hash(Identity.known_destinations[destination_hash][2]):
with Identity.known_destinations_lock: destination_hashes = list(Identity.known_destinations.keys())
for destination_hash in destination_hashes:
entry = Identity.known_destinations.get(destination_hash)
if not entry: continue
if identity_hash == Identity.truncated_hash(entry[2]):
if Identity._retain_destination_data(destination_hash): retained = True
return retained
@@ -311,7 +316,9 @@ class Identity:
no_path = 0
retained = 0
never_used = 0
for destination_hash in Identity.known_destinations:
with Identity.known_destinations_lock: destination_hashes = list(Identity.known_destinations.keys())
for destination_hash in destination_hashes:
try:
if RNS.Transport.has_path(destination_hash): has_path = True
else:
+14
View File
@@ -3405,6 +3405,14 @@ class Transport:
@staticmethod
def blackhole_identity(identity_hash, until=None, reason=None):
"""
Blackholes an identity.
:param identity_hash: The identity hash to blackhole as *bytes*.
:param until: Optional unix timestamp of when the blackhole expires as *float* or *int*.
:param reason: Optional reason for the blackhole as *str*.
:returns: *True* if successful, otherwise *False*.
"""
try:
if not identity_hash in Transport.blackholed_identities:
entry = {"source": Transport.identity.hash, "until": until, "reason": reason}
@@ -3422,6 +3430,12 @@ class Transport:
@staticmethod
def unblackhole_identity(identity_hash):
"""
Lifts blackhole for an identity.
:param identity_hash: The identity hash to blackhole as *bytes*.
:returns: *True* if successful, otherwise *False*.
"""
try:
if identity_hash in Transport.blackholed_identities:
Transport.blackholed_identities.pop(identity_hash)
+11 -2
View File
@@ -978,7 +978,7 @@ class NomadNetworkNode():
# Commit message
if commit_info.get("message"):
content_parts.append(self.m_escape(commit_info["message"]) + "\n")
content_parts.append(self.format_commit(commit_info["message"]) + "\n")
content_parts.append("\n")
# Changed files
@@ -1147,7 +1147,7 @@ class NomadNetworkNode():
# Breadcrumb navigation
repo_link = self.m_link(repo_name, self.PATH_REPO, g=group_name, r=repo_name)
breadcrumb = f">>\n{self.m_link('Node', self.PATH_INDEX)} / {self.m_link(group_name, self.PATH_GROUP, g=group_name)} / {repo_link}"
breadcrumb = f">>\n{self.m_link('Node', self.PATH_INDEX)} / {self.m_link(group_name, self.PATH_GROUP, g=group_name)} / {repo_link} / stats"
nav_parts.append(breadcrumb + "\n")
repo = self.get_accessible_repository(remote_identity, group_name, repo_name)
@@ -2345,6 +2345,15 @@ class NomadNetworkNode():
return "\n".join(formatted_lines)
def format_commit(self, diff_text):
lines = diff_text.replace("\\", "\\\\").split("\n")
formatted_lines = []
for line in lines:
if line.startswith("-"): formatted_lines.append(self.m_escape(f"\\{line}"))
else: formatted_lines.append(self.m_escape(line))
return "\n".join(formatted_lines)
def repository_thanks(self, repo_path, add=False, link_id=None):
if add:
+77 -70
View File
@@ -33,6 +33,7 @@ import os
import sys
import time
import shutil
import fnmatch
import argparse
import threading
import subprocess
@@ -331,45 +332,48 @@ class ReticulumGitClient():
self.progress_enabled = True
self.transfer_label = "unknown"
if not ReticulumGitNode._ensure_git(): RNS.log("The \"git\" command is not available. Aborting server startup.", RNS.LOG_ERROR)
if configdir != None: self.configdir = configdir
else:
if configdir != None: self.configdir = configdir
else:
if os.path.isdir(self.userdir+"/.config/rngit") and os.path.isfile(self.userdir+"/.config/rngit/config"): self.configdir = self.userdir+"/.rngit/reticulum"
else: self.configdir = self.userdir+"/.rngit"
self.logfile = self.configdir+"/client_log"
self.configpath = self.configdir+"/client_config"
self.identitypath = identitypath or self.configdir+"/client_identity"
if os.path.isdir(self.userdir+"/.config/rngit") and os.path.isfile(self.userdir+"/.config/rngit/config"): self.configdir = self.userdir+"/.rngit/reticulum"
else: self.configdir = self.userdir+"/.rngit"
if not os.path.isdir(self.configdir): os.makedirs(self.configdir)
if not os.path.isfile(self.identitypath):
identity = RNS.Identity()
identity.to_file(self.identitypath)
RNS.log(f"Identity generated and persisted to {self.identitypath}", RNS.LOG_DEBUG)
else:
identity = RNS.Identity.from_file(self.identitypath)
RNS.log(f"Client identity loaded from {self.identitypath}", RNS.LOG_DEBUG)
self.logfile = self.configdir+"/client_log"
self.configpath = self.configdir+"/client_config"
self.identitypath = identitypath or self.configdir+"/client_identity"
if not identity: self.abort("Could not initialize client identity")
else: self.identity = identity
if not os.path.isdir(self.configdir): os.makedirs(self.configdir)
if os.path.isfile(self.configpath):
try: self.config = ConfigObj(self.configpath)
except Exception as e:
RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR)
RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR)
RNS.panic()
else:
RNS.log("Could not load config file, creating default configuration file...")
self.__create_default_config()
RNS.log("Default config file created. Make any necessary changes in "+self.configdir+"/config and restart rngit.")
RNS.log("Exiting now")
exit(1)
self.__ensure_identity()
self.__ensure_config()
self.__apply_config()
self.__apply_config()
def _ensure_git(self):
if not ReticulumGitNode._ensure_git(): self.abort("The \"git\" command is not available. Aborting operation.")
def __ensure_identity(self):
if not os.path.isfile(self.identitypath):
identity = RNS.Identity()
identity.to_file(self.identitypath)
RNS.log(f"Identity generated and persisted to {self.identitypath}", RNS.LOG_DEBUG)
else:
identity = RNS.Identity.from_file(self.identitypath)
RNS.log(f"Client identity loaded from {self.identitypath}", RNS.LOG_DEBUG)
if not identity: self.abort("Could not initialize client identity")
else: self.identity = identity
def __ensure_config(self):
if os.path.isfile(self.configpath):
try: self.config = ConfigObj(self.configpath)
except Exception as e:
RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR)
RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR)
RNS.panic()
else:
RNS.log("Could not load config file, creating default configuration file...")
self.__create_default_config()
RNS.log("Default config file created, make any necessary changes in "+self.configdir+"/config")
def __create_default_config(self):
from RNS.Utilities.rngit.client import __default_rngit_config__ as __default_rngit_client_config__
@@ -558,7 +562,7 @@ class ReticulumGitClient():
repo_path = f"{group}/{repo}"
request_data = {self.IDX_REPOSITORY: repo_path}
response, metadata = self.send_request(self.PATH_CREATE, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_CREATE, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -618,7 +622,7 @@ class ReticulumGitClient():
request_data = {self.IDX_REPOSITORY: repo_path, "source": source}
print(f"Remote is {operation_name.lower()}ing repository to {repo_path}...")
response, metadata = self.send_request(path, request_data, timeout=900)
response, metadata = self.send_request(path, request_data, timeout=7200)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -662,7 +666,7 @@ class ReticulumGitClient():
request_data = {self.IDX_REPOSITORY: repo_path}
print(f"Remote is syncing repository...")
response, metadata = self.send_request(self.PATH_SYNC, request_data, timeout=900)
response, metadata = self.send_request(self.PATH_SYNC, request_data, timeout=7200)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -750,7 +754,7 @@ class ReticulumGitClient():
repo_path = f"{group}/{repo}"
request_data = {self.IDX_REPOSITORY: repo_path, "operation": "list"}
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=120)
print("\r \r", end="")
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -809,7 +813,7 @@ class ReticulumGitClient():
repo_path = f"{group}/{repo}"
request_data = {self.IDX_REPOSITORY: repo_path, "operation": "view", "tag": target}
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=300)
print("\r \r", end="")
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -907,7 +911,7 @@ class ReticulumGitClient():
elif offline: return local_manifest_dir+name
self.transfer_label = name
request_data = {self.IDX_REPOSITORY: repo_path, "operation": "fetch", "tag": tag, "artifact": name}
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=30, progress=True)
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=7200, progress=True)
print("\r \r", end="")
if not response: self.abort(f"No response from remote")
@@ -947,13 +951,14 @@ class ReticulumGitClient():
manifest_out = os.path.basename(f"{release_name}_{release_version}.{self.MSG_EXT}")
with open(manifest_out, "wb") as fh: fh.write(rsg)
def match_artifacts(match_expression, manifest_artifacts):
return [entry for entry in manifest_artifacts if fnmatch.fnmatch(entry.get("name", ""), match_expression)]
fetch_artifacts = []
artifacts = release_meta.get("artifacts", [])
if not artifacts: self.abort("Release manifest contains no artifacts")
if artifact == "all": fetch_artifacts = artifacts
else:
for entry in artifacts:
if entry["name"] == artifact:
fetch_artifacts = [entry]; break
else: fetch_artifacts = match_artifacts(artifact, artifacts)
valid_count = 0
if not fetch_artifacts: self.abort("No available artifacts specified for fetch")
@@ -1067,7 +1072,7 @@ class ReticulumGitClient():
"tag": tag, "hash": commit_hash,
"notes": notes, "notes_format": "markdown" }
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote during release init")
status_byte = response[0]
@@ -1093,7 +1098,7 @@ class ReticulumGitClient():
"tag": tag, "artifact_name": artifact,
"artifact_data": artifact_data }
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=300)
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=7200)
if not response or not isinstance(response, bytes) or response[0] != 0:
error_msg = response[1:].decode("utf-8", errors="ignore") if response else "Unknown error"
@@ -1106,7 +1111,7 @@ class ReticulumGitClient():
request_data = { self.IDX_REPOSITORY: repo_path,
"operation": "create", "step": "finalize", "tag": tag }
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=300)
if not response or not isinstance(response, bytes): self.abort("No response from remote during finalize")
@@ -1119,7 +1124,7 @@ class ReticulumGitClient():
except Exception as e: self.abort(f"Error creating release: {e}")
finally:
if self.link: self.link.teardown()
if hasattr(self, "link") and self.link: self.link.teardown()
def delete_release(self, remote=None, target=None):
if not remote: self.abort(f"No remote specified")
@@ -1149,7 +1154,7 @@ class ReticulumGitClient():
request_data = { self.IDX_REPOSITORY: repo_path,
"operation": "delete", "tag": target }
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -1192,7 +1197,7 @@ class ReticulumGitClient():
request_data = { self.IDX_REPOSITORY: repo_path,
"operation": "latest", "tag": target }
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_RELEASE, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -1228,7 +1233,7 @@ class ReticulumGitClient():
request_data = {self.IDX_GROUP: group, "operation": "gperms", "step": "get"}
response, metadata = self.send_request(self.PATH_PERMS, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_PERMS, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1247,7 +1252,7 @@ class ReticulumGitClient():
request_data = {self.IDX_GROUP: group, "operation": "gperms", "step": "set", "content": content}
response, metadata = self.send_request(self.PATH_PERMS, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_PERMS, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1279,7 +1284,7 @@ class ReticulumGitClient():
request_data = {self.IDX_REPOSITORY: repo_path, "operation": "rperms", "step": "get"}
response, metadata = self.send_request(self.PATH_PERMS, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_PERMS, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1298,7 +1303,7 @@ class ReticulumGitClient():
request_data = {self.IDX_REPOSITORY: repo_path, "operation": "rperms", "step": "set", "content": content}
response, metadata = self.send_request(self.PATH_PERMS, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_PERMS, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1333,7 +1338,7 @@ class ReticulumGitClient():
repo_path = f"{group}/{repo}"
request_data = {self.IDX_REPOSITORY: repo_path, "operation": "list", "scope": scope}
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=120)
print("\r \r", end="")
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -1391,7 +1396,7 @@ class ReticulumGitClient():
repo_path = f"{group}/{repo}"
request_data = {self.IDX_REPOSITORY: repo_path, "operation": "view", "doc_id": doc_id, "scope": scope}
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=120)
print("\r \r", end="")
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -1476,7 +1481,7 @@ class ReticulumGitClient():
"title": title, "content": content, "format": "markdown",
"signature": signature }
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=600)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1521,7 +1526,7 @@ class ReticulumGitClient():
"title": title, "content": content, "format": "markdown",
"signature": signature }
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=600)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1557,7 +1562,7 @@ class ReticulumGitClient():
repo_path = f"{group}/{repo}"
request_data = {self.IDX_REPOSITORY: repo_path, "operation": "view", "doc_id": doc_id, "scope": scope}
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=600)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1579,7 +1584,7 @@ class ReticulumGitClient():
request_data = { self.IDX_REPOSITORY: repo_path, "operation": "edit", "doc_id": doc_id,
"scope": scope, "content": content, "title": title, "signature": signature }
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=600)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1619,7 +1624,7 @@ class ReticulumGitClient():
request_data = { self.IDX_REPOSITORY: repo_path,
"operation": "delete", "doc_id": doc_id, "scope": scope }
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1658,7 +1663,7 @@ class ReticulumGitClient():
"operation": "comment", "doc_id": doc_id, "scope": scope,
"content": content, "format": "markdown" }
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=600)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1696,7 +1701,7 @@ class ReticulumGitClient():
request_data = {self.IDX_REPOSITORY: repo_path,
"operation": "complete", "doc_id": doc_id}
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -1735,7 +1740,7 @@ class ReticulumGitClient():
request_data = {self.IDX_REPOSITORY: repo_path,
"operation": "activate", "doc_id": doc_id}
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
@@ -1774,7 +1779,7 @@ class ReticulumGitClient():
request_data = {self.IDX_REPOSITORY: repo_path,
"operation": "perms", "doc_id": doc_id, "step": "get"}
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -1795,7 +1800,7 @@ class ReticulumGitClient():
"operation": "perms", "doc_id": doc_id, "step": "set",
"content": content}
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=120)
if not response or not isinstance(response, bytes): self.abort("No response from remote")
status_byte = response[0]
@@ -2008,9 +2013,7 @@ class ReticulumGitNode():
else:
RNS.log("Could not load config file, creating default configuration file...")
self.__create_default_config()
RNS.log("Default config file created. Make any necessary changes in "+self.configdir+"/config and restart rngit.")
RNS.log("Exiting now")
exit(1)
RNS.log("Default config file created, make any necessary changes in "+self.configdir+"/config")
self.__apply_config()
self.__load_stats()
@@ -2219,7 +2222,7 @@ class ReticulumGitNode():
for group_name in section:
RNS.log(f"Loading repositery group \"{group_name}\"", RNS.LOG_VERBOSE)
group_path = os.path.expanduser(section[group_name])
if not os.path.isdir(group_path): RNS.log(f"The path \"{group_path}\" specified for repository group \"{group_name}\" does not exist, skipping.", RNS.LOG_ERROR)
if not os.path.isdir(group_path): RNS.log(f"The path \"{group_path}\" specified for repository group \"{group_name}\" does not exist, skipping.", RNS.LOG_WARNING)
else: self.load_repository_group(group_name, group_path)
def __resolve_identity_alias(self, alias):
@@ -3912,6 +3915,8 @@ class ReticulumGitNode():
doc_dir = d
break
if not doc_dir: return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Not found"
doc_dir = os.path.join(work_path, scope, str(doc_id))
root_path = os.path.join(doc_dir, "root")
@@ -4075,6 +4080,8 @@ class ReticulumGitNode():
doc_dir = d
break
if not doc_dir: return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Not found"
doc_dir = os.path.join(work_path, scope, str(doc_id))
root_path = os.path.join(doc_dir, "root")
+2
View File
@@ -214,6 +214,8 @@ async def _handle_error(errmsg: RNS.MessageBase):
async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosity: int, quietness: int, noid: bool, destination: str,
timeout: float, command: [str] | None = None):
global _finished, _link
if timeout is None:
timeout = RNS.Transport.PATH_REQUEST_TIMEOUT
with process.TTYRestorer(sys.stdin.fileno()) as ttyRestorer:
loop = asyncio.get_running_loop()
state = InitiatorState.IS_INITIAL
+1 -1
View File
@@ -1 +1 @@
__version__ = "1.2.9"
__version__ = "1.3.0"
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: eeadc4cc4157f9b7fb8f04414d9f0832
config: 3af44598b1ae08e3d831a262fbc2ecf1
tags: 645f666f9bcd5a90fca523b33c5a78b7
+36
View File
@@ -817,6 +817,42 @@ You can fetch individual artifacts from a release by specifying the artifact nam
This downloads only the specified artifact and verifies its signature against the manifest. If a file already exists locally, ``rngit`` verifies it against the manifest signature and skips the download if valid, making it safe to run the command multiple times. When fetching releases, ``rngit release`` will only download files that are missing or invalid according to the manifest. This means that partially completed release fetches can be continued later, if interrupted.
**Pattern Matching for Artifacts**
When fetching selective artifacts, you are not limited to exact names or the ``all`` keyword. You can use shell-style wildcard patterns to match multiple artifacts flexibly. This is particularly useful for selecting platform-specific builds, version ranges, or file types without specifying each file individually.
.. tip::
When using pattern matching, make sure to enclose the target specification in quotes. Otherwise,
your shell will probably interpret it as a shell expansion pattern *before* it is passed as an
argument to ``rngit``!
The pattern matching supports standard Unix wildcards:
- ``*`` matches any sequence of characters (including empty)
- ``?`` matches any single character
- ``[seq]`` matches any character in *seq* (for example ``[0-9]`` or ``[abc]``)
- ``[!seq]`` matches any character not in *seq*
For example, to fetch all wheel files for Python 3 across any platform:
.. code:: text
$ rngit release rns://remote_node/public/myrepo fetch "1.2.0:*-py3-*.whl"
To fetch a specific patch version when you know the major and minor version:
.. code:: text
$ rngit release rns://remote_node/public/myrepo fetch "1.2.0:myapp-1.2.?-linux-x86_64.tar.gz"
Or to retrieve all source archives:
.. code:: text
$ rngit release rns://remote_node/public/myrepo fetch "1.2.0:source_*.tgz"
If your pattern contains no wildcard characters, it must match an artifact name exactly, which is useful for fetching single, specific artifacts. When a pattern matches multiple artifacts, all matched files are fetched and verified. If no artifacts match the pattern, the fetch aborts with an error indicating no matches were found.
**Offline Verification**
Because the release manifest contains embedded signatures, you can verify the integrity of release artifacts offline, without connecting to the repository node. The ``rnid`` and ``rngit`` utilities can validate artifact signatures against ``.rsg`` and manifest files.
+2 -2
View File
@@ -210,8 +210,8 @@ This is not about "dropping out" of society. It is about building a substrate on
**Consider:**
- **The Old Way:** "My connection is slow. I should call my ISP and complain."
- **The Zen Way:** "The path is noisy. I will adjust the antenna or find a better route."
- **The Old Way:** *"My connection is slow. I should call my ISP and complain."*
- **The Zen Way:** *"The path is noisy. I will adjust the antenna or find a better route."*
By taking ownership of the infrastructure, you take ownership of your voice. You stop shouting into someone else's megaphone and start building your own. The network is no longer something that happens to you; it is something you make happen.
+1 -1
View File
@@ -1,5 +1,5 @@
const DOCUMENTATION_OPTIONS = {
VERSION: '1.2.9',
VERSION: '1.3.0',
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>Distributed Development - Reticulum Network Stack 1.2.9 documentation</title>
<title>Distributed Development - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -431,7 +431,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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>Code Examples - Reticulum Network Stack 1.2.9 documentation</title>
<title>Code Examples - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -3665,7 +3665,7 @@ will be fully on-par with natively included interfaces, including all supported
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -296,7 +296,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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>
+11 -5
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.2.9 documentation</title>
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -309,10 +309,12 @@
<h2>B</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Reticulum.blackhole_sources">blackhole_sources() (RNS.Reticulum static method)</a>
<li><a href="reference.html#RNS.Transport.blackhole_identity">blackhole_identity() (RNS.Transport static method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Reticulum.blackhole_sources">blackhole_sources() (RNS.Reticulum static method)</a>
</li>
<li><a href="reference.html#RNS.Buffer">Buffer (class in RNS)</a>
</li>
</ul></td>
@@ -792,6 +794,10 @@
<section id="U" class="genindex-section">
<h2>U</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.Transport.unblackhole_identity">unblackhole_identity() (RNS.Transport static method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#RNS.MessageBase.unpack">unpack() (RNS.MessageBase method)</a>
</li>
@@ -840,7 +846,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>Getting Started Fast - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -968,7 +968,7 @@ All other available modules will still be loaded when needed.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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>
+32 -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>Git Over Reticulum - Reticulum Network Stack 1.2.9 documentation</title>
<title>Git Over Reticulum - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -918,6 +918,34 @@ unicode_icons = yes
</pre></div>
</div>
<p>This downloads only the specified artifact and verifies its signature against the manifest. If a file already exists locally, <code class="docutils literal notranslate"><span class="pre">rngit</span></code> verifies it against the manifest signature and skips the download if valid, making it safe to run the command multiple times. When fetching releases, <code class="docutils literal notranslate"><span class="pre">rngit</span> <span class="pre">release</span></code> will only download files that are missing or invalid according to the manifest. This means that partially completed release fetches can be continued later, if interrupted.</p>
<p><strong>Pattern Matching for Artifacts</strong></p>
<p>When fetching selective artifacts, you are not limited to exact names or the <code class="docutils literal notranslate"><span class="pre">all</span></code> keyword. You can use shell-style wildcard patterns to match multiple artifacts flexibly. This is particularly useful for selecting platform-specific builds, version ranges, or file types without specifying each file individually.</p>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>When using pattern matching, make sure to enclose the target specification in quotes. Otherwise,
your shell will probably interpret it as a shell expansion pattern <em>before</em> it is passed as an
argument to <code class="docutils literal notranslate"><span class="pre">rngit</span></code>!</p>
</div>
<p>The pattern matching supports standard Unix wildcards:</p>
<ul class="simple">
<li><p><code class="docutils literal notranslate"><span class="pre">*</span></code> matches any sequence of characters (including empty)</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">?</span></code> matches any single character</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">[seq]</span></code> matches any character in <em>seq</em> (for example <code class="docutils literal notranslate"><span class="pre">[0-9]</span></code> or <code class="docutils literal notranslate"><span class="pre">[abc]</span></code>)</p></li>
<li><p><code class="docutils literal notranslate"><span class="pre">[!seq]</span></code> matches any character not in <em>seq</em></p></li>
</ul>
<p>For example, to fetch all wheel files for Python 3 across any platform:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release rns://remote_node/public/myrepo fetch &quot;1.2.0:*-py3-*.whl&quot;
</pre></div>
</div>
<p>To fetch a specific patch version when you know the major and minor version:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release rns://remote_node/public/myrepo fetch &quot;1.2.0:myapp-1.2.?-linux-x86_64.tar.gz&quot;
</pre></div>
</div>
<p>Or to retrieve all source archives:</p>
<div class="highlight-text notranslate"><div class="highlight"><pre><span></span>$ rngit release rns://remote_node/public/myrepo fetch &quot;1.2.0:source_*.tgz&quot;
</pre></div>
</div>
<p>If your pattern contains no wildcard characters, it must match an artifact name exactly, which is useful for fetching single, specific artifacts. When a pattern matches multiple artifacts, all matched files are fetched and verified. If no artifacts match the pattern, the fetch aborts with an error indicating no matches were found.</p>
<p><strong>Offline Verification</strong></p>
<p>Because the release manifest contains embedded signatures, you can verify the integrity of release artifacts offline, without connecting to the repository node. The <code class="docutils literal notranslate"><span class="pre">rnid</span></code> and <code class="docutils literal notranslate"><span class="pre">rngit</span></code> utilities can validate artifact signatures against <code class="docutils literal notranslate"><span class="pre">.rsg</span></code> and manifest files.</p>
<p><strong>For individual files:</strong></p>
@@ -1446,7 +1474,7 @@ options:
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>Communications Hardware - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -676,7 +676,7 @@ can be used with Reticulum. This includes virtual software modems such as
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="#"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -710,7 +710,7 @@ to participate in the development of Reticulum itself.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>Configuring Interfaces - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -1774,7 +1774,7 @@ interface basis under the relevant interface configuration section.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>Reticulum License - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -345,7 +345,7 @@ SOFTWARE.
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>Building Networks - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -664,7 +664,7 @@ differently than a mobile device roaming between radio cells.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.
+38 -4
View File
@@ -7,7 +7,7 @@
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
<title>API Reference - Reticulum Network Stack 1.2.9 documentation</title>
<title>API Reference - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -2234,6 +2234,38 @@ will announce it.</p>
</dl>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="RNS.Transport.blackhole_identity">
<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">blackhole_identity</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">identity_hash</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">until</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">reason</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Transport.blackhole_identity" title="Link to this definition"></a></dt>
<dd><p>Blackholes an identity.</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
<dd class="field-odd"><ul class="simple">
<li><p><strong>identity_hash</strong> The identity hash to blackhole as <em>bytes</em>.</p></li>
<li><p><strong>until</strong> Optional unix timestamp of when the blackhole expires as <em>float</em> or <em>int</em>.</p></li>
<li><p><strong>reason</strong> Optional reason for the blackhole as <em>str</em>.</p></li>
</ul>
</dd>
<dt class="field-even">Returns<span class="colon">:</span></dt>
<dd class="field-even"><p><em>True</em> if successful, otherwise <em>False</em>.</p>
</dd>
</dl>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="RNS.Transport.unblackhole_identity">
<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">unblackhole_identity</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">identity_hash</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Transport.unblackhole_identity" title="Link to this definition"></a></dt>
<dd><p>Lifts blackhole for an identity.</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
<dd class="field-odd"><p><strong>identity_hash</strong> The identity hash to blackhole as <em>bytes</em>.</p>
</dd>
<dt class="field-even">Returns<span class="colon">:</span></dt>
<dd class="field-even"><p><em>True</em> if successful, otherwise <em>False</em>.</p>
</dd>
</dl>
</dd></dl>
</dd></dl>
</section>
@@ -2472,6 +2504,8 @@ will announce it.</p>
<li><a class="reference internal" href="#RNS.Transport.next_hop_interface"><code class="docutils literal notranslate"><span class="pre">next_hop_interface()</span></code></a></li>
<li><a class="reference internal" href="#RNS.Transport.await_path"><code class="docutils literal notranslate"><span class="pre">await_path()</span></code></a></li>
<li><a class="reference internal" href="#RNS.Transport.request_path"><code class="docutils literal notranslate"><span class="pre">request_path()</span></code></a></li>
<li><a class="reference internal" href="#RNS.Transport.blackhole_identity"><code class="docutils literal notranslate"><span class="pre">blackhole_identity()</span></code></a></li>
<li><a class="reference internal" href="#RNS.Transport.unblackhole_identity"><code class="docutils literal notranslate"><span class="pre">unblackhole_identity()</span></code></a></li>
</ul>
</li>
</ul>
@@ -2485,7 +2519,7 @@ will announce it.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
<title>Search - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 documentation</span>
</a><form class="sidebar-search-container" method="get" action="#" role="search">
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
@@ -304,7 +304,7 @@
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>Programs Using Reticulum - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -513,7 +513,7 @@ plugin system for expandability.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>Support Reticulum - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -383,7 +383,7 @@ circumstances, so we rely on old-fashioned human feedback.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>Understanding Reticulum - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -1338,7 +1338,7 @@ those risks are acceptable to you.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>Using Reticulum on Your System - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -1636,7 +1636,7 @@ systemctl --user enable rnsd.service
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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.2.9 documentation</title>
<title>What is Reticulum? - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -505,7 +505,7 @@ network, and vice versa.</p>
</aside>
</div>
</div><script src="_static/documentation_options.js?v=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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>
+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>Zen of Reticulum - Reticulum Network Stack 1.2.9 documentation</title>
<title>Zen of Reticulum - Reticulum Network Stack 1.3.0 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.2.9 documentation</div></a>
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.0 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.2.9 documentation</span>
<span class="sidebar-brand-text">Reticulum Network Stack 1.3.0 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">
@@ -399,8 +399,8 @@
<p>This is not about “dropping out” of society. It is about building a substrate on which an actual <em>Society</em> can function.</p>
<p><strong>Consider:</strong></p>
<ul class="simple">
<li><p><strong>The Old Way:</strong> “My connection is slow. I should call my ISP and complain.”</p></li>
<li><p><strong>The Zen Way:</strong> “The path is noisy. I will adjust the antenna or find a better route.”</p></li>
<li><p><strong>The Old Way:</strong> <em>“My connection is slow. I should call my ISP and complain.”</em></p></li>
<li><p><strong>The Zen Way:</strong> <em>“The path is noisy. I will adjust the antenna or find a better route.”</em></p></li>
</ul>
<p>By taking ownership of the infrastructure, you take ownership of your voice. You stop shouting into someone elses megaphone and start building your own. The network is no longer something that happens to you; it is something you make happen.</p>
</section>
@@ -677,7 +677,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=e917149c"></script>
</div><script src="_static/documentation_options.js?v=1f29e9d3"></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>
+31
View File
@@ -766,6 +766,37 @@ $ rngit release rns://remote_node/public/myrepo fetch 1.2.0:myapp-1.2.0.tar.gz
This downloads only the specified artifact and verifies its signature against the manifest. If a file already exists locally, `rngit` verifies it against the manifest signature and skips the download if valid, making it safe to run the command multiple times. When fetching releases, `rngit release` will only download files that are missing or invalid according to the manifest. This means that partially completed release fetches can be continued later, if interrupted.
**Pattern Matching for Artifacts**
When fetching selective artifacts, you are not limited to exact names or the `all` keyword. You can use shell-style wildcard patterns to match multiple artifacts flexibly. This is particularly useful for selecting platform-specific builds, version ranges, or file types without specifying each file individually.
The pattern matching supports standard Unix wildcards:
- `*` matches any sequence of characters (including empty)
- `?` matches any single character
- `[seq]` matches any character in *seq* (for example `[0-9]` or `[abc]`)
- `[!seq]` matches any character not in *seq*
For example, to fetch all wheel files for Python 3 across any platform:
```text
$ rngit release rns://remote_node/public/myrepo fetch "1.2.0:*-py3-*.whl"
```
To fetch a specific patch version when you know the major and minor version:
```text
$ rngit release rns://remote_node/public/myrepo fetch "1.2.0:myapp-1.2.?-linux-x86_64.tar.gz"
```
Or to retrieve all source archives:
```text
$ rngit release rns://remote_node/public/myrepo fetch "1.2.0:source_*.tgz"
```
If your pattern contains no wildcard characters, it must match an artifact name exactly, which is useful for fetching single, specific artifacts. When a pattern matches multiple artifacts, all matched files are fetched and verified. If no artifacts match the pattern, the fetch aborts with an error indicating no matches were found.
**Offline Verification**
Because the release manifest contains embedded signatures, you can verify the integrity of release artifacts offline, without connecting to the repository node. The `rnid` and `rngit` utilities can validate artifact signatures against `.rsg` and manifest files.
+21 -1
View File
@@ -1295,4 +1295,24 @@ will announce it.
* **Parameters:**
* **destination_hash** A destination hash as *bytes*.
* **on_interface** If specified, the path request will only be sent on this interface. In normal use, Reticulum handles this automatically, and this parameter should not be used.
* **on_interface** If specified, the path request will only be sent on this interface. In normal use, Reticulum handles this automatically, and this parameter should not be used.
#### `static blackhole_identity(identity_hash, until=None, reason=None)`
Blackholes an identity.
* **Parameters:**
* **identity_hash** The identity hash to blackhole as *bytes*.
* **until** Optional unix timestamp of when the blackhole expires as *float* or *int*.
* **reason** Optional reason for the blackhole as *str*.
* **Returns:**
*True* if successful, otherwise *False*.
#### `static unblackhole_identity(identity_hash)`
Lifts blackhole for an identity.
* **Parameters:**
**identity_hash** The identity hash to blackhole as *bytes*.
* **Returns:**
*True* if successful, otherwise *False*.
+2 -2
View File
@@ -189,8 +189,8 @@ This is not about “dropping out” of society. It is about building a substrat
**Consider:**
- **The Old Way:** “My connection is slow. I should call my ISP and complain.”
- **The Zen Way:** “The path is noisy. I will adjust the antenna or find a better route.”
- **The Old Way:** *“My connection is slow. I should call my ISP and complain.”*
- **The Zen Way:** *“The path is noisy. I will adjust the antenna or find a better route.”*
By taking ownership of the infrastructure, you take ownership of your voice. You stop shouting into someone elses megaphone and start building your own. The network is no longer something that happens to you; it is something you make happen.
+36
View File
@@ -817,6 +817,42 @@ You can fetch individual artifacts from a release by specifying the artifact nam
This downloads only the specified artifact and verifies its signature against the manifest. If a file already exists locally, ``rngit`` verifies it against the manifest signature and skips the download if valid, making it safe to run the command multiple times. When fetching releases, ``rngit release`` will only download files that are missing or invalid according to the manifest. This means that partially completed release fetches can be continued later, if interrupted.
**Pattern Matching for Artifacts**
When fetching selective artifacts, you are not limited to exact names or the ``all`` keyword. You can use shell-style wildcard patterns to match multiple artifacts flexibly. This is particularly useful for selecting platform-specific builds, version ranges, or file types without specifying each file individually.
.. tip::
When using pattern matching, make sure to enclose the target specification in quotes. Otherwise,
your shell will probably interpret it as a shell expansion pattern *before* it is passed as an
argument to ``rngit``!
The pattern matching supports standard Unix wildcards:
- ``*`` matches any sequence of characters (including empty)
- ``?`` matches any single character
- ``[seq]`` matches any character in *seq* (for example ``[0-9]`` or ``[abc]``)
- ``[!seq]`` matches any character not in *seq*
For example, to fetch all wheel files for Python 3 across any platform:
.. code:: text
$ rngit release rns://remote_node/public/myrepo fetch "1.2.0:*-py3-*.whl"
To fetch a specific patch version when you know the major and minor version:
.. code:: text
$ rngit release rns://remote_node/public/myrepo fetch "1.2.0:myapp-1.2.?-linux-x86_64.tar.gz"
Or to retrieve all source archives:
.. code:: text
$ rngit release rns://remote_node/public/myrepo fetch "1.2.0:source_*.tgz"
If your pattern contains no wildcard characters, it must match an artifact name exactly, which is useful for fetching single, specific artifacts. When a pattern matches multiple artifacts, all matched files are fetched and verified. If no artifacts match the pattern, the fetch aborts with an error indicating no matches were found.
**Offline Verification**
Because the release manifest contains embedded signatures, you can verify the integrity of release artifacts offline, without connecting to the repository node. The ``rnid`` and ``rngit`` utilities can validate artifact signatures against ``.rsg`` and manifest files.
+2 -2
View File
@@ -210,8 +210,8 @@ This is not about "dropping out" of society. It is about building a substrate on
**Consider:**
- **The Old Way:** "My connection is slow. I should call my ISP and complain."
- **The Zen Way:** "The path is noisy. I will adjust the antenna or find a better route."
- **The Old Way:** *"My connection is slow. I should call my ISP and complain."*
- **The Zen Way:** *"The path is noisy. I will adjust the antenna or find a better route."*
By taking ownership of the infrastructure, you take ownership of your voice. You stop shouting into someone else's megaphone and start building your own. The network is no longer something that happens to you; it is something you make happen.
+42 -1
View File
@@ -62,7 +62,7 @@ class Packet:
def set_delivered_callback(self, callback: Callable[[Packet], None]):
self.delivered_callback = callback
def delivered(self):
with self.lock:
self.state = MessageState.MSGSTATE_DELIVERED
@@ -265,6 +265,47 @@ class TestChannel(unittest.TestCase):
self.assertEqual(MessageState.MSGSTATE_FAILED, packet.state)
self.assertFalse(envelope.tracked)
def test_send_on_failing_outlet_does_not_corrupt_state(self):
# if outlet.send() returns a packet that never reached
# the wire (LinkChannelOutlet does this when the link is not ACTIVE; the
# returned packet has raw=None), Channel.send() must not consume a
# sequence number or leave a packetless envelope in _tx_ring. Before
# the fix, the envelope was queued before outlet.send() returned, so a
# "dead" return left a raw=None envelope in the ring and silently
# advanced _next_sequence, stalling the channel on the other end.
print("Channel test send on failing outlet")
original_send = self.h.outlet.send
def ghost_send(raw):
with self.h.outlet.lock:
packet = Packet(None)
packet.state = MessageState.MSGSTATE_FAILED
self.h.outlet.packets.append(packet)
return packet
self.h.outlet.send = ghost_send
pre_sequence = self.h.channel._next_sequence
self.assertEqual(0, len(self.h.channel._tx_ring))
with self.assertRaises(RNS.Channel.ChannelException):
self.h.channel.send(MessageTest())
# Sequence must not have been consumed.
self.assertEqual(pre_sequence, self.h.channel._next_sequence)
# _tx_ring must not contain a packetless envelope.
self.assertEqual(0, len(self.h.channel._tx_ring))
# A subsequent successful send should use the same sequence number as
# was reserved for the failed attempt.
self.h.outlet.send = original_send
envelope = self.h.channel.send(MessageTest())
self.assertEqual(pre_sequence, envelope.sequence)
self.assertIsNotNone(envelope.packet)
self.assertIsNotNone(envelope.packet.raw)
self.assertTrue(envelope in self.h.channel._tx_ring)
def test_multiple_handler(self):
print("Channel test multiple handler short circuit")