mirror of
https://github.com/markqvist/Reticulum.git
synced 2026-06-23 12:24:30 -07:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 95502e2c21 | |||
| 3dd4145e62 | |||
| 1d7ddc3f8a | |||
| d731b4396c | |||
| c186a1f6b0 | |||
| a049ec8b7b | |||
| 4c93f6c7f4 | |||
| 35c7a89b19 | |||
| c86b9c9703 | |||
| 64ebdd0ee3 | |||
| 9179b914d5 | |||
| eb5d46b20b | |||
| 54c36f515b | |||
| 5c5668a4fc | |||
| eeefb60c89 | |||
| 018df10a26 | |||
| 93ead77435 | |||
| bd0e1ad0ca | |||
| d0ceeacb37 | |||
| 7d5fb6a13f | |||
| 855ef7bfd1 | |||
| 323890021a | |||
| e004e7592b | |||
| 0ebec014e5 | |||
| 1b624cc0e2 | |||
| e8d161c0d5 | |||
| e5c7dd7ec7 | |||
| 7d6ed59e6e | |||
| 11e4e7953a | |||
| a5b292ee81 | |||
| d619bafb8d | |||
| 0119a589dc | |||
| b7346bed4d | |||
| fcea57cb8e | |||
| 8d8af5e60a | |||
| 1a732ac1c1 | |||
| f827d945be | |||
| e03c4ee455 | |||
| 35e7ccb773 | |||
| a932a10492 | |||
| c5108c3a19 | |||
| 767782e425 | |||
| 60c440a3b6 | |||
| 6551a25877 | |||
| 70db2c5369 | |||
| 8ed31d0dc8 | |||
| ef1ecb35e1 | |||
| 6768f10631 | |||
| fee6a53473 | |||
| bbfa3b0aa0 | |||
| 325ae654ef | |||
| 8655a4fb37 | |||
| b30d272ee6 | |||
| cc90ac2853 | |||
| 55473f39cb | |||
| 6d73881b07 | |||
| d107cd4b42 | |||
| 33247e21b2 | |||
| 6bdc769af3 | |||
| e923ccbf1b | |||
| d402ee33a2 | |||
| d8d420745f | |||
| 524f2068cd | |||
| 5db089ff19 | |||
| 08d6780c73 | |||
| ca3f0bba6d | |||
| 830327e4a2 | |||
| f96409dfa9 | |||
| 18e2da7d2b | |||
| dfd046afb6 | |||
| 63d7f1e295 |
@@ -1,3 +1,70 @@
|
||||
### 2026-05-14: RNS 1.2.6
|
||||
|
||||
This release adds further improvements to the `rnid` and `rngit` utilities, and includes several bugfixes and other improvements.
|
||||
|
||||
**Changes**
|
||||
- Added embedded message signing, validation and viewing to `rnid`
|
||||
- Added file encryption for multiple file path inputs and shell expansions to `rnid`
|
||||
- Added file decryption for multiple file path inputs and shell expansions to `rnid`
|
||||
- Added signature creation for multiple file path inputs and shell expansions to `rnid`
|
||||
- Added signature validation of multiple file path inputs and shell expansions to `rnid`
|
||||
- Added workdoc signing and validation to `rngit`
|
||||
- Added ability to edit workdoc titles to `rngit`
|
||||
- Added ability to download workdocs via the `nomadnet` interface to `rngit`
|
||||
- Added local URL resolution to the `rngit` repository frontpage markdown readme renderer
|
||||
- Improved `rnstatus` remote monitor loop
|
||||
- Improved `rngit` workdoc page handling
|
||||
- Improved `rngit` release page rendering
|
||||
- Fixed missing none check in interface discovery sanitizer thanks to PAzter1101
|
||||
- Fixed potential race condition in interface discovery
|
||||
- Fixed `rngit` remote helper hanging on startup if no client config had been created previously, and RNS loglevel was configured at debug or higher
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `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 rns*.whl
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether it is valid. If the signature cannot be verified, the file has been tampered with and should be thrown very far away in a jiffy.
|
||||
|
||||
### 2026-05-09: RNS 1.2.5
|
||||
|
||||
This release brings substantial improvements to path request handling, and should significantly reduce overall network and local transport node processing loads. Path requests are now automatically ingress and egress limited per interface and sub-interface. Although the defaults are effective and sane, and should work right out of the box bring an end to practically all the PR and announce spam going on lately, the backend is fully configurable for both defaults and per interface, if you want to fiddle with the settings.
|
||||
|
||||
People who have written (ahem... *prompted into existence*) strange applications, that believed sending 25 random path requests every 10 seconds to try and punch holes through announce limiting, will now most likely find any potential users of such applications complaining that they are losing the ability to resolve paths alltogether, which is (entirely) by design, of course. Seriously, don't do crap like that.
|
||||
|
||||
You can read more about how the new ingress and egress controls work in the updated manual sections, in the Interfaces chapter.
|
||||
|
||||
For all node ops out there, I'd recomment updating to this at some sort of semi-expedient, but of course not un-leisurely pace, so peace and order on the networks can be restored.
|
||||
|
||||
**Changes**
|
||||
- Added path request ingress and egress control with sane defaults for transport nodes
|
||||
- Added full configurability of ingress and egress controls per interface and for instance-wide defaults
|
||||
- Significantly improved transport logic for path request and announce handling
|
||||
- Added path request frequency display to `rnstatus`
|
||||
- Added AutoInterface per-peer announe rate display to `rnstatus`
|
||||
- Added abilit to filter interfaces by burst state to `rnstatus`
|
||||
- Added hex/base32/base64 ASCII-wrapped output to `rnid` signature generator
|
||||
- Tuned default ingress control parameters
|
||||
- Fixed regression in link close handling in `rnstatus` and `rnpath` remote management handling
|
||||
- Fixed invalid handling of corrupted interface discovery files
|
||||
- Fixed announce processing edge case handling if path was cleaned while waiting for rebroadcast
|
||||
- Improved `rngit` error logging
|
||||
- Improved transport background jobs error handling
|
||||
- Fixed various edge-cases and inconsistencies in markdown rendering in `rngit`
|
||||
- Ensured canonical validation functions in `rngit`
|
||||
- Lots of other small fixes and stability improvements to `rngit`
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `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 rns-1.2.5-py3-none-any.whl
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether it is valid. If the signature cannot be verified, the file has been tampered with and should be thrown very far away in a jiffy.
|
||||
|
||||
### 2026-05-07: RNS 1.2.4
|
||||
|
||||
This release brings a complete rewrite and update to the `rnid` utility, which is now a lot more useful, and better at finding and saving identities. It also includes a bunch of other improvements, such as expanded `rngit` functionality, better transport performance and a few bugfixes. Enjoy!
|
||||
|
||||
+47
-36
@@ -6,6 +6,7 @@ import random
|
||||
import threading
|
||||
import ipaddress
|
||||
import subprocess
|
||||
from threading import Lock
|
||||
from .vendor import umsgpack as msgpack
|
||||
|
||||
NAME = 0xFF
|
||||
@@ -86,6 +87,7 @@ class InterfaceAnnouncer():
|
||||
RNS.trace_exception(e)
|
||||
|
||||
def sanitize(self, in_str):
|
||||
if in_str == None: return None
|
||||
sanitized = in_str.replace("\n", "")
|
||||
sanitized = sanitized.replace("\r", "")
|
||||
sanitized = sanitized.strip()
|
||||
@@ -374,6 +376,8 @@ class InterfaceDiscovery():
|
||||
AUTOCONNECT_TYPES = ["BackboneInterface", "TCPServerInterface"]
|
||||
DISCOVERABLE_TYPES = ["BackboneInterface", "TCPServerInterface", "I2PInterface", "RNodeInterface", "WeaveInterface", "KISSInterface"]
|
||||
|
||||
discovery_lock = Lock()
|
||||
|
||||
def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None, discover_interfaces=True):
|
||||
if not required_value: required_value = InterfaceAnnouncer.DEFAULT_STAMP_VALUE
|
||||
|
||||
@@ -401,8 +405,10 @@ class InterfaceDiscovery():
|
||||
discovery_sources = RNS.Reticulum.interface_discovery_sources()
|
||||
for filename in os.listdir(self.storagepath):
|
||||
try:
|
||||
filepath = os.path.join(self.storagepath, filename)
|
||||
with open(filepath, "rb") as f: info = msgpack.unpackb(f.read())
|
||||
with self.discovery_lock:
|
||||
filepath = os.path.join(self.storagepath, filename)
|
||||
with open(filepath, "rb") as f: info = msgpack.unpackb(f.read())
|
||||
|
||||
should_remove = False
|
||||
heard_delta = now-info["last_heard"]
|
||||
info["name"] = InterfaceAnnounceHandler.sanitize_name(info["name"])
|
||||
@@ -434,8 +440,8 @@ class InterfaceDiscovery():
|
||||
if should_append: discovered_interfaces.append(info)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while loading discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"The interface data file {os.path.join(self.storagepath, filename)} may be corrupt", RNS.LOG_ERROR)
|
||||
RNS.log(f"Error while loading discovered interface data: {e}", RNS.LOG_WARNING)
|
||||
RNS.log(f"The interface data file {os.path.join(self.storagepath, filename)} may be corrupt", RNS.LOG_WARNING)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
discovered_interfaces.sort(key=lambda info: (info["status_code"], info["value"], info["last_heard"]), reverse=True)
|
||||
@@ -453,41 +459,45 @@ class InterfaceDiscovery():
|
||||
filename = RNS.hexrep(discovery_hash, delimit=False)
|
||||
filepath = os.path.join(self.storagepath, filename)
|
||||
RNS.log(f"Discovered {interface_type} {hops} hop{ms} away with stamp value {value}: {name}", RNS.LOG_DEBUG)
|
||||
if not os.path.isfile(filepath):
|
||||
try:
|
||||
with open(filepath, "wb") as f:
|
||||
info["discovered"] = info["received"]
|
||||
info["last_heard"] = info["received"]
|
||||
info["heard_count"] = 0
|
||||
f.write(msgpack.packb(info))
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
return
|
||||
with self.discovery_lock:
|
||||
if not os.path.isfile(filepath):
|
||||
try:
|
||||
with open(filepath, "wb") as f:
|
||||
info["discovered"] = info["received"]
|
||||
info["last_heard"] = info["received"]
|
||||
info["heard_count"] = 0
|
||||
f.write(msgpack.packb(info))
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
return
|
||||
|
||||
else:
|
||||
discovered = None
|
||||
heard_count = None
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
last_info = msgpack.unpackb(f.read())
|
||||
discovered = last_info["discovered"]
|
||||
heard_count = last_info["heard_count"]
|
||||
else:
|
||||
discovered = None
|
||||
heard_count = None
|
||||
try:
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
last_info = msgpack.unpackb(f.read())
|
||||
discovered = last_info["discovered"]
|
||||
heard_count = last_info["heard_count"]
|
||||
|
||||
if discovered == None: discovered = info["discovered"]
|
||||
if heard_count == None: heard_count = 0
|
||||
except Exception as e: RNS.log(f"Error while reading existing data for discovered interface, re-creating data", RNS.LOG_ERROR)
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
info["discovered"] = discovered
|
||||
info["last_heard"] = info["received"]
|
||||
info["heard_count"] = heard_count+1
|
||||
f.write(msgpack.packb(info))
|
||||
if discovered == None: discovered = info["received"]
|
||||
if heard_count == None: heard_count = 0
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
return
|
||||
with open(filepath, "wb") as f:
|
||||
info["discovered"] = discovered
|
||||
info["last_heard"] = info["received"]
|
||||
info["heard_count"] = heard_count+1
|
||||
f.write(msgpack.packb(info))
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error processing discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
@@ -658,10 +668,11 @@ class InterfaceDiscovery():
|
||||
RNS.log(f"Auto-connecting discovered {interface_type} {interface_name}")
|
||||
interface.autoconnect_hash = endpoint_hash
|
||||
interface.autoconnect_source = info["network_id"]
|
||||
mode = RNS.Interfaces.Interface.Interface.MODE_GATEWAY if RNS.Reticulum.transport_enabled() else None
|
||||
ar_target = RNS.Reticulum.get_instance()._default_ar_target() if RNS.Reticulum.transport_enabled() else None
|
||||
ar_penalty = RNS.Reticulum.get_instance()._default_ar_penalty() if RNS.Reticulum.transport_enabled() else None
|
||||
ar_grace = RNS.Reticulum.get_instance()._default_ar_grace() if RNS.Reticulum.transport_enabled() else None
|
||||
RNS.Reticulum.get_instance()._add_interface(interface, ifac_netname=ifac_netname, ifac_netkey=ifac_netkey, configured_bitrate=5E6,
|
||||
RNS.Reticulum.get_instance()._add_interface(interface, mode=mode, ifac_netname=ifac_netname, ifac_netkey=ifac_netkey, configured_bitrate=5E6,
|
||||
announce_rate_target=ar_target, announce_rate_grace=ar_grace, announce_rate_penalty=ar_penalty)
|
||||
self.monitor_interface(interface)
|
||||
|
||||
|
||||
@@ -539,6 +539,11 @@ class AutoInterface(Interface):
|
||||
spawned_interface.ic_new_time = self.ic_new_time
|
||||
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
|
||||
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
|
||||
|
||||
spawned_interface.egress_control = self.egress_control
|
||||
spawned_interface.ec_pr_freq = self.ec_pr_freq
|
||||
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
|
||||
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
|
||||
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
|
||||
@@ -156,6 +156,58 @@ class BackboneInterface(Interface):
|
||||
else:
|
||||
raise SystemError("Insufficient parameters to create listener")
|
||||
|
||||
|
||||
__last_ic_burst_check = 0
|
||||
__last_ic_burst_state = False
|
||||
@property
|
||||
def ic_burst_active(self):
|
||||
if time.time() > self.__last_ic_burst_check + 2:
|
||||
self.__last_ic_burst_state = any(i.ic_burst_active for i in self.spawned_interfaces)
|
||||
|
||||
return self.__last_ic_burst_state
|
||||
|
||||
@ic_burst_active.setter
|
||||
def ic_burst_active(self, value): pass
|
||||
|
||||
__ic_burst_activated_check = 0
|
||||
__ic_burst_activated = 0
|
||||
@property
|
||||
def ic_burst_activated(self):
|
||||
if time.time() > self.__ic_burst_activated_check + 2:
|
||||
activated = [i.ic_burst_activated for i in self.spawned_interfaces if i.ic_burst_active]
|
||||
if activated: self.__ic_burst_activated = min(activated)
|
||||
|
||||
return self.__ic_burst_activated
|
||||
|
||||
@ic_burst_activated.setter
|
||||
def ic_burst_activated(self, value): pass
|
||||
|
||||
|
||||
__last_ic_pr_burst_check = 0
|
||||
__last_ic_pr_burst_state = False
|
||||
@property
|
||||
def ic_pr_burst_active(self):
|
||||
if time.time() > self.__last_ic_pr_burst_check + 2:
|
||||
self.__last_ic_pr_burst_state = any(i.ic_pr_burst_active for i in self.spawned_interfaces)
|
||||
|
||||
return self.__last_ic_pr_burst_state
|
||||
|
||||
@ic_pr_burst_active.setter
|
||||
def ic_pr_burst_active(self, value): pass
|
||||
|
||||
__ic_pr_burst_activated_check = 0
|
||||
__ic_pr_burst_activated = 0
|
||||
@property
|
||||
def ic_pr_burst_activated(self):
|
||||
if time.time() > self.__ic_pr_burst_activated_check + 2:
|
||||
activated = [i.ic_pr_burst_activated for i in self.spawned_interfaces if i.ic_pr_burst_active]
|
||||
if activated: self.__ic_pr_burst_activated = min(activated)
|
||||
|
||||
return self.__ic_pr_burst_activated
|
||||
|
||||
@ic_pr_burst_activated.setter
|
||||
def ic_pr_burst_activated(self, value): pass
|
||||
|
||||
@staticmethod
|
||||
def start():
|
||||
if not BackboneInterface._job_active: threading.Thread(target=BackboneInterface.__job, daemon=True).start()
|
||||
@@ -196,17 +248,17 @@ class BackboneInterface(Interface):
|
||||
@staticmethod
|
||||
def register_in(fileno):
|
||||
if fileno < 0:
|
||||
RNS.log(f"Attempt to register invalid file descriptor {fileno}", RNS.LOG_ERROR)
|
||||
RNS.log(f"Attempt to register invalid file descriptor {fileno}", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
try: BackboneInterface.epoll.register(fileno, select.EPOLLIN)
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while registering EPOLL_IN for file descriptor {fileno}: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"An error occurred while registering EPOLL_IN for file descriptor {fileno}: {e}", RNS.LOG_WARNING)
|
||||
|
||||
@staticmethod
|
||||
def deregister_fileno(fileno):
|
||||
if fileno < 0:
|
||||
RNS.log(f"Attempt to deregister invalid file descriptor {fileno}", RNS.LOG_ERROR)
|
||||
RNS.log(f"Attempt to deregister invalid file descriptor {fileno}", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
try: BackboneInterface.epoll.unregister(fileno)
|
||||
@@ -288,7 +340,7 @@ class BackboneInterface(Interface):
|
||||
except Exception as e: RNS.log(f"Error while removing spawned interface from {pif}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
try: client_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_WARNING)
|
||||
spawned_interface.receive(b"")
|
||||
|
||||
spawned_interface.transmit_buffer = spawned_interface.transmit_buffer[written:]
|
||||
@@ -320,18 +372,24 @@ class BackboneInterface(Interface):
|
||||
elif fileno in BackboneInterface.listener_filenos:
|
||||
owner_interface, server_socket = BackboneInterface.listener_filenos[fileno]
|
||||
if fileno == server_socket.fileno() and (event & select.EPOLLIN):
|
||||
client_socket, address = server_socket.accept()
|
||||
client_socket.setblocking(0)
|
||||
if not owner_interface.incoming_connection(client_socket):
|
||||
try:
|
||||
client_socket, address = server_socket.accept()
|
||||
client_socket.setblocking(0)
|
||||
if not owner_interface.incoming_connection(client_socket):
|
||||
try: client_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_WARNING)
|
||||
|
||||
except:
|
||||
RNS.log(f"Accepting socket failed for incoming connection: {e}", RNS.LOG_WARNING)
|
||||
try: client_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"Error while closing socket for failed incoming socket accept: {e}", RNS.LOG_WARNING)
|
||||
|
||||
elif fileno == server_socket.fileno() and (event & select.EPOLLHUP):
|
||||
try: BackboneInterface.deregister_fileno(fileno)
|
||||
except Exception as e: RNS.log(f"Error while deregistering listener file descriptor {fileno}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
try: server_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing listener socket for {server_socket}: {e}", RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"Error while closing listener socket for {server_socket}: {e}", RNS.LOG_WARNING)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"BackboneInterface error: {e}", RNS.LOG_ERROR)
|
||||
@@ -356,6 +414,11 @@ class BackboneInterface(Interface):
|
||||
spawned_interface.ic_new_time = self.ic_new_time
|
||||
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
|
||||
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
|
||||
|
||||
spawned_interface.egress_control = self.egress_control
|
||||
spawned_interface.ec_pr_freq = self.ec_pr_freq
|
||||
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
|
||||
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
|
||||
|
||||
spawned_interface.socket = socket
|
||||
spawned_interface.target_ip = socket.getpeername()[0]
|
||||
@@ -408,6 +471,12 @@ class BackboneInterface(Interface):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.ip_freq_deque.append(time.time())
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.op_freq_deque.append(time.time())
|
||||
|
||||
def process_outgoing(self, data):
|
||||
pass
|
||||
|
||||
@@ -578,8 +647,8 @@ class BackboneClientInterface(Interface):
|
||||
|
||||
except Exception as e:
|
||||
if initial:
|
||||
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("Leaving unconnected and retrying connection in "+str(BackboneClientInterface.RECONNECT_WAIT)+" seconds.", RNS.LOG_ERROR)
|
||||
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_WARNING)
|
||||
RNS.log("Leaving unconnected and retrying connection in "+str(BackboneClientInterface.RECONNECT_WAIT)+" seconds.", RNS.LOG_WARNING)
|
||||
return False
|
||||
|
||||
else:
|
||||
@@ -602,7 +671,7 @@ class BackboneClientInterface(Interface):
|
||||
attempts += 1
|
||||
|
||||
if self.max_reconnect_tries != None and attempts > self.max_reconnect_tries:
|
||||
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_ERROR)
|
||||
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_WARNING)
|
||||
self.teardown()
|
||||
break
|
||||
|
||||
|
||||
@@ -957,6 +957,11 @@ class I2PInterface(Interface):
|
||||
spawned_interface.ic_new_time = self.ic_new_time
|
||||
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
|
||||
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
|
||||
|
||||
spawned_interface.egress_control = self.egress_control
|
||||
spawned_interface.ec_pr_freq = self.ec_pr_freq
|
||||
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
|
||||
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
|
||||
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.online = True
|
||||
@@ -1003,6 +1008,12 @@ class I2PInterface(Interface):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.ip_freq_deque.append(time.time())
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.op_freq_deque.append(time.time())
|
||||
|
||||
def detach(self):
|
||||
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
|
||||
self.i2p.stop()
|
||||
|
||||
+115
-29
@@ -55,8 +55,15 @@ class Interface:
|
||||
|
||||
# How many samples to use for announce
|
||||
# frequency calculations
|
||||
IA_FREQ_SAMPLES = 128
|
||||
OA_FREQ_SAMPLES = 128
|
||||
IA_FREQ_SAMPLES = 48
|
||||
OA_FREQ_SAMPLES = 48
|
||||
IP_FREQ_SAMPLES = 48
|
||||
OP_FREQ_SAMPLES = 48
|
||||
|
||||
AR_MINFREQ_HZ = 0.1
|
||||
PR_MINFREQ_HZ = 0.1
|
||||
AR_FREQ_DECAY = 1/AR_MINFREQ_HZ
|
||||
PR_FREQ_DECAY = 1/PR_MINFREQ_HZ
|
||||
|
||||
# Maximum amount of ingress limited announces
|
||||
# to hold at any given time.
|
||||
@@ -66,12 +73,17 @@ class Interface:
|
||||
# considered to be newly created. Two
|
||||
# hours by default.
|
||||
IC_NEW_TIME = 2*60*60
|
||||
IC_BURST_FREQ_NEW = 6
|
||||
IC_BURST_FREQ = 35
|
||||
IC_BURST_HOLD = 1*60
|
||||
IC_BURST_FREQ_NEW = 3
|
||||
IC_BURST_FREQ = 10
|
||||
IC_PR_BURST_FREQ_NEW = 3
|
||||
IC_PR_BURST_FREQ = 8
|
||||
IC_BURST_HOLD = 15
|
||||
IC_BURST_PENALTY = 15
|
||||
IC_HELD_RELEASE_INTERVAL = 2
|
||||
IC_DEQUE_MIN_SAMPLE = 32
|
||||
IC_HELD_RELEASE_INTERVAL = 5
|
||||
IC_DEQUE_MIN_SAMPLE = 2
|
||||
IC_BURST_MIN_SAMPLES = 6
|
||||
EC_PR_FREQ = 5
|
||||
EGRESS_CONTROL = False
|
||||
|
||||
# Default announce rate targets
|
||||
DEFAULT_AR_TARGET = 3600
|
||||
@@ -90,29 +102,38 @@ class Interface:
|
||||
self.bitrate = 62500
|
||||
self.HW_MTU = None
|
||||
|
||||
self.supports_discovery = False
|
||||
self.discoverable = False
|
||||
self.last_discovery_announce = 0
|
||||
self.bootstrap_only = False
|
||||
self.parent_interface = None
|
||||
self.spawned_interfaces = None
|
||||
self.tunnel_id = None
|
||||
self.ingress_control = True
|
||||
self.phy_keepalive = False
|
||||
self.ic_max_held_announces = Interface.MAX_HELD_ANNOUNCES
|
||||
self.ic_burst_hold = Interface.IC_BURST_HOLD
|
||||
self.ic_burst_active = False
|
||||
self.ic_burst_activated = 0
|
||||
self.ic_held_release = 0
|
||||
self.ic_burst_freq_new = Interface.IC_BURST_FREQ_NEW
|
||||
self.ic_burst_freq = Interface.IC_BURST_FREQ
|
||||
self.ic_new_time = Interface.IC_NEW_TIME
|
||||
self.ic_burst_penalty = Interface.IC_BURST_PENALTY
|
||||
self.ic_held_release_interval = Interface.IC_HELD_RELEASE_INTERVAL
|
||||
self.held_announces = {}
|
||||
self.supports_discovery = False
|
||||
self.discoverable = False
|
||||
self.last_discovery_announce = 0
|
||||
self.bootstrap_only = False
|
||||
self.parent_interface = None
|
||||
self.spawned_interfaces = None
|
||||
self.tunnel_id = None
|
||||
self.ingress_control = True
|
||||
self.phy_keepalive = False
|
||||
|
||||
self.ic_burst_active = False
|
||||
self.ic_burst_activated = 0
|
||||
self.ic_pr_burst_active = False
|
||||
self.ic_pr_burst_activated = 0
|
||||
self.ic_held_release = 0
|
||||
self.ic_max_held_announces = RNS.Reticulum.get_instance()._default_ic_max_held_announces()
|
||||
self.ic_burst_hold = RNS.Reticulum.get_instance()._default_ic_burst_hold()
|
||||
self.ic_burst_freq_new = RNS.Reticulum.get_instance()._default_ic_burst_freq_new()
|
||||
self.ic_burst_freq = RNS.Reticulum.get_instance()._default_ic_burst_freq()
|
||||
self.ic_pr_burst_freq_new = RNS.Reticulum.get_instance()._default_ic_pr_burst_freq_new()
|
||||
self.ic_pr_burst_freq = RNS.Reticulum.get_instance()._default_ic_pr_burst_freq()
|
||||
self.ic_new_time = RNS.Reticulum.get_instance()._default_ic_new_time()
|
||||
self.ic_burst_penalty = RNS.Reticulum.get_instance()._default_ic_burst_penalty()
|
||||
self.ic_held_release_interval = RNS.Reticulum.get_instance()._default_ic_held_release_interval()
|
||||
self.ec_pr_freq = RNS.Reticulum.get_instance()._default_ec_pr_freq()
|
||||
self.egress_control = RNS.Reticulum.get_instance()._default_egress_control()
|
||||
self.held_announces = {}
|
||||
|
||||
self.ia_freq_deque = deque(maxlen=Interface.IA_FREQ_SAMPLES)
|
||||
self.oa_freq_deque = deque(maxlen=Interface.OA_FREQ_SAMPLES)
|
||||
self.ip_freq_deque = deque(maxlen=Interface.IA_FREQ_SAMPLES)
|
||||
self.op_freq_deque = deque(maxlen=Interface.OA_FREQ_SAMPLES)
|
||||
|
||||
def get_hash(self):
|
||||
return RNS.Identity.full_hash(str(self).encode("utf-8"))
|
||||
@@ -128,7 +149,7 @@ class Interface:
|
||||
|
||||
if self.ic_burst_active:
|
||||
if ia_freq < freq_threshold and time.time() > self.ic_burst_activated+self.ic_burst_hold:
|
||||
self.ic_burst_active = False
|
||||
if len(self.ia_freq_deque) >= self.IC_BURST_MIN_SAMPLES: self.ic_burst_active = False
|
||||
|
||||
return True
|
||||
|
||||
@@ -143,6 +164,37 @@ class Interface:
|
||||
|
||||
else: return False
|
||||
|
||||
def should_ingress_limit_pr(self):
|
||||
if self.ingress_control:
|
||||
freq_threshold = self.ic_pr_burst_freq_new if self.age() < self.ic_new_time else self.ic_pr_burst_freq
|
||||
ip_freq = self.incoming_pr_frequency()
|
||||
|
||||
if self.ic_pr_burst_active:
|
||||
if ip_freq < freq_threshold and time.time() > self.ic_pr_burst_activated+self.ic_burst_hold:
|
||||
self.ic_pr_burst_active = False
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
if ip_freq > freq_threshold:
|
||||
self.ic_pr_burst_active = True
|
||||
self.ic_pr_burst_activated = time.time()
|
||||
return True
|
||||
|
||||
else: return False
|
||||
|
||||
else: return False
|
||||
|
||||
def should_egress_limit_pr(self):
|
||||
if self.egress_control:
|
||||
freq_threshold = self.ec_pr_freq
|
||||
op_freq = self.outgoing_pr_frequency()
|
||||
|
||||
if op_freq > freq_threshold:
|
||||
if len(self.op_freq_deque) >= self.IC_BURST_MIN_SAMPLES: return True
|
||||
|
||||
return False
|
||||
|
||||
def optimise_mtu(self):
|
||||
if self.AUTOCONFIGURE_MTU:
|
||||
if self.bitrate >= 1_000_000_000:
|
||||
@@ -168,7 +220,7 @@ class Interface:
|
||||
else:
|
||||
self.HW_MTU = None
|
||||
|
||||
RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_DEBUG) # TODO: Remove debug
|
||||
RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_DEBUG)
|
||||
|
||||
def age(self):
|
||||
return time.time()-self.created
|
||||
@@ -214,12 +266,23 @@ class Interface:
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.sent_announce(from_spawned=True)
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
self.ip_freq_deque.append(time.time())
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.received_path_request(from_spawned=True)
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
self.op_freq_deque.append(time.time())
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.sent_path_request(from_spawned=True)
|
||||
|
||||
def incoming_announce_frequency(self):
|
||||
n = len(self.ia_freq_deque)
|
||||
if not n > self.IC_DEQUE_MIN_SAMPLE: return 0
|
||||
else:
|
||||
oldest = self.ia_freq_deque[0]
|
||||
span = time.time() - oldest
|
||||
if span > self.AR_FREQ_DECAY: self.ia_freq_deque.popleft()
|
||||
if span <= 0: return 0
|
||||
hz = n / span
|
||||
return hz
|
||||
@@ -230,6 +293,29 @@ class Interface:
|
||||
else:
|
||||
oldest = self.oa_freq_deque[0]
|
||||
span = time.time() - oldest
|
||||
if span > self.AR_FREQ_DECAY: self.oa_freq_deque.popleft()
|
||||
if span <= 0: return 0
|
||||
hz = n / span
|
||||
return hz
|
||||
|
||||
def incoming_pr_frequency(self):
|
||||
n = len(self.ip_freq_deque)
|
||||
if not n > self.IC_DEQUE_MIN_SAMPLE: return 0
|
||||
else:
|
||||
oldest = self.ip_freq_deque[0]
|
||||
span = time.time() - oldest
|
||||
if span > self.PR_FREQ_DECAY: self.ip_freq_deque.popleft()
|
||||
if span <= 0: return 0
|
||||
hz = n / span
|
||||
return hz
|
||||
|
||||
def outgoing_pr_frequency(self):
|
||||
n = len(self.op_freq_deque)
|
||||
if not len(self.op_freq_deque) > 1: return 0
|
||||
else:
|
||||
oldest = self.op_freq_deque[0]
|
||||
span = time.time() - oldest
|
||||
if span > self.PR_FREQ_DECAY: self.op_freq_deque.popleft()
|
||||
if span <= 0: return 0
|
||||
hz = n / span
|
||||
return hz
|
||||
|
||||
@@ -488,6 +488,12 @@ class LocalServerInterface(Interface):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.ip_freq_deque.append(time.time())
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.op_freq_deque.append(time.time())
|
||||
|
||||
def __str__(self):
|
||||
if self.socket_path: return "Shared Instance["+str(self.socket_path.replace("\0", ""))+"]"
|
||||
else: return "Shared Instance["+str(self.bind_port)+"]"
|
||||
|
||||
@@ -549,6 +549,12 @@ class RNodeMultiInterface(Interface):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.ip_freq_deque.append(time.time())
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.op_freq_deque.append(time.time())
|
||||
|
||||
def readLoop(self):
|
||||
try:
|
||||
in_frame = False
|
||||
|
||||
@@ -589,6 +589,11 @@ class TCPServerInterface(Interface):
|
||||
spawned_interface.ic_burst_penalty = self.ic_burst_penalty
|
||||
spawned_interface.ic_held_release_interval = self.ic_held_release_interval
|
||||
|
||||
spawned_interface.egress_control = self.egress_control
|
||||
spawned_interface.ec_pr_freq = self.ec_pr_freq
|
||||
spawned_interface.ic_pr_burst_freq_new = self.ic_pr_burst_freq_new
|
||||
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
|
||||
|
||||
spawned_interface.target_ip = handler.client_address[0]
|
||||
spawned_interface.target_port = str(handler.client_address[1])
|
||||
spawned_interface.parent_interface = self
|
||||
@@ -634,6 +639,12 @@ class TCPServerInterface(Interface):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def received_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.ip_freq_deque.append(time.time())
|
||||
|
||||
def sent_path_request(self, from_spawned=False):
|
||||
if from_spawned: self.op_freq_deque.append(time.time())
|
||||
|
||||
def process_outgoing(self, data):
|
||||
pass
|
||||
|
||||
|
||||
+2
-1
@@ -117,7 +117,7 @@ class Packet:
|
||||
__slots__ = "hops", "header", "header_type", "packet_type", "transport_type", "context", "context_flag", "destination"
|
||||
__slots__ += "transport_id", "data", "flags", "raw", "packed", "sent", "create_receipt", "receipt", "fromPacked", "MTU"
|
||||
__slots__ += "sent_at", "packet_hash", "ratchet_id", "attached_interface", "receiving_interface", "rssi", "snr", "q"
|
||||
__slots__ += "ciphertext", "plaintext", "destination_hash", "destination_type", "link", "map_hash"
|
||||
__slots__ += "ciphertext", "plaintext", "destination_hash", "destination_type", "link", "map_hash", "is_outbound_pr"
|
||||
|
||||
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST,
|
||||
header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True, context_flag=FLAG_UNSET):
|
||||
@@ -161,6 +161,7 @@ class Packet:
|
||||
|
||||
self.attached_interface = attached_interface
|
||||
self.receiving_interface = None
|
||||
self.is_outbound_pr = False
|
||||
self.rssi = None
|
||||
self.snr = None
|
||||
self.q = None
|
||||
|
||||
+122
-15
@@ -249,22 +249,33 @@ class Reticulum:
|
||||
Reticulum.blackholepath = Reticulum.configdir+"/storage/blackhole"
|
||||
Reticulum.interfacepath = Reticulum.configdir+"/interfaces"
|
||||
|
||||
Reticulum.__network_identity = None
|
||||
Reticulum.__transport_enabled = False
|
||||
Reticulum.__link_mtu_discovery = Reticulum.LINK_MTU_DISCOVERY
|
||||
Reticulum.__remote_management_enabled = False
|
||||
Reticulum.__use_implicit_proof = True
|
||||
Reticulum.__allow_probes = False
|
||||
Reticulum.__discovery_enabled = False
|
||||
Reticulum.__discover_interfaces = False
|
||||
Reticulum.__network_identity = None
|
||||
Reticulum.__transport_enabled = False
|
||||
Reticulum.__link_mtu_discovery = Reticulum.LINK_MTU_DISCOVERY
|
||||
Reticulum.__remote_management_enabled = False
|
||||
Reticulum.__use_implicit_proof = True
|
||||
Reticulum.__allow_probes = False
|
||||
Reticulum.__discovery_enabled = False
|
||||
Reticulum.__discover_interfaces = False
|
||||
Reticulum.__autoconnect_discovered_interfaces = False
|
||||
Reticulum.__required_discovery_value = None
|
||||
Reticulum.__publish_blackhole = False
|
||||
Reticulum.__blackhole_sources = []
|
||||
Reticulum.__interface_sources = []
|
||||
Reticulum.__default_ar_target = None
|
||||
Reticulum.__default_ar_penalty = None
|
||||
Reticulum.__default_ar_grace = None
|
||||
Reticulum.__required_discovery_value = None
|
||||
Reticulum.__publish_blackhole = False
|
||||
Reticulum.__blackhole_sources = []
|
||||
Reticulum.__interface_sources = []
|
||||
Reticulum.__default_ar_target = None
|
||||
Reticulum.__default_ar_penalty = None
|
||||
Reticulum.__default_ar_grace = None
|
||||
Reticulum.__ic_max_held_announces = None
|
||||
Reticulum.__ic_burst_hold = None
|
||||
Reticulum.__ic_burst_freq_new = None
|
||||
Reticulum.__ic_burst_freq = None
|
||||
Reticulum.__ic_pr_burst_freq_new = None
|
||||
Reticulum.__ic_pr_burst_freq = None
|
||||
Reticulum.__ic_new_time = None
|
||||
Reticulum.__ic_burst_penalty = None
|
||||
Reticulum.__ic_held_release_interval = None
|
||||
Reticulum.__ec_pr_freq = None
|
||||
Reticulum.__egress_control = None
|
||||
|
||||
Reticulum.panic_on_interface_error = False
|
||||
|
||||
@@ -596,6 +607,51 @@ class Reticulum:
|
||||
v = self.config["reticulum"].as_int(option)
|
||||
if v >= 0: Reticulum.__default_ar_grace = v
|
||||
|
||||
if option == "ic_max_held_announces":
|
||||
v = self.config["reticulum"].as_int(option)
|
||||
if v >= 0: Reticulum.__ic_max_held_announces = v
|
||||
|
||||
if option == "ic_burst_hold":
|
||||
v = self.config["reticulum"].as_float(option)
|
||||
if v >= 0: Reticulum.__ic_burst_hold = v
|
||||
|
||||
if option == "ic_burst_freq_new":
|
||||
v = self.config["reticulum"].as_float(option)
|
||||
if v >= 0: Reticulum.__ic_burst_freq_new = v
|
||||
|
||||
if option == "ic_burst_freq":
|
||||
v = self.config["reticulum"].as_float(option)
|
||||
if v >= 0: Reticulum.__ic_burst_freq = v
|
||||
|
||||
if option == "ic_pr_burst_freq_new":
|
||||
v = self.config["reticulum"].as_float(option)
|
||||
if v >= 0: Reticulum.__ic_pr_burst_freq_new = v
|
||||
|
||||
if option == "ic_pr_burst_freq":
|
||||
v = self.config["reticulum"].as_float(option)
|
||||
if v >= 0: Reticulum.__ic_pr_burst_freq = v
|
||||
|
||||
if option == "ec_pr_freq":
|
||||
v = self.config["reticulum"].as_float(option)
|
||||
if v >= 0: Reticulum.__ec_pr_freq = v
|
||||
|
||||
if option == "egress_control":
|
||||
v = self.config["reticulum"].as_bool(option)
|
||||
if v >= 0: Reticulum.__egress_control = v
|
||||
|
||||
if option == "ic_new_time":
|
||||
v = self.config["reticulum"].as_float(option)
|
||||
if v >= 0: Reticulum.__ic_new_time = v
|
||||
|
||||
if option == "ic_burst_penalty":
|
||||
v = self.config["reticulum"].as_float(option)
|
||||
if v >= 0: Reticulum.__ic_burst_penalty = v
|
||||
|
||||
if option == "ic_held_release_interval":
|
||||
v = self.config["reticulum"].as_float(option)
|
||||
if v >= 0: Reticulum.__ic_held_release_interval = v
|
||||
|
||||
|
||||
if RNS.compiled: RNS.log("Reticulum running in compiled mode", RNS.LOG_DEBUG)
|
||||
else: RNS.log("Reticulum running in interpreted mode", RNS.LOG_DEBUG)
|
||||
|
||||
@@ -683,6 +739,8 @@ class Reticulum:
|
||||
|
||||
ingress_control = True
|
||||
if "ingress_control" in c: ingress_control = c.as_bool("ingress_control")
|
||||
egress_control = None
|
||||
if "egress_control" in c: egress_control = c.as_bool("egress_control")
|
||||
ic_max_held_announces = None
|
||||
if "ic_max_held_announces" in c: ic_max_held_announces = c.as_int("ic_max_held_announces")
|
||||
ic_burst_hold = None
|
||||
@@ -691,6 +749,12 @@ class Reticulum:
|
||||
if "ic_burst_freq_new" in c: ic_burst_freq_new = c.as_float("ic_burst_freq_new")
|
||||
ic_burst_freq = None
|
||||
if "ic_burst_freq" in c: ic_burst_freq = c.as_float("ic_burst_freq")
|
||||
ic_pr_burst_freq_new = None
|
||||
if "ic_pr_burst_freq_new" in c: ic_pr_burst_freq_new = c.as_float("ic_pr_burst_freq_new")
|
||||
ic_pr_burst_freq = None
|
||||
if "ic_pr_burst_freq" in c: ic_pr_burst_freq = c.as_float("ic_pr_burst_freq")
|
||||
ec_pr_freq = None
|
||||
if "ec_pr_freq" in c: ec_pr_freq = c.as_float("ec_pr_freq")
|
||||
ic_new_time = None
|
||||
if "ic_new_time" in c: ic_new_time = c.as_float("ic_new_time")
|
||||
ic_burst_penalty = None
|
||||
@@ -816,10 +880,14 @@ class Reticulum:
|
||||
interface.announce_rate_grace = announce_rate_grace
|
||||
interface.announce_rate_penalty = announce_rate_penalty
|
||||
interface.ingress_control = ingress_control
|
||||
if egress_control != None: interface.egress_control = egress_control
|
||||
if ic_max_held_announces != None: interface.ic_max_held_announces = ic_max_held_announces
|
||||
if ic_burst_hold != None: interface.ic_burst_hold = ic_burst_hold
|
||||
if ic_burst_freq_new != None: interface.ic_burst_freq_new = ic_burst_freq_new
|
||||
if ic_burst_freq != None: interface.ic_burst_freq = ic_burst_freq
|
||||
if ic_pr_burst_freq_new != None: interface.ic_pr_burst_freq_new = ic_pr_burst_freq_new
|
||||
if ic_pr_burst_freq != None: interface.ic_pr_burst_freq = ic_pr_burst_freq
|
||||
if ec_pr_freq != None: interface.ec_pr_freq = ec_pr_freq
|
||||
if ic_new_time != None: interface.ic_new_time = ic_new_time
|
||||
if ic_burst_penalty != None: interface.ic_burst_penalty = ic_burst_penalty
|
||||
if ic_held_release_interval != None: interface.ic_held_release_interval = ic_held_release_interval
|
||||
@@ -1021,6 +1089,39 @@ class Reticulum:
|
||||
def _default_ar_grace(self):
|
||||
return self.__default_ar_grace or RNS.Interfaces.Interface.Interface.DEFAULT_AR_GRACE
|
||||
|
||||
def _default_ic_max_held_announces(self):
|
||||
return self.__ic_max_held_announces or RNS.Interfaces.Interface.Interface.MAX_HELD_ANNOUNCES
|
||||
|
||||
def _default_ic_burst_hold(self):
|
||||
return self.__ic_burst_hold or RNS.Interfaces.Interface.Interface.IC_BURST_HOLD
|
||||
|
||||
def _default_ic_burst_freq_new(self):
|
||||
return self.__ic_burst_freq_new or RNS.Interfaces.Interface.Interface.IC_BURST_FREQ_NEW
|
||||
|
||||
def _default_ic_burst_freq(self):
|
||||
return self.__ic_burst_freq or RNS.Interfaces.Interface.Interface.IC_BURST_FREQ
|
||||
|
||||
def _default_ic_pr_burst_freq_new(self):
|
||||
return self.__ic_pr_burst_freq_new or RNS.Interfaces.Interface.Interface.IC_PR_BURST_FREQ_NEW
|
||||
|
||||
def _default_ic_pr_burst_freq(self):
|
||||
return self.__ic_pr_burst_freq or RNS.Interfaces.Interface.Interface.IC_PR_BURST_FREQ
|
||||
|
||||
def _default_ec_pr_freq(self):
|
||||
return self.__ec_pr_freq or RNS.Interfaces.Interface.Interface.EC_PR_FREQ
|
||||
|
||||
def _default_egress_control(self):
|
||||
return self.__egress_control or RNS.Interfaces.Interface.Interface.EGRESS_CONTROL
|
||||
|
||||
def _default_ic_new_time(self):
|
||||
return self.__ic_new_time or RNS.Interfaces.Interface.Interface.IC_NEW_TIME
|
||||
|
||||
def _default_ic_burst_penalty(self):
|
||||
return self.__ic_burst_penalty or RNS.Interfaces.Interface.Interface.IC_BURST_PENALTY
|
||||
|
||||
def _default_ic_held_release_interval(self):
|
||||
return self.__ic_held_release_interval or RNS.Interfaces.Interface.Interface.IC_HELD_RELEASE_INTERVAL
|
||||
|
||||
def _should_persist_data(self, background=False):
|
||||
if time.time() > self.last_data_persist+Reticulum.GRACIOUS_PERSIST_INTERVAL:
|
||||
def job(): self.__persist_data(background=background)
|
||||
@@ -1321,10 +1422,16 @@ class Reticulum:
|
||||
ifstats["txb"] = interface.txb
|
||||
ifstats["incoming_announce_frequency"] = interface.incoming_announce_frequency()
|
||||
ifstats["outgoing_announce_frequency"] = interface.outgoing_announce_frequency()
|
||||
ifstats["incoming_pr_frequency"] = interface.incoming_pr_frequency()
|
||||
ifstats["outgoing_pr_frequency"] = interface.outgoing_pr_frequency()
|
||||
ifstats["announce_rate_target"] = interface.announce_rate_target
|
||||
ifstats["announce_rate_penalty"] = interface.announce_rate_penalty
|
||||
ifstats["announce_rate_grace"] = interface.announce_rate_grace
|
||||
ifstats["held_announces"] = len(interface.held_announces)
|
||||
ifstats["burst_active"] = interface.ic_burst_active
|
||||
ifstats["burst_activated"] = interface.ic_burst_activated
|
||||
ifstats["pr_burst_active"] = interface.ic_pr_burst_active
|
||||
ifstats["pr_burst_activated"] = interface.ic_pr_burst_activated
|
||||
ifstats["status"] = interface.online
|
||||
ifstats["mode"] = interface.mode
|
||||
|
||||
|
||||
+122
-60
@@ -38,6 +38,7 @@ import inspect
|
||||
import threading
|
||||
from time import sleep
|
||||
from threading import Lock
|
||||
from collections import deque
|
||||
from .vendor import umsgpack as umsgpack
|
||||
from RNS.Interfaces.BackboneInterface import BackboneInterface
|
||||
|
||||
@@ -124,6 +125,7 @@ class Transport:
|
||||
discovery_path_requests = {} # A table for keeping track of path requests on behalf of other nodes
|
||||
discovery_pr_tags = [] # A table for keeping track of tagged path requests
|
||||
max_pr_tags = 32000 # Maximum amount of unique path request tags to remember
|
||||
max_queued_discovery_prs = 32 # Maximum amount of queued discovery path requests
|
||||
|
||||
interfaces_lock = Lock()
|
||||
destinations_lock = Lock()
|
||||
@@ -580,40 +582,41 @@ class Transport:
|
||||
if block_rebroadcasts: announce_context = RNS.Packet.PATH_RESPONSE
|
||||
announce_data = packet.data
|
||||
announce_identity = RNS.Identity.recall(packet.destination_hash, _no_use=True)
|
||||
announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown");
|
||||
announce_destination.hash = packet.destination_hash
|
||||
announce_destination.hexhash = announce_destination.hash.hex()
|
||||
|
||||
new_packet = RNS.Packet(
|
||||
announce_destination,
|
||||
announce_data,
|
||||
RNS.Packet.ANNOUNCE,
|
||||
context = announce_context,
|
||||
header_type = RNS.Packet.HEADER_2,
|
||||
transport_type = Transport.TRANSPORT,
|
||||
transport_id = Transport.identity.hash,
|
||||
attached_interface = attached_interface,
|
||||
context_flag = packet.context_flag,
|
||||
)
|
||||
if not announce_identity:
|
||||
RNS.log("Completed announce processing for "+RNS.prettyhexrep(destination_hash)+", the path was cleaned while waiting for announce rebroadcast", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
completed_announces.append(destination_hash)
|
||||
|
||||
new_packet.hops = announce_entry[4]
|
||||
if block_rebroadcasts:
|
||||
RNS.log("Rebroadcasting announce as path response for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
else:
|
||||
RNS.log("Rebroadcasting announce for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
outgoing.append(new_packet)
|
||||
announce_destination = RNS.Destination(announce_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "unknown", "unknown");
|
||||
announce_destination.hash = packet.destination_hash
|
||||
announce_destination.hexhash = announce_destination.hash.hex()
|
||||
|
||||
new_packet = RNS.Packet(announce_destination,
|
||||
announce_data,
|
||||
RNS.Packet.ANNOUNCE,
|
||||
context = announce_context,
|
||||
header_type = RNS.Packet.HEADER_2,
|
||||
transport_type = Transport.TRANSPORT,
|
||||
transport_id = Transport.identity.hash,
|
||||
attached_interface = attached_interface,
|
||||
context_flag = packet.context_flag)
|
||||
|
||||
# This handles an edge case where a peer sends a past
|
||||
# request for a destination just after an announce for
|
||||
# said destination has arrived, but before it has been
|
||||
# rebroadcast locally. In such a case the actual announce
|
||||
# is temporarily held, and then reinserted when the path
|
||||
# request has been served to the peer.
|
||||
if destination_hash in Transport.held_announces:
|
||||
held_entry = Transport.held_announces.pop(destination_hash)
|
||||
Transport.announce_table[destination_hash] = held_entry
|
||||
RNS.log("Reinserting held announce into table", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
new_packet.hops = announce_entry[4]
|
||||
if block_rebroadcasts: RNS.log("Rebroadcasting announce as path response for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
else: RNS.log("Rebroadcasting announce for "+RNS.prettyhexrep(announce_destination.hash)+" with hop count "+str(new_packet.hops), RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
outgoing.append(new_packet)
|
||||
|
||||
# This handles an edge case where a peer sends a past
|
||||
# request for a destination just after an announce for
|
||||
# said destination has arrived, but before it has been
|
||||
# rebroadcast locally. In such a case the actual announce
|
||||
# is temporarily held, and then reinserted when the path
|
||||
# request has been served to the peer.
|
||||
if destination_hash in Transport.held_announces:
|
||||
held_entry = Transport.held_announces.pop(destination_hash)
|
||||
Transport.announce_table[destination_hash] = held_entry
|
||||
RNS.log("Reinserting held announce into table", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
for destination_hash in completed_announces:
|
||||
if destination_hash in Transport.announce_table: Transport.announce_table.pop(destination_hash)
|
||||
@@ -772,10 +775,14 @@ class Transport:
|
||||
# Cull the pending path requests table
|
||||
stale_path_requests = []
|
||||
with Transport.path_requests_lock:
|
||||
for destination_hash in Transport.path_requests:
|
||||
if time.time() > Transport.path_requests[destination_hash] + Transport.PATH_REQUEST_GATE_TIMEOUT:
|
||||
stale_path_requests.append(destination_hash)
|
||||
RNS.log("Path request entry for "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
try:
|
||||
for destination_hash in Transport.path_requests:
|
||||
if time.time() > Transport.path_requests[destination_hash] + Transport.PATH_REQUEST_GATE_TIMEOUT:
|
||||
stale_path_requests.append(destination_hash)
|
||||
RNS.log("Path request entry for "+RNS.prettyhexrep(destination_hash)+" timed out and was removed", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not complete stale path request enumeration in this job round, retrying later: {e}", RNS.LOG_WARNING)
|
||||
|
||||
# Cull the pending discovery path requests table
|
||||
stale_discovery_path_requests = []
|
||||
@@ -917,6 +924,8 @@ class Transport:
|
||||
Transport.prioritize_interfaces()
|
||||
try:
|
||||
for interface in Transport.interfaces:
|
||||
interface.should_ingress_limit()
|
||||
interface.should_ingress_limit_pr()
|
||||
interface.process_held_announces()
|
||||
if interface.phy_keepalive: interface.send_keepalive()
|
||||
Transport.interface_last_jobs = time.time()
|
||||
@@ -980,15 +989,50 @@ class Transport:
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e) # TODO: Remove
|
||||
|
||||
for packet in outgoing: packet.send()
|
||||
if outgoing:
|
||||
def job(): Transport.handle_outgoing_announces(outgoing)
|
||||
threading.Thread(target=job).start()
|
||||
|
||||
for destination_hash in path_requests:
|
||||
blocked_if = path_requests[destination_hash]
|
||||
if blocked_if == None: Transport.request_path(destination_hash)
|
||||
else:
|
||||
for interface in Transport.interfaces:
|
||||
if interface != blocked_if: Transport.request_path(destination_hash, on_interface=interface)
|
||||
else: pass
|
||||
if path_requests:
|
||||
with Transport.discovery_pr_tx_lock:
|
||||
for destination_hash in path_requests:
|
||||
if not destination_hash in Transport.pending_discovery_prs:
|
||||
if not len(Transport.pending_discovery_prs) >= Transport.max_queued_discovery_prs:
|
||||
Transport.pending_discovery_prs.append([destination_hash, path_requests[destination_hash]])
|
||||
|
||||
if len(Transport.pending_discovery_prs):
|
||||
def job(): Transport.handle_disovery_path_requests()
|
||||
threading.Thread(target=job).start()
|
||||
|
||||
|
||||
discovery_pr_tx_throttle = 0.5
|
||||
discovery_pr_tx_lock = Lock()
|
||||
discovery_pr_handle_lock = Lock()
|
||||
pending_discovery_prs = deque(maxlen=max_queued_discovery_prs)
|
||||
@staticmethod
|
||||
def handle_disovery_path_requests():
|
||||
if Transport.discovery_pr_handle_lock.locked(): return
|
||||
with Transport.discovery_pr_handle_lock:
|
||||
while len(Transport.pending_discovery_prs):
|
||||
time.sleep(Transport.discovery_pr_tx_throttle)
|
||||
destination_hash = None
|
||||
blocked_if = None
|
||||
with Transport.discovery_pr_tx_lock:
|
||||
entry = Transport.pending_discovery_prs.popleft()
|
||||
destination_hash = entry[0]
|
||||
blocked_if = entry[1]
|
||||
|
||||
if destination_hash:
|
||||
if blocked_if == None: Transport.request_path(destination_hash)
|
||||
else:
|
||||
for interface in Transport.interfaces:
|
||||
if interface != blocked_if: Transport.request_path(destination_hash, on_interface=interface)
|
||||
else: pass
|
||||
|
||||
|
||||
@staticmethod
|
||||
def handle_outgoing_announces(outgoing):
|
||||
for packet in sorted(outgoing, key=lambda p: p.hops): packet.send()
|
||||
|
||||
@staticmethod
|
||||
def transmit(interface, raw):
|
||||
@@ -1263,6 +1307,7 @@ class Transport:
|
||||
|
||||
Transport.transmit(interface, packet.raw)
|
||||
if packet.packet_type == RNS.Packet.ANNOUNCE: interface.sent_announce()
|
||||
if packet.destination.type == RNS.Destination.PLAIN and packet.is_outbound_pr: interface.sent_path_request()
|
||||
packet_sent(packet)
|
||||
sent = True
|
||||
|
||||
@@ -1629,10 +1674,12 @@ class Transport:
|
||||
# announces, queueing rebroadcasts of these, and removal
|
||||
# of queued announce rebroadcasts once handed to the next node.
|
||||
if packet.packet_type == RNS.Packet.ANNOUNCE:
|
||||
if interface != None and RNS.Identity.validate_announce(packet, only_validate_signature=True):
|
||||
interface.received_announce()
|
||||
announce_signature_valid = RNS.Identity.validate_announce(packet, only_validate_signature=True)
|
||||
if not announce_signature_valid: return
|
||||
elif interface != None: interface.received_announce()
|
||||
announced_destination_known = packet.destination_hash in Transport.path_table
|
||||
|
||||
if not packet.destination_hash in Transport.path_table:
|
||||
if not announced_destination_known:
|
||||
# This is an unknown destination, and we'll apply
|
||||
# potential ingress limiting. Already known
|
||||
# destinations will have re-announces controlled
|
||||
@@ -1694,7 +1741,7 @@ class Transport:
|
||||
random_blob = packet.data[RNS.Identity.KEYSIZE//8+RNS.Identity.NAME_HASH_LENGTH//8:RNS.Identity.KEYSIZE//8+RNS.Identity.NAME_HASH_LENGTH//8+10]
|
||||
random_blobs = []
|
||||
with Transport.inbound_announce_lock:
|
||||
if packet.destination_hash in Transport.path_table:
|
||||
if announced_destination_known:
|
||||
random_blobs = Transport.path_table[packet.destination_hash][IDX_PT_RANDBLOBS]
|
||||
|
||||
# If we already have a path to the announced
|
||||
@@ -2141,8 +2188,7 @@ class Transport:
|
||||
RNS.log("Invalid link request proof in transport for link "+RNS.prettyhexrep(packet.destination_hash)+", dropping proof.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while transporting link request proof. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Could not transport link request proof. The contained exception was: "+str(e), RNS.LOG_WARNING)
|
||||
else:
|
||||
RNS.log("Link request proof received on wrong interface, not transporting it.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
else:
|
||||
@@ -2747,8 +2793,10 @@ class Transport:
|
||||
wait_time = (tx_time / on_interface.announce_cap)
|
||||
on_interface.announce_allowed_at = now + wait_time
|
||||
|
||||
packet.is_outbound_pr = True
|
||||
packet.send()
|
||||
Transport.path_requests[destination_hash] = time.time()
|
||||
|
||||
with Transport.path_requests_lock: Transport.path_requests[destination_hash] = time.time()
|
||||
|
||||
@staticmethod
|
||||
def remote_status_handler(path, data, request_id, link_id, remote_identity, requested_at):
|
||||
@@ -2832,6 +2880,7 @@ class Transport:
|
||||
|
||||
unique_tag = destination_hash+tag_bytes
|
||||
|
||||
if packet.receiving_interface: packet.receiving_interface.received_path_request()
|
||||
with Transport.discovery_pr_tags_lock:
|
||||
if not unique_tag in Transport.discovery_pr_tags:
|
||||
Transport.discovery_pr_tags.append(unique_tag)
|
||||
@@ -2851,15 +2900,16 @@ class Transport:
|
||||
@staticmethod
|
||||
def path_request(destination_hash, is_from_local_client, attached_interface, requestor_transport_id=None, tag=None):
|
||||
should_search_for_unknown = False
|
||||
should_ingress_limit = False
|
||||
|
||||
if attached_interface != None:
|
||||
if RNS.Reticulum.transport_enabled() and attached_interface.mode in RNS.Interfaces.Interface.Interface.DISCOVER_PATHS_FOR:
|
||||
should_search_for_unknown = True
|
||||
interface_str = " on "+str(attached_interface)
|
||||
|
||||
else: interface_str = ""
|
||||
should_ingress_limit = attached_interface.should_ingress_limit_pr()
|
||||
if RNS.Reticulum.transport_enabled():
|
||||
if attached_interface.mode in RNS.Interfaces.Interface.Interface.DISCOVER_PATHS_FOR: should_search_for_unknown = True
|
||||
|
||||
RNS.log("Path request for "+RNS.prettyhexrep(destination_hash)+interface_str, RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
if RNS.sl(RNS.LOG_DEBUG):
|
||||
interface_str = f" on {attached_interface}"
|
||||
RNS.log(f"Path request for {RNS.prettyhexrep(destination_hash)}{interface_str}", RNS.LOG_DEBUG)
|
||||
|
||||
destination_exists_on_local_client = False
|
||||
if len(Transport.local_client_interfaces) > 0:
|
||||
@@ -2886,7 +2936,7 @@ class Transport:
|
||||
received_from = Transport.path_table[destination_hash][IDX_PT_RVCD_IF]
|
||||
|
||||
if packet == None:
|
||||
RNS.log("Could not retrieve announce packet from cache while answering path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_ERROR)
|
||||
RNS.log("Could not retrieve announce packet from cache while answering path request for "+RNS.prettyhexrep(destination_hash), RNS.LOG_WARNING)
|
||||
|
||||
elif attached_interface.mode == RNS.Interfaces.Interface.Interface.MODE_ROAMING and attached_interface == received_from:
|
||||
RNS.log("Not answering path request on roaming-mode interface, since next hop is on same roaming-mode interface", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
@@ -2956,6 +3006,15 @@ class Transport:
|
||||
if destination_hash in Transport.discovery_path_requests:
|
||||
RNS.log("There is already a waiting path request for "+RNS.prettyhexrep(destination_hash)+" on behalf of path request"+interface_str, RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
else:
|
||||
# Abort recursive path request if receiving
|
||||
# interface has PR burst active, or should
|
||||
# otherwise ingress limit path requests.
|
||||
if should_ingress_limit:
|
||||
if RNS.sl(RNS.LOG_DEBUG):
|
||||
interface_str = f" for {attached_interface}" if attached_interface else ""
|
||||
RNS.log(f"Not sending recursive path request{interface_str} due to active ingress limiting", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
# Forward path request on all interfaces
|
||||
# except the requestor interface
|
||||
RNS.log("Attempting to discover unknown path to "+RNS.prettyhexrep(destination_hash)+" on behalf of path request"+interface_str, RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
@@ -2964,9 +3023,12 @@ class Transport:
|
||||
|
||||
for interface in Transport.interfaces:
|
||||
if not interface == attached_interface:
|
||||
# Use the previously extracted tag from this path request
|
||||
# on the new path requests as well, to avoid potential loops
|
||||
Transport.request_path(destination_hash, on_interface=interface, tag=tag, recursive=True)
|
||||
if interface.should_egress_limit_pr():
|
||||
RNS.log(f"Not sending recursive path request on {interface} due to active egress limiting", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
else:
|
||||
# Use the previously extracted tag from this path request
|
||||
# on the new path requests as well, to avoid potential loops
|
||||
Transport.request_path(destination_hash, on_interface=interface, tag=tag, recursive=True)
|
||||
|
||||
elif not is_from_local_client and len(Transport.local_client_interfaces) > 0:
|
||||
# Forward the path request on all local
|
||||
|
||||
@@ -138,12 +138,6 @@ class ReticulumGitClient():
|
||||
self.configpath = self.configdir+"/client_config"
|
||||
self.identitypath = self.configdir+"/client_identity"
|
||||
|
||||
RNS.logfile = self.logfile
|
||||
try: self.reticulum = RNS.Reticulum(configdir=rnsconfigdir, logdest=RNS.LOG_FILE)
|
||||
except Exception as e:
|
||||
print(f"Failed to initialize Reticulum: {e}", file=sys.stderr)
|
||||
return
|
||||
|
||||
if os.path.isfile(self.configpath):
|
||||
try: self.config = ConfigObj(self.configpath)
|
||||
except Exception as e:
|
||||
@@ -152,6 +146,12 @@ class ReticulumGitClient():
|
||||
|
||||
else: self.__create_default_config()
|
||||
|
||||
RNS.logfile = self.logfile
|
||||
try: self.reticulum = RNS.Reticulum(configdir=rnsconfigdir, logdest=RNS.LOG_FILE)
|
||||
except Exception as e:
|
||||
print(f"Failed to initialize Reticulum: {e}", file=sys.stderr)
|
||||
return
|
||||
|
||||
self.__apply_config()
|
||||
self.ready = True
|
||||
|
||||
|
||||
@@ -165,15 +165,15 @@ class SyntaxHighlighter:
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Pygments highlighting failed, falling back: {e}", RNS.LOG_WARNING)
|
||||
return self._plain_text(content)
|
||||
return self._plain_text(content).replace("\\", "\\\\")
|
||||
|
||||
# TODO: Implement Python tokenize fallback for .py files.
|
||||
# For now, route to plain text
|
||||
if filename and filename.endswith(".py"):
|
||||
return self._plain_text(content)
|
||||
return self._plain_text(content).replace("\\", "\\\\")
|
||||
|
||||
# Universal fallback
|
||||
return self._plain_text(content)
|
||||
return self._plain_text(content).replace("\\", "\\\\")
|
||||
|
||||
def _highlight_pygments(self, content, filename=None, language=None):
|
||||
from pygments.lexers import get_lexer_for_filename, guess_lexer, get_lexer_by_name
|
||||
@@ -301,7 +301,8 @@ class MicronFormatter:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _escape_value(value: str) -> str: return value.replace("`", "\\`")
|
||||
def _escape_value(value):
|
||||
return value.replace("\\", "\\\\").replace("`", "\\`")
|
||||
|
||||
# Required by Pygments formatter API, returns None for Micron
|
||||
def get_style_defs(self, arg=None): return None
|
||||
|
||||
+156
-55
@@ -37,7 +37,7 @@ import RNS
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from RNS.Utilities.rngit import APP_NAME
|
||||
from RNS.Utilities.rngit.util import MarkdownToMicron
|
||||
from RNS.Utilities.rngit.util import MarkdownToMicron, san_sha
|
||||
from RNS.Utilities.rngit.highlight import SyntaxHighlighter
|
||||
from RNS.vendor.configobj import ConfigObj
|
||||
from RNS.vendor import umsgpack as mp
|
||||
@@ -60,14 +60,15 @@ class NomadNetworkNode():
|
||||
PATH_RELEASE = "/page/release.mu"
|
||||
PATH_WORK = "/page/work.mu"
|
||||
PATH_WORK_DOC = "/page/work_doc.mu"
|
||||
PATH_ARTIFACT = "/file/artifact"
|
||||
PATH_DOWNLOAD = "/file/download"
|
||||
FILE_ARTIFACT = "/file/artifact"
|
||||
FILE_DOWNLOAD = "/file/download"
|
||||
FILE_WORKDOC = "/file/workdoc"
|
||||
|
||||
BLOB_SIZE_LIMIT = 256 * 1024
|
||||
TREE_ENTRIES_PER_PAGE = 1000
|
||||
COMMITS_PER_PAGE = 100
|
||||
SHOW_DIFF_BY_DEFAULT = True
|
||||
GIT_COMMAND_TIMEOUT = 5
|
||||
GIT_COMMAND_TIMEOUT = 8
|
||||
MAX_RENDER_WIDTH = 100
|
||||
USE_NERDFONTS = True
|
||||
|
||||
@@ -98,6 +99,10 @@ class NomadNetworkNode():
|
||||
CLR_DIM = "`F666"
|
||||
CLR_DIM_H = "`F444"
|
||||
|
||||
# Yes, I'm being intentionally weird here. If you
|
||||
# want to use tabs, three spaces is all you get.
|
||||
TAB_WIDTH = " "
|
||||
|
||||
RENDERABLE_EXTS = [".md", ".mu"]
|
||||
RENDER_DEFAULT = [".md", ".mu"]
|
||||
|
||||
@@ -217,8 +222,9 @@ class NomadNetworkNode():
|
||||
self.destination.register_request_handler(self.PATH_RELEASE, response_generator=self.serve_release_page, allow=RNS.Destination.ALLOW_ALL)
|
||||
self.destination.register_request_handler(self.PATH_WORK, response_generator=self.serve_work_page, allow=RNS.Destination.ALLOW_ALL)
|
||||
self.destination.register_request_handler(self.PATH_WORK_DOC, response_generator=self.serve_work_doc_page, allow=RNS.Destination.ALLOW_ALL)
|
||||
self.destination.register_request_handler(self.PATH_ARTIFACT, response_generator=self.serve_artifact, allow=RNS.Destination.ALLOW_ALL)
|
||||
self.destination.register_request_handler(self.PATH_DOWNLOAD, response_generator=self.serve_download, allow=RNS.Destination.ALLOW_ALL)
|
||||
self.destination.register_request_handler(self.FILE_ARTIFACT, response_generator=self.serve_artifact, allow=RNS.Destination.ALLOW_ALL)
|
||||
self.destination.register_request_handler(self.FILE_DOWNLOAD, response_generator=self.serve_download, allow=RNS.Destination.ALLOW_ALL)
|
||||
self.destination.register_request_handler(self.FILE_WORKDOC, response_generator=self.serve_wd_download, allow=RNS.Destination.ALLOW_ALL)
|
||||
|
||||
def get_template(self, template):
|
||||
filename = f"{template}.mu"
|
||||
@@ -244,6 +250,7 @@ class NomadNetworkNode():
|
||||
return None
|
||||
|
||||
def render_template(self, page_content, nav_content=None, template=None, st=None):
|
||||
page_content = self.format_tabs(page_content)
|
||||
custom_template = self.get_template(template) if template else None
|
||||
if custom_template:
|
||||
template = custom_template
|
||||
@@ -254,7 +261,6 @@ class NomadNetworkNode():
|
||||
page_content = template.replace("{PAGE_CONTENT}", page_content)
|
||||
|
||||
base_template = self.get_template("base") or self.templates["base"]
|
||||
base_template = base_template.replace("{PAGE_CONTENT}", page_content)
|
||||
base_template = base_template.replace("{NODE_NAME}", self.node_name)
|
||||
base_template = base_template.replace("{VERSION}", __version__)
|
||||
|
||||
@@ -263,6 +269,7 @@ class NomadNetworkNode():
|
||||
|
||||
gt = f"Generated in {RNS.prettytime(time.time()-st)}" if st else "Unknown generation time"
|
||||
base_template = base_template.replace("{GEN_TIME}", gt)
|
||||
base_template = base_template.replace("{PAGE_CONTENT}", page_content)
|
||||
return base_template.encode("utf-8")
|
||||
|
||||
#############################
|
||||
@@ -337,7 +344,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Group page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
|
||||
if not group_name:
|
||||
@@ -380,7 +387,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Repository page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
ref = data.get("var_ref", "HEAD") if data else "HEAD"
|
||||
@@ -449,20 +456,18 @@ class NomadNetworkNode():
|
||||
content_parts.append(self.m_divider())
|
||||
|
||||
if readme_is_markdown:
|
||||
converted = self.mdc.format_block(readme_content)
|
||||
url_scope = f":/page/blob.mu`g={group_name}|r={repo_name}|ref={ref}|path="
|
||||
mdc = MarkdownToMicron(max_width=self.MAX_RENDER_WIDTH, syntax_highlighter=self.highlighter, url_scope=url_scope)
|
||||
converted = mdc.format_block(readme_content)
|
||||
content_parts.append(converted)
|
||||
|
||||
else: content_parts.append(f"\n{readme_content}\n")
|
||||
|
||||
content_parts.append("\n")
|
||||
content_parts.append(self.m_divider())
|
||||
else: content_parts.append(f"\n{readme_content}")
|
||||
|
||||
else:
|
||||
content_parts.append(self.m_divider())
|
||||
content_parts.append("\n")
|
||||
content_parts.append(self.m_italic("No README file found in this repository."))
|
||||
|
||||
content_parts.append("\n")
|
||||
content_parts.append("\n")
|
||||
|
||||
self.owner.view_succeeded(group_name, repo_name, remote_identity)
|
||||
page_content = "".join(content_parts)
|
||||
@@ -473,7 +478,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Tree page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
ref = data.get("var_ref", "HEAD") if data else "HEAD"
|
||||
@@ -519,7 +524,7 @@ class NomadNetworkNode():
|
||||
if i == len(path_components) - 1: breadcrumb_parts.append(component) # Last component not a link
|
||||
else: breadcrumb_parts.append(self.m_link(component, self.PATH_TREE, g=group_name, r=repo_name, ref=ref, path=current_path))
|
||||
|
||||
else: breadcrumb_parts.append("") # Could be "root" or something, but a bit confusing
|
||||
else: breadcrumb_parts.append("")
|
||||
|
||||
breadcrumb = " / ".join(breadcrumb_parts)
|
||||
nav_parts.append(">>\n" + breadcrumb + "\n")
|
||||
@@ -668,11 +673,11 @@ class NomadNetworkNode():
|
||||
|
||||
breadcrumb = " / ".join(breadcrumb_parts)
|
||||
nav_parts.append(">>\n" + breadcrumb + "\n")
|
||||
sep = self.icon("sep")
|
||||
|
||||
dl_link = self.m_link("Download", self.PATH_DOWNLOAD, g=group_name, r=repo_name, ref=ref, path=file_path)
|
||||
if not renderable: nav_parts.append(f"\n{dl_link}\n")
|
||||
dl_link = self.m_link("Download", self.FILE_DOWNLOAD, g=group_name, r=repo_name, ref=ref, path=file_path)
|
||||
if not renderable: nav_parts.append(f"\nDisplaying Raw {sep} {dl_link}\n")
|
||||
else:
|
||||
sep = self.icon("sep")
|
||||
rnd_link = self.m_link("View rendered", self.PATH_BLOB, g=group_name, r=repo_name, ref=ref, path=file_path, render="y")
|
||||
raw_link = self.m_link("View raw", self.PATH_BLOB, g=group_name, r=repo_name, ref=ref, path=file_path, raw="y")
|
||||
if render: render_controls = f"Displaying Rendered {sep} {raw_link}"
|
||||
@@ -737,7 +742,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Commits page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
ref = data.get("var_ref", "HEAD") if data else "HEAD"
|
||||
@@ -820,7 +825,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Commit page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
commit_hash = data.get("var_h", "") if data else ""
|
||||
@@ -863,6 +868,7 @@ class NomadNetworkNode():
|
||||
content = self.m_heading("Error", 2) + f"\nThe hash {commit_hash} does not refer to a commit.\n"
|
||||
return self.render_template(content, st=st)
|
||||
|
||||
except subprocess.TimeoutExpired: RNS.log(f"Git command execution timed out", RNS.LOG_WARNING)
|
||||
except Exception:
|
||||
content = self.m_heading("Error", 2) + "\nCould not verify commit object.\n"
|
||||
return self.render_template(content, st=st)
|
||||
@@ -954,7 +960,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Refs page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
ref_type = data.get("var_type", "") if data else "" # "heads", "tags", or empty for both
|
||||
@@ -995,6 +1001,7 @@ class NomadNetworkNode():
|
||||
|
||||
if head_result.returncode == 0: default_branch = head_result.stdout.strip().replace("refs/heads/", "")
|
||||
|
||||
except subprocess.TimeoutExpired: RNS.log(f"Git command execution timed out", RNS.LOG_WARNING)
|
||||
except Exception: pass
|
||||
|
||||
show_heads = not ref_type or ref_type == "heads"
|
||||
@@ -1057,7 +1064,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Statistics page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
|
||||
@@ -1131,7 +1138,7 @@ class NomadNetworkNode():
|
||||
content_parts.append("\n")
|
||||
content_parts.append(self.render_combined_chart(stats["views"]["daily"], stats["fetches"]["daily"], stats["pushes"]["daily"], stats["timeline_labels"]))
|
||||
|
||||
else: content_parts.append(self.m_italic("\nNo activity recorded for this repository in the selected time period.\n\n"))
|
||||
else: content_parts.append(self.m_italic("\nNo development activity recorded for this repository in the selected time period.\n\n"))
|
||||
|
||||
page_content = "".join(content_parts)
|
||||
nav_content = "".join(nav_parts)
|
||||
@@ -1141,7 +1148,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Releases page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
|
||||
@@ -1196,14 +1203,14 @@ class NomadNetworkNode():
|
||||
content_parts.append("\n")
|
||||
|
||||
self.owner.view_succeeded(group_name, repo_name, remote_identity)
|
||||
page_content = "".join(content_parts)
|
||||
page_content = "".join(content_parts).rstrip()+"\n"
|
||||
return self.render_template(page_content, nav_content=nav_content, template="releases", st=st)
|
||||
|
||||
def serve_release_page(self, path, data, request_id, link_id, remote_identity, requested_at):
|
||||
st = time.time()
|
||||
RNS.log(f"Release page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
tag = data.get("var_t", "") if data else ""
|
||||
@@ -1253,6 +1260,11 @@ class NomadNetworkNode():
|
||||
|
||||
sep = self.icon("sep")
|
||||
heart = self.icon("heart")
|
||||
|
||||
thanks = True if data.get("var_thanks", "") else False
|
||||
thanks_count = self.release_thanks(release_dir, add=thanks, link_id=link_id)
|
||||
content_parts.append(f"{self.m_link_r(self.icon('heart')+f' Thanks ({thanks_count})', self.PATH_RELEASE, g=group_name, r=repo_name, t=tag, thanks='y')}\n\n")
|
||||
|
||||
created_ts = release_info.get("created", 0)
|
||||
ts_str = f" {sep} {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(created_ts))}" if created_ts else ""
|
||||
content_parts.append(self.m_heading(f"Release {tag}{ts_str}", 2))
|
||||
@@ -1272,7 +1284,7 @@ class NomadNetworkNode():
|
||||
if artifacts:
|
||||
content_parts.append(self.m_heading(f"Artifacts ({len(artifacts)})", 2))
|
||||
content_parts.append("\n")
|
||||
for art in artifacts:
|
||||
for art in sorted(artifacts, key=lambda e: e["name"]):
|
||||
name = art.get("name", "unknown")
|
||||
size = art.get("size", 0)
|
||||
size_str = RNS.prettysize(size) if size else "0 B"
|
||||
@@ -1280,18 +1292,13 @@ class NomadNetworkNode():
|
||||
|
||||
lstr_1 = f"{self.icon('file')} {self.m_escape(name)}"
|
||||
lstr_2 = f"({size_str})"
|
||||
link_1 = self.m_link_r(lstr_1, self.PATH_ARTIFACT, g=group_name, r=repo_name, t=tag, a=name)
|
||||
link_2 = self.m_link_r(lstr_2, self.PATH_ARTIFACT, g=group_name, r=repo_name, t=tag, a=name)
|
||||
link_1 = self.m_link_r(lstr_1, self.FILE_ARTIFACT, g=group_name, r=repo_name, t=tag, a=name)
|
||||
link_2 = self.m_link_r(lstr_2, self.FILE_ARTIFACT, g=group_name, r=repo_name, t=tag, a=name)
|
||||
content_parts.append(f"{link_1} {self.CLR_DIM}{link_2}`f\n")
|
||||
content_parts.append("\n")
|
||||
|
||||
else:
|
||||
content_parts.append(self.m_heading("Artifacts", 2))
|
||||
content_parts.append("\nNo artifacts for this release.\n\n")
|
||||
|
||||
thanks = True if data.get("var_thanks", "") else False
|
||||
thanks_count = self.release_thanks(release_dir, add=thanks, link_id=link_id)
|
||||
content_parts.append(f"{self.m_link_r(self.icon('heart')+f' Thanks ({thanks_count})', self.PATH_RELEASE, g=group_name, r=repo_name, t=tag, thanks='y')}\n")
|
||||
content_parts.append("\n`*No artifacts for this release`*\n")
|
||||
|
||||
self.owner.view_succeeded(group_name, repo_name, remote_identity)
|
||||
page_content = "".join(content_parts)
|
||||
@@ -1301,7 +1308,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Work page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
scope = data.get("var_scope", "active") if data else "active"
|
||||
@@ -1395,11 +1402,11 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Work document page request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
doc_id = data.get("var_id", "") if data else ""
|
||||
scope = data.get("var_scope", "active") if data else "active"
|
||||
scope = data.get("var_scope", "all") if data else "all"
|
||||
if scope not in ["active", "completed", "all"]: scope = "active"
|
||||
|
||||
if not group_name or not repo_name or not doc_id:
|
||||
@@ -1417,7 +1424,19 @@ class NomadNetworkNode():
|
||||
return self.render_template(content, st=st)
|
||||
|
||||
work_path = f"{repo['path']}.work"
|
||||
doc_dir = os.path.join(work_path, scope, str(doc_id))
|
||||
active_dir = os.path.join(work_path, "active", str(doc_id))
|
||||
completed_dir = os.path.join(work_path, "completed", str(doc_id))
|
||||
if scope == "active": doc_dir = active_dir
|
||||
elif scope == "completed": doc_dir = completed_dir
|
||||
elif scope == "all":
|
||||
if os.path.isdir(active_dir):
|
||||
doc_dir = active_dir
|
||||
scope = "active"
|
||||
|
||||
else:
|
||||
doc_dir = completed_dir
|
||||
scope = "completed"
|
||||
|
||||
root_path = os.path.join(doc_dir, "root")
|
||||
|
||||
if not os.path.isfile(root_path):
|
||||
@@ -1433,29 +1452,45 @@ class NomadNetworkNode():
|
||||
nav_parts = []
|
||||
|
||||
# Breadcrumb navigation
|
||||
dl_link = self.m_link("Download", self.FILE_WORKDOC, g=group_name, r=repo_name, id=doc_id)
|
||||
breadcrumb = f">>\n{self.m_link('Node', self.PATH_INDEX)} / {self.m_link(group_name, self.PATH_GROUP, g=group_name)} / {self.m_link(repo_name, self.PATH_REPO, g=group_name, r=repo_name)} / {self.m_link('work', self.PATH_WORK, g=group_name, r=repo_name)} / #{doc_id}"
|
||||
nav_parts.append(breadcrumb + "\n")
|
||||
nav_parts.append(f"\n{dl_link}\n")
|
||||
nav_content = "".join(nav_parts)
|
||||
|
||||
doc_title = doc['meta'].get('title', 'Untitled')[:64]
|
||||
doc_title = doc['meta'].get('title', 'Untitled')[:256]
|
||||
if len(doc_title) < len(doc['meta'].get('title', 'Untitled')): doc_title += "…"
|
||||
meta = doc.get("meta", {})
|
||||
author = meta.get("author", b"")
|
||||
author_str = RNS.prettyhexrep(author) if author else "Unknown"
|
||||
signature = meta.get("signature", None)
|
||||
pubkey = meta.get("identity", None)
|
||||
created = meta.get("created", 0)
|
||||
edited = meta.get("edited", 0)
|
||||
fmt = meta.get("format", "markdown")
|
||||
content = doc.get("content", "")
|
||||
|
||||
signature_validated = False
|
||||
signature_str = "Document not signed"
|
||||
if signature and type(signature) == bytes and len(signature) == RNS.Identity.SIGLENGTH//8:
|
||||
if pubkey and type(pubkey) == bytes and len(pubkey) == RNS.Identity.KEYSIZE//8:
|
||||
signature_str = "Not valid"
|
||||
identity = RNS.Identity(create_keys=False)
|
||||
identity.load_public_key(pubkey)
|
||||
signature_validated = identity.validate(signature, content.encode("utf-8"))
|
||||
if signature_validated: signature_str = "Valid"
|
||||
|
||||
# Document header
|
||||
content_parts.append(self.m_heading(f"{doc_title}", 2))
|
||||
content_parts.append(f"\n{self.CLR_DIM}Author : {author_str}`f\n")
|
||||
content_parts.append(f"{self.CLR_DIM}Created : {time.strftime('%Y-%m-%d %H:%M', time.localtime(created)) if created else 'unknown'}`f\n")
|
||||
content_parts.append(f"\n{self.CLR_DIM}Author : {author_str}`f\n")
|
||||
content_parts.append(f"{self.CLR_DIM}Signature : {signature_str}`f\n")
|
||||
content_parts.append(f"{self.CLR_DIM}Created : {time.strftime('%Y-%m-%d %H:%M', time.localtime(created)) if created else 'unknown'}`f\n")
|
||||
if edited and edited != created:
|
||||
content_parts.append(f"{self.CLR_DIM}Edited : {time.strftime('%Y-%m-%d %H:%M', time.localtime(edited))}`f\n")
|
||||
content_parts.append(f"{self.CLR_DIM}Status : {scope.capitalize()}`f\n\n")
|
||||
content_parts.append(f"{self.CLR_DIM}Edited : {time.strftime('%Y-%m-%d %H:%M', time.localtime(edited))}`f\n")
|
||||
content_parts.append(f"{self.CLR_DIM}Status : {scope.capitalize()}`f\n\n")
|
||||
|
||||
# Document content
|
||||
content = doc.get("content", "").strip()
|
||||
content = content.strip()
|
||||
if content:
|
||||
if fmt == "micron": content_parts.append(content)
|
||||
else: content_parts.append(self.mdc.format_block(content))
|
||||
@@ -1504,7 +1539,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"Artifact file request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data: data = {}
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
tag = data.get("var_t", "") if data else ""
|
||||
@@ -1560,6 +1595,7 @@ class NomadNetworkNode():
|
||||
st = time.time()
|
||||
RNS.log(f"File download request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
ref = data.get("var_ref", "HEAD") if data else "HEAD"
|
||||
@@ -1597,6 +1633,68 @@ class NomadNetworkNode():
|
||||
|
||||
return None
|
||||
|
||||
def serve_wd_download(self, path, data, request_id, link_id, remote_identity, requested_at):
|
||||
st = time.time()
|
||||
RNS.log(f"Workdoc download request from {remote_identity}", RNS.LOG_DEBUG)
|
||||
|
||||
if not data or not type(data) == dict: data = {}
|
||||
group_name = data.get("var_g", "") if data else ""
|
||||
repo_name = data.get("var_r", "") if data else ""
|
||||
doc_id = data.get("var_id", "") if data else ""
|
||||
scope = data.get("var_scope", "all") if data else "all"
|
||||
if scope not in ["active", "completed", "all"]: scope = "active"
|
||||
|
||||
if not group_name or not repo_name or not doc_id:
|
||||
RNS.log(f"Invalid workdoc download request for {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
|
||||
return None
|
||||
|
||||
try: doc_id = int(doc_id)
|
||||
except:
|
||||
RNS.log(f"Could not parse document ID for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
|
||||
return None
|
||||
|
||||
repo = self.get_accessible_repository(remote_identity, group_name, repo_name)
|
||||
if not repo:
|
||||
RNS.log(f"Repository not found or no access for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
|
||||
return None
|
||||
|
||||
work_path = f"{repo['path']}.work"
|
||||
active_dir = os.path.join(work_path, "active", str(doc_id))
|
||||
completed_dir = os.path.join(work_path, "completed", str(doc_id))
|
||||
if scope == "active": doc_dir = active_dir
|
||||
elif scope == "completed": doc_dir = completed_dir
|
||||
elif scope == "all":
|
||||
if os.path.isdir(active_dir):
|
||||
doc_dir = active_dir
|
||||
scope = "active"
|
||||
|
||||
else:
|
||||
doc_dir = completed_dir
|
||||
scope = "completed"
|
||||
|
||||
root_path = os.path.join(doc_dir, "root")
|
||||
|
||||
if not os.path.isfile(root_path):
|
||||
RNS.log(f"Document not found for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
|
||||
return None
|
||||
|
||||
doc = self.owner._work_load_document(root_path)
|
||||
if not doc:
|
||||
RNS.log(f"Could not load document for workdoc download request {group_name[:128]}/{repo_name[:128]}/{doc_id[:128]}", RNS.LOG_WARNING)
|
||||
return None
|
||||
|
||||
meta = doc.get("meta", {})
|
||||
fmt = meta.get("format", "markdown")
|
||||
title = meta.get('title', 'Untitled')[:256]
|
||||
content = doc.get("content", "").strip()
|
||||
|
||||
if content:
|
||||
if fmt == "micron": file_name = f"{title}.mu"
|
||||
else: file_name = f"{title}.md"
|
||||
return [file_name, content.encode("utf-8")]
|
||||
|
||||
return None
|
||||
|
||||
#######################
|
||||
# Git Data Extraction #
|
||||
#######################
|
||||
@@ -1658,8 +1756,7 @@ class NomadNetworkNode():
|
||||
|
||||
if result.returncode == 0:
|
||||
hash_val = result.stdout.strip()
|
||||
# Validate it's a 40-char hex string
|
||||
if len(hash_val) == 40 and all(c in "0123456789abcdef" for c in hash_val.lower()): return hash_val.lower()
|
||||
return san_sha(hash_val.lower())
|
||||
|
||||
except subprocess.TimeoutExpired: RNS.log(f"Timeout resolving ref '{ref}'", RNS.LOG_WARNING)
|
||||
except Exception as e: RNS.log(f"Error resolving ref: {e}", RNS.LOG_WARNING)
|
||||
@@ -2085,8 +2182,12 @@ class NomadNetworkNode():
|
||||
years = int(diff / 31536000)
|
||||
return f"{years} year{'s' if years != 1 else ''} ago"
|
||||
|
||||
def format_diff(self, diff_text: str) -> str:
|
||||
lines = diff_text.split("\n")
|
||||
def format_tabs(self, text):
|
||||
if text == None: return None
|
||||
else: return text.replace("\t", self.TAB_WIDTH)
|
||||
|
||||
def format_diff(self, diff_text):
|
||||
lines = diff_text.replace("\\", "\\\\").split("\n")
|
||||
formatted_lines = []
|
||||
|
||||
for line in lines:
|
||||
@@ -2131,7 +2232,7 @@ class NomadNetworkNode():
|
||||
with open(thanks_path, "wb") as fh: fh.write(mp.packb({"count": thanks_count}))
|
||||
return thanks_count
|
||||
|
||||
except Exception as e: RNS.log(f"Error while processing repository thanks for {group_name}/{repo_name}: {e}", RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"Error while processing repository thanks for {repo_path}: {e}", RNS.LOG_ERROR)
|
||||
return 0
|
||||
|
||||
def release_thanks(self, release_path, add=False, link_id=None):
|
||||
@@ -2156,7 +2257,7 @@ class NomadNetworkNode():
|
||||
with open(thanks_path, "wb") as fh: fh.write(mp.packb({"count": thanks_count}))
|
||||
return thanks_count
|
||||
|
||||
except Exception as e: RNS.log(f"Error while processing release thanks for {group_name}/{repo_name}: {e}", RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log(f"Error while processing release thanks for {release_path}: {e}", RNS.LOG_ERROR)
|
||||
return 0
|
||||
|
||||
###################
|
||||
|
||||
+194
-96
@@ -44,6 +44,7 @@ from tempfile import NamedTemporaryFile
|
||||
from RNS._version import __version__
|
||||
from RNS.Utilities.rngit import APP_NAME
|
||||
from RNS.Utilities.rngit.pages import NomadNetworkNode
|
||||
from RNS.Utilities.rngit.util import san_ref, san_refs, san_sha
|
||||
from RNS.vendor.configobj import ConfigObj
|
||||
from RNS.vendor import umsgpack as mp
|
||||
|
||||
@@ -94,7 +95,7 @@ def program_setup(configdir, rnsconfigdir=None, verbosity=0, quietness=0, servic
|
||||
if operation == "list": git_client.work_list(remote=task["remote"], scope=scope)
|
||||
elif operation == "view": git_client.work_view(remote=task["remote"], doc_id=doc_id, scope=scope)
|
||||
elif operation == "create": git_client.work_create(remote=task["remote"], title=title)
|
||||
elif operation == "edit": git_client.work_edit(remote=task["remote"], doc_id=doc_id, scope=scope)
|
||||
elif operation == "edit": git_client.work_edit(remote=task["remote"], title=title, doc_id=doc_id, scope=scope)
|
||||
elif operation == "delete": git_client.work_delete(remote=task["remote"], doc_id=doc_id, scope=scope)
|
||||
elif operation == "update": git_client.work_comment(remote=task["remote"], doc_id=doc_id, scope=scope)
|
||||
elif operation == "complete": git_client.work_complete(remote=task["remote"], doc_id=doc_id)
|
||||
@@ -316,8 +317,7 @@ class ReticulumGitClient():
|
||||
self.response_speed = (bd/td)*8 if td > 0 else 0
|
||||
self.previous_progress = self.response_progress
|
||||
self.progress_updated_at = now
|
||||
|
||||
# Report progress to git via stderr
|
||||
|
||||
if self.progress_enabled and self.response_size:
|
||||
percent = round(self.response_progress * 100, 1)
|
||||
size = self.response_size
|
||||
@@ -355,8 +355,7 @@ class ReticulumGitClient():
|
||||
subprocess.run(["which", fallback], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
editor = fallback
|
||||
break
|
||||
except subprocess.CalledProcessError:
|
||||
continue
|
||||
except subprocess.CalledProcessError: continue
|
||||
|
||||
if not editor:
|
||||
print("No editor found. Please set $EDITOR environment variable.")
|
||||
@@ -727,16 +726,32 @@ class ReticulumGitClient():
|
||||
if len(response) <= 1: self.abort("Empty response from remote")
|
||||
|
||||
doc = mp.unpackb(response[1:])
|
||||
|
||||
author_str = f"{doc['meta']['author']} (not locally validated)"
|
||||
signature_str = "Document not signed"
|
||||
signature = doc["meta"].get("signature", None)
|
||||
pubkey = doc["meta"].get("identity", None)
|
||||
content = doc.get("content", "")
|
||||
if signature and type(signature) == bytes and len(signature) == RNS.Identity.SIGLENGTH//8:
|
||||
if pubkey and type(pubkey) == bytes and len(pubkey) == RNS.Identity.KEYSIZE//8:
|
||||
signature_str = "Not valid"
|
||||
identity = RNS.Identity(create_keys=False)
|
||||
identity.load_public_key(pubkey)
|
||||
signature_validated = identity.validate(signature, content.encode("utf-8"))
|
||||
if signature_validated:
|
||||
signature_str = "Valid"
|
||||
author_str = RNS.prettyhexrep(identity.hash)
|
||||
|
||||
dt = f"{doc['meta']['title']} (#{doc['id']})"
|
||||
print(f"{dt}")
|
||||
print("="*len(dt))
|
||||
print(f"Author : {doc['meta']['author']}")
|
||||
print(f"Status : {scope}")
|
||||
print(f"Created : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(doc['meta']['created']))}")
|
||||
print(f"Edited : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(doc['meta']['edited']))}")
|
||||
print(f"Format : {doc['meta']['format']}")
|
||||
print(f"Updates : {len(doc.get('comments', []))}")
|
||||
print(f"Author : {author_str}")
|
||||
print(f"Signature : {signature_str}")
|
||||
print(f"Status : {scope.capitalize()}")
|
||||
print(f"Created : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(doc['meta']['created']))}")
|
||||
print(f"Edited : {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(doc['meta']['edited']))}")
|
||||
print(f"Format : {doc['meta']['format']}")
|
||||
print(f"Updates : {len(doc.get('comments', []))}")
|
||||
print()
|
||||
print(doc['content'])
|
||||
|
||||
@@ -775,9 +790,13 @@ class ReticulumGitClient():
|
||||
|
||||
content = self._edit_work_content(title=title)
|
||||
if content is None: print("Creation cancelled"); return
|
||||
|
||||
signature = self.identity.sign(content.encode("utf-8"))
|
||||
if not signature: self.abort("Could not sign work document")
|
||||
|
||||
request_data = { self.IDX_REPOSITORY: repo_path,
|
||||
"operation": "create", "title": title, "content": content, "format": "markdown" }
|
||||
request_data = { self.IDX_REPOSITORY: repo_path, "operation": "create",
|
||||
"title": title, "content": content, "format": "markdown",
|
||||
"signature": signature }
|
||||
|
||||
response, metadata = self.send_request(self.PATH_WORK, request_data, timeout=30)
|
||||
if not response or not isinstance(response, bytes): self.abort("No response from remote")
|
||||
@@ -797,7 +816,7 @@ class ReticulumGitClient():
|
||||
finally:
|
||||
if self.link: self.link.teardown()
|
||||
|
||||
def work_edit(self, remote=None, doc_id=None, scope="active"):
|
||||
def work_edit(self, remote=None, doc_id=None, title=None, scope="active"):
|
||||
if not remote: print(f"No remote specified"); exit(1)
|
||||
if doc_id is None: print(f"No document ID specified"); exit(1)
|
||||
self.connect_remote(remote)
|
||||
@@ -830,9 +849,12 @@ class ReticulumGitClient():
|
||||
content = self._edit_work_content(title=current_title, content=current_content)
|
||||
if content is None: print("Edit cancelled"); return
|
||||
|
||||
title = current_title
|
||||
request_data = { self.IDX_REPOSITORY: repo_path,
|
||||
"operation": "edit", "doc_id": doc_id, "scope": scope, "content": content }
|
||||
signature = self.identity.sign(content.encode("utf-8"))
|
||||
if not signature: self.abort("Could not sign work document")
|
||||
|
||||
title = title or current_title
|
||||
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)
|
||||
if not response or not isinstance(response, bytes): self.abort("No response from remote")
|
||||
@@ -1186,7 +1208,7 @@ class ReticulumGitNode():
|
||||
IDX_REPOSITORY = 0x00
|
||||
IDX_RESULT_CODE = 0x01
|
||||
|
||||
WORK_DOC_LIMIT = 256*1024*1024
|
||||
WORK_DOC_LIMIT = 256*1024
|
||||
|
||||
def __init__(self, configdir=None, verbosity=None, print_identity=False):
|
||||
self.identity = None
|
||||
@@ -1295,7 +1317,9 @@ class ReticulumGitNode():
|
||||
def __persist_stats(self):
|
||||
with self.stats_lock:
|
||||
try:
|
||||
with open(self.statspath, "wb") as fh: fh.write(mp.packb(self.stats))
|
||||
tmp_path = self.statspath+".tmp"
|
||||
with open(tmp_path, "wb") as fh: fh.write(mp.packb(self.stats))
|
||||
os.rename(tmp_path, self.statspath)
|
||||
except Exception as e: RNS.log(f"Could not write stats file to {self.statspath}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def __apply_config(self):
|
||||
@@ -1717,10 +1741,12 @@ class ReticulumGitNode():
|
||||
|
||||
# Check for_push permission if requested
|
||||
for_push = data.get("for_push", False)
|
||||
if for_push: access = self.resolve_permission(remote_identity, group_name, repository_name, self.PERM_WRITE)
|
||||
else: access = self.resolve_permission(remote_identity, group_name, repository_name, self.PERM_READ)
|
||||
read_access = self.resolve_permission(remote_identity, group_name, repository_name, self.PERM_READ)
|
||||
write_access = self.resolve_permission(remote_identity, group_name, repository_name, self.PERM_WRITE)
|
||||
if for_push: access = write_access
|
||||
else: access = read_access
|
||||
|
||||
if not access: return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Not found"
|
||||
if not access: return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Not allowed" if read_access else self.RES_NOT_FOUND.to_bytes(1, "big") + b"Not found"
|
||||
else:
|
||||
repository_path = self.groups[group_name]["repositories"][repository_name]["path"]
|
||||
|
||||
@@ -1728,7 +1754,7 @@ class ReticulumGitNode():
|
||||
RNS.log(f"Listing refs for {group_name}/{repository_name}", RNS.LOG_DEBUG)
|
||||
|
||||
# Get HEAD symref
|
||||
head_path = os.path.join(repository_path, "HEAD")
|
||||
head_path = os.path.join(repository_path, "HEAD")
|
||||
head_ref = "master" # Use "master" as default
|
||||
if os.path.exists(head_path):
|
||||
with open(head_path, "rb") as fh:
|
||||
@@ -1738,7 +1764,9 @@ class ReticulumGitNode():
|
||||
execv = ["git", "for-each-ref", "--format", "%(objectname) %(refname)"]
|
||||
result = subprocess.run(execv, cwd=repository_path, capture_output=True, check=False, text=True)
|
||||
|
||||
if result.returncode != 0: return self.RES_REMOTE_FAIL.to_bytes(1, "big") + result.stderr.encode("utf-8")
|
||||
if result.returncode != 0:
|
||||
RNS.log(f"Error while listing refs for {group_name}/{repository_name}: {result.stderr}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Could not list refs"
|
||||
|
||||
# Build response in format: refs + @<ref> HEAD
|
||||
response_lines = result.stdout.strip()
|
||||
@@ -1765,7 +1793,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while handling list request for {group_name}/{repository_name}: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def handle_fetch(self, path, data, request_id, link_id, remote_identity, requested_at):
|
||||
RNS.log(f"Fetch request from remote {remote_identity}", RNS.LOG_DEBUG)
|
||||
@@ -1788,7 +1816,8 @@ class ReticulumGitNode():
|
||||
if not refs: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No refs specified"
|
||||
|
||||
try:
|
||||
ref_names = [r["ref"] for r in refs]
|
||||
ref_names = san_refs([r["ref"] for r in refs])
|
||||
if not ref_names: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
|
||||
RNS.log(f"Fetching refs {ref_names} for {group_name}/{repository_name}", RNS.LOG_DEBUG)
|
||||
|
||||
if not hasattr(link, "temporary_directories"): link.temporary_directories = []
|
||||
@@ -1801,19 +1830,23 @@ class ReticulumGitNode():
|
||||
execv = ["git", "bundle", "create", "--no-progress", bundle_path]
|
||||
|
||||
for r in refs:
|
||||
execv.append(r["ref"])
|
||||
# Per-ref have: The client already has this ancestor,
|
||||
# so the server can exclude objects reachable from it.
|
||||
if "have" in r and r["have"]:
|
||||
have_sha = r["have"]
|
||||
cat_result = subprocess.run(["git", "cat-file", "-t", have_sha], cwd=repository_path, capture_output=True, check=False)
|
||||
if cat_result.returncode == 0: execv.append(f"^{have_sha}")
|
||||
else: RNS.log(f"Client have-sha {have_sha} not found in repository, skipping", RNS.LOG_WARNING)
|
||||
if not san_ref(r["ref"]): return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
|
||||
else:
|
||||
execv.append(r["ref"])
|
||||
# Per-ref have: The client already has this ancestor,
|
||||
# so the server can exclude objects reachable from it.
|
||||
if "have" in r and r["have"]:
|
||||
have_sha = san_sha(r["have"])
|
||||
if not have_sha: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid SHA"
|
||||
cat_result = subprocess.run(["git", "cat-file", "-t", have_sha], cwd=repository_path, capture_output=True, check=False)
|
||||
if cat_result.returncode == 0: execv.append(f"^{have_sha}")
|
||||
else: RNS.log(f"Client have-sha {have_sha} not found in repository, skipping", RNS.LOG_WARNING)
|
||||
|
||||
# Global have list: SHAs of objects the client already has.
|
||||
# Exclude objects reachable from these to produce thin bundles.
|
||||
have_shas = data.get("have", [])
|
||||
for sha in have_shas:
|
||||
if not san_sha(sha): return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid SHA"
|
||||
cat_result = subprocess.run(["git", "cat-file", "-t", sha], cwd=repository_path, capture_output=True, check=False)
|
||||
if cat_result.returncode == 0: execv.append(f"^{sha}")
|
||||
else: RNS.log(f"Client have-sha {sha} not found in repository, skipping", RNS.LOG_WARNING)
|
||||
@@ -1826,13 +1859,15 @@ class ReticulumGitNode():
|
||||
RNS.log(f"Empty bundle for {ref_names}, all objects already on client", RNS.LOG_DEBUG)
|
||||
return b"\x00"
|
||||
|
||||
else: return self.RES_REMOTE_FAIL.to_bytes(1, "big") + result.stderr.encode("utf-8")
|
||||
else:
|
||||
RNS.log(f"Error while fetching refs {ref_names} for {group_name}/{repository_name}: {result.stderr}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Could not fetch refs"
|
||||
|
||||
return open(bundle_path, "rb"), {self.IDX_RESULT_CODE: self.RES_OK}
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while handling fetch request for {group_name}/{repository_name}: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def handle_push(self, path, data, request_id, remote_identity, requested_at):
|
||||
RNS.log(f"Push request from remote {remote_identity}", RNS.LOG_DEBUG)
|
||||
@@ -1847,8 +1882,8 @@ class ReticulumGitNode():
|
||||
if not write_access: return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Not found" if not read_access else self.RES_DISALLOWED.to_bytes(1, "big") + b"Not allowed"
|
||||
else:
|
||||
repository_path = self.groups[group_name]["repositories"][repository_name]["path"]
|
||||
local_ref = data.get("local_ref", "")
|
||||
remote_ref = data.get("remote_ref", "")
|
||||
local_ref = san_ref(data.get("local_ref", ""))
|
||||
remote_ref = san_ref(data.get("remote_ref", ""))
|
||||
force = data.get("force", False)
|
||||
bundle_data = data.get("bundle", None)
|
||||
operations = data.get("operations", None)
|
||||
@@ -1867,20 +1902,24 @@ class ReticulumGitNode():
|
||||
execv = ["git", "bundle", "verify", bundle_path]
|
||||
result = subprocess.run(execv, cwd=repository_path, capture_output=True, check=False)
|
||||
|
||||
if result.returncode != 0: return self.RES_REMOTE_FAIL.to_bytes(1, "big") + result.stderr
|
||||
if result.returncode != 0:
|
||||
RNS.log(f"Bundle verification failed for push {local_ref}:{remote_ref} to {group_name}/{repository_name}: {result.stderr}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Could not verify bundle"
|
||||
|
||||
execv = ["git", "fetch", bundle_path, f"{local_ref}:{remote_ref}"]
|
||||
if force: execv.append("--force")
|
||||
|
||||
result = subprocess.run(execv, cwd=repository_path, capture_output=True, check=False)
|
||||
|
||||
if result.returncode != 0: return self.RES_REMOTE_FAIL.to_bytes(1, "big") + result.stderr
|
||||
if result.returncode != 0:
|
||||
RNS.log(f"Bundle verification failed for push {local_ref}:{remote_ref} to {group_name}/{repository_name}: {result.stderr}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Could not verify bundle"
|
||||
|
||||
return b"\x00"
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while handling push request for {group_name}/{repository_name}: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
elif operations:
|
||||
if not type(operations) == list: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid data for operations"
|
||||
@@ -1888,13 +1927,14 @@ class ReticulumGitNode():
|
||||
try:
|
||||
for op in operations:
|
||||
action = op.get("action", "")
|
||||
ref = op.get("ref", "")
|
||||
sha = op.get("sha", "")
|
||||
ref = san_ref(op.get("ref", ""))
|
||||
sha = san_sha(op.get("sha", ""))
|
||||
op_force = op.get("force", False)
|
||||
|
||||
if action != "update_ref": return self.RES_INVALID_REQ.to_bytes(1, "big") + f"Unknown operation: {action}".encode("utf-8")
|
||||
if not ref.startswith("refs/"): return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid ref"
|
||||
if not sha or len(sha) < 40: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid SHA"
|
||||
if not ref: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
|
||||
if not ref.startswith("refs/"): return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
|
||||
if not sha: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid SHA"
|
||||
|
||||
# Verify the target object exists in the repository
|
||||
cat_result = subprocess.run(["git", "cat-file", "-t", sha], cwd=repository_path, capture_output=True, check=False)
|
||||
@@ -1912,13 +1952,15 @@ class ReticulumGitNode():
|
||||
execv = ["git", "update-ref", ref, sha]
|
||||
result = subprocess.run(execv, cwd=repository_path, capture_output=True, check=False)
|
||||
|
||||
if result.returncode != 0: return self.RES_REMOTE_FAIL.to_bytes(1, "big") + result.stderr
|
||||
if result.returncode != 0:
|
||||
RNS.log(f"Error while updating ref {ref} to {sha} for {group_name}/{repository_name}: {result.stderr}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Could not update refs"
|
||||
|
||||
return b"\x00"
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while handling push operations for {group_name}/{repository_name}: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
else: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request data"
|
||||
|
||||
@@ -1935,20 +1977,24 @@ class ReticulumGitNode():
|
||||
if not write_access: return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Not found" if not read_access else self.RES_DISALLOWED.to_bytes(1, "big") + b"Not allowed"
|
||||
else:
|
||||
repository_path = self.groups[group_name]["repositories"][repository_name]["path"]
|
||||
ref_to_delete = data.get("ref", "")
|
||||
ref_to_delete = san_ref(data.get("ref", ""))
|
||||
|
||||
if not ref_to_delete.startswith("refs/"): return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid ref"
|
||||
if not ref_to_delete: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
|
||||
if not ref_to_delete.startswith("refs/"): return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
|
||||
try:
|
||||
RNS.log(f"Deleting ref {ref_to_delete} in {group_name}/{repository_name}", RNS.LOG_DEBUG)
|
||||
execv = ["git", "update-ref", "-d", ref_to_delete]
|
||||
result = subprocess.run(execv, cwd=repository_path, capture_output=True, check=False)
|
||||
|
||||
if result.returncode != 0:
|
||||
RNS.log(f"Error while deleting ref {ref_to_delete} for {group_name}/{repository_name}: {result.stderr}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Could not delete ref"
|
||||
|
||||
if result.returncode != 0: return self.RES_REMOTE_FAIL.to_bytes(1, "big") + result.stderr
|
||||
else: return b"\x00"
|
||||
else: return b"\x00"
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while handling delete request for {group_name}/{repository_name}: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def handle_release(self, path, data, request_id, remote_identity, requested_at):
|
||||
RNS.log(f"Release request from remote {remote_identity}", RNS.LOG_DEBUG)
|
||||
@@ -1984,7 +2030,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while handling release request for {group_name}/{repository_name}: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def releases_list_data(self, releases_path):
|
||||
try:
|
||||
@@ -2114,8 +2160,9 @@ class ReticulumGitNode():
|
||||
return None
|
||||
|
||||
def _release_view(self, releases_path, data):
|
||||
tag = data.get("tag")
|
||||
if not tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No tag specified"
|
||||
tag = data.get("tag", "")
|
||||
if "/" in tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid tag specified"
|
||||
if not tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid tag specified"
|
||||
|
||||
tag = os.path.basename(tag)
|
||||
release_dir = os.path.join(releases_path, tag)
|
||||
@@ -2137,12 +2184,13 @@ class ReticulumGitNode():
|
||||
else: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
|
||||
|
||||
def _release_create_init(self, releases_path, repository_path, data, remote_identity):
|
||||
tag = data.get("tag")
|
||||
tag = data.get("tag", "")
|
||||
commit_hash = data.get("hash")
|
||||
notes = data.get("notes", "")
|
||||
notes_format = data.get("notes_format", "markdown") # "markdown" or "micron"
|
||||
|
||||
if not tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No tag specified"
|
||||
if not tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid tag specified"
|
||||
if "/" in tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid tag specified"
|
||||
|
||||
tag = os.path.basename(tag)
|
||||
if not tag or tag in [".", ".."]: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid tag name"
|
||||
@@ -2183,14 +2231,15 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error creating release: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def _release_create_artifact(self, releases_path, data):
|
||||
tag = data.get("tag")
|
||||
tag = data.get("tag", "")
|
||||
artifact_name = data.get("artifact_name")
|
||||
artifact_data = data.get("artifact_data")
|
||||
|
||||
if not tag or not artifact_name: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Missing tag or artifact name"
|
||||
if "/" in tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid tag specified"
|
||||
if artifact_data is None: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No artifact data"
|
||||
|
||||
tag = os.path.basename(tag)
|
||||
@@ -2219,12 +2268,12 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error adding artifact: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def _release_create_finalize(self, releases_path, data):
|
||||
tag = data.get("tag")
|
||||
|
||||
if not tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No tag specified"
|
||||
tag = data.get("tag", "")
|
||||
if not tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No tag specified"
|
||||
if "/" in tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid tag specified"
|
||||
|
||||
tag = os.path.basename(tag)
|
||||
|
||||
@@ -2247,12 +2296,13 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error finalizing release: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def _release_delete(self, releases_path, data):
|
||||
tag = data.get("tag")
|
||||
tag = data.get("tag", "")
|
||||
|
||||
if not tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No tag specified"
|
||||
if "/" in tag: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid tag specified"
|
||||
|
||||
tag = os.path.basename(tag)
|
||||
release_dir = os.path.join(releases_path, tag)
|
||||
@@ -2266,7 +2316,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error deleting release: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
#########################
|
||||
# Work Document Methods #
|
||||
@@ -2324,7 +2374,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while handling work request for {group_name}/{repository_name}: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def _work_get_next_id(self, base_path):
|
||||
if not os.path.isdir(base_path): return 1
|
||||
@@ -2387,13 +2437,22 @@ class ReticulumGitNode():
|
||||
|
||||
def _work_view(self, work_path, data, remote_identity):
|
||||
doc_id = data.get("doc_id")
|
||||
scope = data.get("scope", "active")
|
||||
scope = data.get("scope", "all")
|
||||
if not scope in ["active", "completed", "all"]: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
|
||||
|
||||
if doc_id is None: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No document ID specified"
|
||||
try: doc_id = int(doc_id)
|
||||
except: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid document ID"
|
||||
|
||||
scope = None
|
||||
doc_dir = None
|
||||
for s in ["active", "completed"]:
|
||||
d = os.path.join(work_path, s, str(doc_id))
|
||||
if os.path.isdir(d):
|
||||
scope = s
|
||||
doc_dir = d
|
||||
break
|
||||
|
||||
doc_dir = os.path.join(work_path, scope, str(doc_id))
|
||||
root_path = os.path.join(doc_dir, "root")
|
||||
|
||||
@@ -2421,13 +2480,16 @@ class ReticulumGitNode():
|
||||
|
||||
comments.sort(key=lambda x: x["id"])
|
||||
|
||||
meta = doc.get("meta", {})
|
||||
result = { "id": doc_id, "scope": scope,
|
||||
"content": doc.get("content", ""), "comments": comments,
|
||||
"meta": { "title": doc.get("meta", {}).get("title", "Untitled"),
|
||||
"created": doc.get("meta", {}).get("created", 0),
|
||||
"edited": doc.get("meta", {}).get("edited", 0),
|
||||
"author": RNS.hexrep(doc.get("meta", {}).get("author", b""), delimit=False) if doc.get("meta", {}).get("author") else "",
|
||||
"format": doc.get("meta", {}).get("format", "markdown") } }
|
||||
"meta": { "title": meta.get("title", "Untitled"),
|
||||
"created": meta.get("created", 0),
|
||||
"edited": meta.get("edited", 0),
|
||||
"author": RNS.hexrep(meta.get("author", b""), delimit=False) if meta.get("author") else "",
|
||||
"identity": meta.get("identity", None),
|
||||
"signature": meta.get("signature", None),
|
||||
"format": meta.get("format", "markdown") } }
|
||||
|
||||
return b"\x00" + mp.packb(result)
|
||||
|
||||
@@ -2436,9 +2498,13 @@ class ReticulumGitNode():
|
||||
content = data.get("content", "").strip()
|
||||
format_type = data.get("format", "markdown")
|
||||
signature = data.get("signature", None)
|
||||
signed_data = content.encode("utf-8")
|
||||
sig_length = RNS.Identity.SIGLENGTH//8
|
||||
limit = self.WORK_DOC_LIMIT
|
||||
|
||||
if signature and not len(signature) == RNS.Identity.SIGLENGTH: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature"
|
||||
if not signature: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No signature provided"
|
||||
if signature and not len(signature) == sig_length: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature length"
|
||||
if not remote_identity.validate(signature, signed_data): return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature"
|
||||
if len(title)+len(content)+len(format_type) > limit: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Content limit exceeded"
|
||||
if not title: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Title is required"
|
||||
if not content: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Content is required"
|
||||
@@ -2452,8 +2518,8 @@ class ReticulumGitNode():
|
||||
now = time.time()
|
||||
document = { "content": content,
|
||||
"meta": { "format": format_type if format_type in ["markdown", "micron"] else "markdown",
|
||||
"title": title, "created": now, "edited": now,
|
||||
"signature": signature, "author": remote_identity.hash } }
|
||||
"title": title, "created": now, "edited": now, "author": remote_identity.hash,
|
||||
"signature": signature, "identity": remote_identity.get_public_key() } }
|
||||
|
||||
root_path = os.path.join(doc_dir, "root")
|
||||
if not self._work_save_document(root_path, document):
|
||||
@@ -2464,31 +2530,45 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error creating work document: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def _work_edit(self, work_path, data, remote_identity):
|
||||
doc_id = data.get("doc_id")
|
||||
scope = data.get("scope", "active")
|
||||
content = data.get("content")
|
||||
title = data.get("title")
|
||||
signature = data.get("signature", None)
|
||||
limit = self.WORK_DOC_LIMIT
|
||||
doc_id = data.get("doc_id")
|
||||
scope = data.get("scope", "active")
|
||||
content = data.get("content", "")
|
||||
title = data.get("title", "")
|
||||
signature = data.get("signature", None)
|
||||
signed_data = content.encode("utf-8")
|
||||
sig_length = RNS.Identity.SIGLENGTH//8
|
||||
limit = self.WORK_DOC_LIMIT
|
||||
|
||||
size = 0
|
||||
if title: size += len(title)
|
||||
if content: size += len(content)
|
||||
|
||||
if not scope in ["active", "completed", "all"]: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid request"
|
||||
if signature and not len(signature) == RNS.Identity.SIGLENGTH: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature"
|
||||
if not signature: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No signature provided"
|
||||
if signature and not len(signature) == sig_length: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature length"
|
||||
if not remote_identity.validate(signature, signed_data): return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid signature"
|
||||
if size > limit: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Content limit exceeded"
|
||||
if content is None and title is None: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No changes specified"
|
||||
if doc_id is None: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No document ID specified"
|
||||
if not content and not title: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No changes specified"
|
||||
if not doc_id: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"No document ID specified"
|
||||
|
||||
try: doc_id = int(doc_id)
|
||||
except: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid document ID"
|
||||
|
||||
scope = None
|
||||
doc_dir = None
|
||||
for s in ["active", "completed"]:
|
||||
d = os.path.join(work_path, s, str(doc_id))
|
||||
if os.path.isdir(d):
|
||||
scope = s
|
||||
doc_dir = d
|
||||
break
|
||||
|
||||
doc_dir = os.path.join(work_path, scope, str(doc_id))
|
||||
root_path = os.path.join(doc_dir, "root")
|
||||
RNS.log(f"PATH: {root_path}")
|
||||
|
||||
if not os.path.isfile(root_path): return self.RES_NOT_FOUND.to_bytes(1, "big") + b"Document not found"
|
||||
|
||||
@@ -2498,10 +2578,11 @@ class ReticulumGitNode():
|
||||
if doc.get("meta", {}).get("author") != remote_identity.hash: return self.RES_DISALLOWED.to_bytes(1, "big") + b"No access, not author"
|
||||
|
||||
try:
|
||||
if title is not None: doc["meta"]["title"] = title.strip()
|
||||
if content is not None: doc["content"] = content.strip()
|
||||
if title: doc["meta"]["title"] = title.strip()
|
||||
if content: doc["content"] = content.strip()
|
||||
doc["meta"]["edited"] = time.time()
|
||||
doc["meta"]["signature"] = signature
|
||||
doc["meta"]["identity"] = remote_identity.get_public_key()
|
||||
|
||||
if not self._work_save_document(root_path, doc): return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Error saving document"
|
||||
|
||||
@@ -2510,7 +2591,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error editing work document: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def _work_delete(self, work_path, data, remote_identity):
|
||||
doc_id = data.get("doc_id")
|
||||
@@ -2521,6 +2602,15 @@ class ReticulumGitNode():
|
||||
|
||||
try: doc_id = int(doc_id)
|
||||
except: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid document ID"
|
||||
|
||||
scope = None
|
||||
doc_dir = None
|
||||
for s in ["active", "completed"]:
|
||||
d = os.path.join(work_path, s, str(doc_id))
|
||||
if os.path.isdir(d):
|
||||
scope = s
|
||||
doc_dir = d
|
||||
break
|
||||
|
||||
doc_dir = os.path.join(work_path, scope, str(doc_id))
|
||||
root_path = os.path.join(doc_dir, "root")
|
||||
@@ -2539,7 +2629,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error deleting work document: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def _work_comment(self, work_path, data, remote_identity):
|
||||
doc_id = data.get("doc_id")
|
||||
@@ -2558,6 +2648,15 @@ class ReticulumGitNode():
|
||||
except: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Invalid document ID"
|
||||
|
||||
if not content: return self.RES_INVALID_REQ.to_bytes(1, "big") + b"Content is required"
|
||||
|
||||
scope = None
|
||||
doc_dir = None
|
||||
for s in ["active", "completed"]:
|
||||
d = os.path.join(work_path, s, str(doc_id))
|
||||
if os.path.isdir(d):
|
||||
scope = s
|
||||
doc_dir = d
|
||||
break
|
||||
|
||||
doc_dir = os.path.join(work_path, scope, str(doc_id))
|
||||
root_path = os.path.join(doc_dir, "root")
|
||||
@@ -2581,7 +2680,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error adding comment: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def _work_complete(self, work_path, data, remote_identity):
|
||||
doc_id = data.get("doc_id")
|
||||
@@ -2610,7 +2709,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error completing work document: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def _work_activate(self, work_path, data, remote_identity):
|
||||
doc_id = data.get("doc_id")
|
||||
@@ -2639,7 +2738,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error activating work document: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + str(e).encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Remote error"
|
||||
|
||||
def _work_perms(self, work_path, data, remote_identity):
|
||||
step = data.get("step")
|
||||
@@ -2698,7 +2797,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error getting document permissions: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + f"Error getting permissions: {e}".encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Error getting permissions"
|
||||
|
||||
def _work_set_permissions(self, work_path, data, remote_identity, group_name, repository_name):
|
||||
doc_id = data.get("doc_id")
|
||||
@@ -2758,7 +2857,7 @@ class ReticulumGitNode():
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error setting permissions: {e}", RNS.LOG_ERROR)
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + f"Error setting permissions: {e}".encode("utf-8")
|
||||
return self.RES_REMOTE_FAIL.to_bytes(1, "big") + b"Error setting permissions"
|
||||
|
||||
def repository_stats(self, remote_identity, group_name, repository_name, lookback_days=14):
|
||||
if not self.resolve_permission(remote_identity, group_name, repository_name, self.PERM_STATS): return None
|
||||
@@ -2859,7 +2958,6 @@ class ReticulumGitNode():
|
||||
if group_name and repository_name: self.record_fetch(group_name, repository_name)
|
||||
|
||||
def push_succeeded(self, group_name, repository_name, remote_identity):
|
||||
if remote_identity and remote_identity.hash in self.stats_ignored: return
|
||||
if self.stats_enabled:
|
||||
if group_name and repository_name: self.record_push(group_name, repository_name)
|
||||
|
||||
|
||||
@@ -31,6 +31,51 @@
|
||||
import re
|
||||
import RNS
|
||||
|
||||
# Validate ref names according to https://git-scm.com/docs/git-check-ref-format
|
||||
# This may be a bit overkill, since git validates names as well, but why not.
|
||||
def san_ref(ref):
|
||||
if ref.startswith("-"): return None
|
||||
if ref.startswith("/"): return None
|
||||
if ref.endswith("/"): return None
|
||||
if ref.endswith("."): return None
|
||||
|
||||
if " " in ref: return None
|
||||
if not "/" in ref: return None
|
||||
if ".." in ref: return None
|
||||
if "/." in ref: return None
|
||||
if "//" in ref: return None
|
||||
if "\\" in ref: return None
|
||||
|
||||
for comp in ref.split("/"):
|
||||
if comp.endswith(".lock"): return None
|
||||
|
||||
if not all(ord(c) >= 40 for c in ref): return None # Any control character
|
||||
if "\x7f" in ref: return None # ASCII DEL (177)
|
||||
if "~" in ref: return None
|
||||
if "^" in ref: return None
|
||||
if ":" in ref: return None
|
||||
if "?" in ref: return None
|
||||
if "*" in ref: return None
|
||||
if "[" in ref: return None
|
||||
if "@{" in ref: return None
|
||||
if "@" == ref: return None
|
||||
|
||||
return ref
|
||||
|
||||
def san_refs(refs):
|
||||
if not type(refs) == list: return None
|
||||
for ref in refs:
|
||||
if not san_ref(ref): return None
|
||||
|
||||
return refs
|
||||
|
||||
# Git SHA format validation
|
||||
def san_sha(sha):
|
||||
if len(sha) < 40: return None
|
||||
try: bytes.fromhex(sha)
|
||||
except: return None
|
||||
return sha
|
||||
|
||||
class MarkdownToMicron:
|
||||
BOLD = "`!"
|
||||
BOLD_END = "`!"
|
||||
@@ -107,6 +152,7 @@ class MarkdownToMicron:
|
||||
return w if w is not None and w >= 0 else len(text)
|
||||
|
||||
def format_block(self, text, url_scope=None):
|
||||
# text = text.replace("\\", "\\\\") # Now handled in format_line instead
|
||||
lines = text.split('\n')
|
||||
result_lines = []
|
||||
in_code_block = False
|
||||
@@ -183,10 +229,6 @@ class MarkdownToMicron:
|
||||
for line in lines:
|
||||
is_fence, lang_hint = self._detect_code_fence(line)
|
||||
|
||||
if line.startswith("-") and not line.startswith("---") and not line.startswith("- "): line = f"\\{line}"
|
||||
if line.startswith(">"): line = f"\\{line}"
|
||||
if line.startswith("<"): line = f"\\{line}"
|
||||
|
||||
if is_fence:
|
||||
# Flush any pending structures before code fence
|
||||
flush_quote_buffer()
|
||||
@@ -255,6 +297,11 @@ class MarkdownToMicron:
|
||||
|
||||
def format_line(self, line, mode="normal"):
|
||||
if mode == "codeblock": return self._escape_literals(line)
|
||||
line = line.replace("\\", "\\\\")
|
||||
|
||||
if line.startswith("-") and not line.startswith("---") and not line.startswith("- "): line = f"\\{line}"
|
||||
if line.startswith("<"): line = f"\\{line}"
|
||||
# if line.startswith(">"): line = f"\\{line}" # Now handled by blockquotes
|
||||
|
||||
if self.HORIZONTAL_RULE_RE.match(line): return self._format_horizontal_rule()
|
||||
|
||||
|
||||
+269
-28
@@ -45,10 +45,12 @@ from RNS.Cryptography.Hashes import file_sha256
|
||||
|
||||
APP_NAME = "rns"
|
||||
DEFAULT_ASPECTS = f"{APP_NAME}.id"
|
||||
NO_MESSAGE = 0x01
|
||||
|
||||
PRV_EXT = "rid"
|
||||
PUB_EXT = "pub"
|
||||
SIG_EXT = "rsg"
|
||||
MSG_EXT = "rsm"
|
||||
ENCRYPT_EXT = "rfe"
|
||||
CHUNK_BLOCKS = 1024*1024
|
||||
ENC_CHUNK = CHUNK_BLOCKS*RNS.Identity.AES256_BLOCKSIZE
|
||||
@@ -69,6 +71,8 @@ R_INVALID_ASPECTS = 9
|
||||
R_INVALID_SIGNATURE = 10
|
||||
R_FILE_EXISTS = 11
|
||||
R_DECRYPT_FAILED = 12
|
||||
R_INVALID_ARGS = 250
|
||||
R_SEQUENCE_ERROR = 251
|
||||
R_READ_ERROR = 252
|
||||
R_WRITE_ERROR = 253
|
||||
R_UNKNOWN_ERROR = 254
|
||||
@@ -78,7 +82,7 @@ reticulum = None
|
||||
|
||||
def validate_args(args):
|
||||
ops = 0;
|
||||
for o in [args.encrypt, args.decrypt, args.validate, args.sign]:
|
||||
for o in [args.encrypt, args.decrypt, args.validate, args.sign, args.sign_message]:
|
||||
if o: ops += 1
|
||||
if ops > 1: print("This utility currently only supports one of the encrypt, decrypt, sign or verify operations per invocation"); exit(1)
|
||||
|
||||
@@ -88,9 +92,9 @@ def validate_args(args):
|
||||
if g > 1: print("The -i, -g, -m and -M args are mutually exclusive"); exit(1)
|
||||
|
||||
g = 0;
|
||||
for a in [args.base64, args.base32]:
|
||||
for a in [args.base64, args.base32, args.base256, args.hex]:
|
||||
if a: g += 1
|
||||
if g > 1: print("The -b and -B args are mutually exclusive"); exit(1)
|
||||
if g > 1: print("The -b, -B, --hex and --base256 args are mutually exclusive"); exit(1)
|
||||
|
||||
return True
|
||||
|
||||
@@ -114,10 +118,11 @@ def main():
|
||||
# Operations
|
||||
parser.add_argument("-a", "--announce", metavar="aspects", action="store", nargs="?", const=DEFAULT_ASPECTS, default=None, help="announce a destination based on this Identity")
|
||||
parser.add_argument("-H", "--hash", metavar="aspects", action="store", default=None, help="show destination hashes for other aspects for this Identity")
|
||||
parser.add_argument("-d", "--decrypt", metavar="file", action="store", default=None, help="decrypt file")
|
||||
parser.add_argument("-e", "--encrypt", metavar="file", action="store", default=None, help="encrypt file")
|
||||
parser.add_argument("-V", "--validate", metavar="path", action="store", default=None, help="validate signature")
|
||||
parser.add_argument("-s", "--sign", metavar="path", action="store", default=None, help="sign file")
|
||||
parser.add_argument("-d", "--decrypt", metavar="file", action="store", nargs="*", default=None, help="decrypt file")
|
||||
parser.add_argument("-e", "--encrypt", metavar="file", action="store", nargs="*", default=None, help="encrypt file")
|
||||
parser.add_argument("-V", "--validate", metavar="path", action="store", nargs="*", default=None, help="validate signature")
|
||||
parser.add_argument("-s", "--sign", metavar="path", action="store", nargs="*", default=None, help="sign file")
|
||||
parser.add_argument("-S", "--sign-message", metavar="path", action="store", nargs="?", const=NO_MESSAGE, default=None, help="create embedded signed message")
|
||||
parser.add_argument("--raw", action="store_true", default=False, help="sign raw input data instead of hashing first")
|
||||
|
||||
# I/O Control
|
||||
@@ -136,6 +141,8 @@ def main():
|
||||
# Formatting Control
|
||||
parser.add_argument("-b", "--base64", action="store_true", default=False, help="Use base64-encoded input and output")
|
||||
parser.add_argument("-B", "--base32", action="store_true", default=False, help="Use base32-encoded input and output")
|
||||
parser.add_argument("--hex", action="store_true", default=False, help="Use hex-encoded input and output")
|
||||
parser.add_argument("--base256", action="store_true", default=False, help="Use base256-encoded input and output")
|
||||
|
||||
parser.add_argument("--version", action="version", version="rnid {version}".format(version=__version__))
|
||||
|
||||
@@ -154,6 +161,7 @@ def main():
|
||||
if args.announce: announce(args, identity); op = True
|
||||
if args.validate: validate(args, identity or args.identity); op = True
|
||||
if args.sign: sign(args, identity); op = True
|
||||
if args.sign_message: sign_message(args, identity); op = True
|
||||
if args.encrypt: encrypt(args, identity); op = True
|
||||
if args.decrypt: decrypt(args, identity); op = True
|
||||
if args.write: write_identity(args, identity); op = True
|
||||
@@ -375,9 +383,9 @@ def announce(args, identity):
|
||||
except Exception as e: print(f"An error ocurred while attempting to send the announce: {e}"); exit(R_UNKNOWN_ERROR)
|
||||
|
||||
|
||||
###################################
|
||||
# Signing & Validation Operations #
|
||||
###################################
|
||||
########################################
|
||||
# Canonical RSG Manipulation Functions #
|
||||
########################################
|
||||
|
||||
def get_rsg_data(rsg):
|
||||
rsg_data = None
|
||||
@@ -386,11 +394,23 @@ def get_rsg_data(rsg):
|
||||
elif type(rsg) == str:
|
||||
try: rsg_data = base64.urlsafe_b64decode(rsg)
|
||||
except: pass
|
||||
try: rsg_data = base64.b32decode(rsg)
|
||||
try: rsg_data = base64.b32decode(rsg.strip(RSG_PADDING))
|
||||
except: pass
|
||||
try: rsg_data = bytes.fromhex(rsg.strip(RSG_PADDING))
|
||||
except: pass
|
||||
try: rsg_data = RNS.b256_to_bytes(rsg.strip(RSG_PADDING.decode("utf-8")))
|
||||
except: pass
|
||||
|
||||
return rsg_data
|
||||
|
||||
def extract_signed_rsg_data(rsg):
|
||||
siglen = RNS.Identity.SIGLENGTH//8
|
||||
rsg_data = get_rsg_data(rsg)
|
||||
envelope = rsg_data[siglen:]
|
||||
|
||||
try: return mp.unpackb(envelope)
|
||||
except: return None
|
||||
|
||||
def get_rsg_hash(message):
|
||||
sha = None
|
||||
if type(message) == bytes: sha = sha256(message)
|
||||
@@ -459,34 +479,114 @@ def validate_rsg(rsg, message=None, required_signer=None):
|
||||
|
||||
return False, signed_data, signing_identity
|
||||
|
||||
def create_rsg(signer_identity, message, note=None, meta=None, output="bin"):
|
||||
if not output in ["bin", "hex", "base32", "base64"]: raise TypeError(f"Invalid output format for rsg creation")
|
||||
if not type(signer_identity) == RNS.Identity: raise TypeError(f"{signer_identity} is not a Reticulum Identity")
|
||||
if not signer_identity.get_private_key(): raise ValueError(f"{signer_identity} does not hold a private key")
|
||||
def create_rsg(signer_identity, message, embed=False, note=None, meta=None, output="bin"):
|
||||
if not output in ["bin", "hex", "base32", "base256", "base64"]: raise TypeError(f"Invalid output format for rsg creation")
|
||||
if not type(signer_identity) == RNS.Identity: raise TypeError(f"{signer_identity} is not a Reticulum Identity")
|
||||
if not signer_identity.get_private_key(): raise ValueError(f"{signer_identity} does not hold a private key")
|
||||
|
||||
signed_data = { "hashtype": "sha256", "hash": get_rsg_hash(message),
|
||||
"meta": { "signer": signer_identity.hash,
|
||||
"pubkey": signer_identity.get_public_key(),
|
||||
"note" : note } }
|
||||
|
||||
if embed:
|
||||
if type(message) == str: message = message.encode("utf-8")
|
||||
signed_data["message"] = message
|
||||
|
||||
if meta and type(meta) == dict:
|
||||
for key in meta:
|
||||
if not key in signed_data["meta"]: signed_data["meta"]["key"] = meta["key"]
|
||||
|
||||
envelope = mp.packb(signed_data)
|
||||
signature = signer_identity.sign(envelope)
|
||||
rsg_data = signature+envelope
|
||||
|
||||
return signature+envelope
|
||||
if output == "bin": rsg = rsg_data
|
||||
elif output == "hex": rsg = RNS.hexrep(rsg_data, delimit=False).encode("ascii")
|
||||
elif output == "base32": rsg = base64.b32encode(rsg_data)
|
||||
elif output == "base64": rsg = base64.urlsafe_b64encode(rsg_data)
|
||||
elif output == "base256": rsg = RNS.b256rep(rsg_data)
|
||||
else: return None
|
||||
|
||||
def validate(args, identity):
|
||||
return rsg
|
||||
|
||||
RSG_ASCII_HEADER = b"#### Start of rsg data "
|
||||
RSG_ASCII_FOOTER = b" End of rsg data ####"
|
||||
RSG_ASCII_ROW_WIDTH = 64
|
||||
RSG_PADDING = b"="
|
||||
def wrap_rsg(rsg):
|
||||
if type(rsg) == str: return wrap_rsg_str(rsg)
|
||||
def pad(chunk): return chunk+(RSG_ASCII_ROW_WIDTH-len(chunk))*RSG_PADDING
|
||||
header = RSG_ASCII_HEADER+b"#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_HEADER))
|
||||
footer = b"#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_FOOTER))+RSG_ASCII_FOOTER
|
||||
wrapped = header+b"\n"
|
||||
read = 0
|
||||
while len(rsg):
|
||||
chunk = rsg[:RSG_ASCII_ROW_WIDTH]
|
||||
if len(chunk) < RSG_ASCII_ROW_WIDTH: chunk = pad(chunk)
|
||||
wrapped += chunk+b"\n"; read += len(chunk)
|
||||
rsg = rsg[len(chunk):]
|
||||
|
||||
wrapped += footer
|
||||
return wrapped.decode("ascii")
|
||||
|
||||
def wrap_rsg_str(rsg):
|
||||
def pad(chunk): return chunk+(RSG_ASCII_ROW_WIDTH-len(chunk))*RSG_PADDING.decode("utf-8")
|
||||
header = RSG_ASCII_HEADER.decode("utf-8")+"#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_HEADER.decode("utf-8")))
|
||||
footer = "#"*(RSG_ASCII_ROW_WIDTH-len(RSG_ASCII_FOOTER.decode("utf-8")))+RSG_ASCII_FOOTER.decode("utf-8")
|
||||
wrapped = header+"\n"
|
||||
read = 0
|
||||
while len(rsg):
|
||||
chunk = rsg[:RSG_ASCII_ROW_WIDTH]
|
||||
if len(chunk) < RSG_ASCII_ROW_WIDTH: chunk = pad(chunk)
|
||||
wrapped += chunk+"\n"; read += len(chunk)
|
||||
rsg = rsg[len(chunk):]
|
||||
|
||||
wrapped += footer
|
||||
return wrapped
|
||||
|
||||
def unwrap_rsg(wrapped_rsg):
|
||||
unwrapped = ""
|
||||
if type(wrapped_rsg) == bytes: wrapped_rsg = wrapped_rsg.decode("ascii")
|
||||
elif type(wrapped_rsg) == str: pass
|
||||
else: return None
|
||||
|
||||
for line in wrapped_rsg.splitlines():
|
||||
if not line.strip(): continue
|
||||
if line.startswith("#"): continue
|
||||
unwrapped += line
|
||||
|
||||
return unwrapped if unwrapped else None
|
||||
|
||||
|
||||
###################################
|
||||
# Signing & Validation Operations #
|
||||
###################################
|
||||
|
||||
def validate(args, identity, __recursive=False):
|
||||
if type(args.validate) == list:
|
||||
paths = args.validate.copy()
|
||||
validated = 0
|
||||
for path in paths:
|
||||
args.validate = path
|
||||
code = validate(args, identity, __recursive=True)
|
||||
if code != 0: print(f"Sequence error on recursive signature validation"); exit(R_SEQUENCE_ERROR)
|
||||
else: validated += 1
|
||||
|
||||
if len(paths) != validated: print(f"Sequence error on recursive signature validation"); exit(R_SEQUENCE_ERROR)
|
||||
else: exit(R_OK)
|
||||
|
||||
msg_ext = f".{MSG_EXT}"
|
||||
sig_ext = f".{SIG_EXT}"
|
||||
validate_path = os.path.expanduser(args.validate)
|
||||
path_is_msgfile = validate_path.lower().endswith(msg_ext)
|
||||
path_is_sigfile = validate_path.lower().endswith(sig_ext)
|
||||
if path_is_sigfile: signature_path = validate_path; file_path = validate_path[:-len(sig_ext)]
|
||||
else: signature_path = f"{validate_path}{sig_ext}"; file_path = validate_path
|
||||
signature_exists = os.path.isfile(signature_path)
|
||||
file_exists = os.path.isfile(file_path)
|
||||
|
||||
if path_is_msgfile: return validate_message(args, identity, __recursive=__recursive)
|
||||
if not file_exists: print(f"The validation target \"{file_path}\" does not exist"); exit(R_NO_FILE)
|
||||
if not signature_exists: print(f"No signature file exists for \"{file_path}\""); exit(R_NO_FILE)
|
||||
|
||||
@@ -511,7 +611,7 @@ def validate(args, identity):
|
||||
identity_str = RNS.prettyhexrep(identity) if type(identity) == bytes else f"{identity}"
|
||||
signer_description = f"\nThis file was NOT signed by {identity_str or signing_identity}" if identity else ""
|
||||
if not valid: print(f"Invalid signature {signature_path} for file {file_path}{signer_description}"); exit(R_INVALID_SIGNATURE)
|
||||
else: print(f"Signature is valid, the file {file_path} was signed by {signing_identity}"); exit(R_OK)
|
||||
else: print(f"Signature is valid, the file {file_path} was signed by {signing_identity}"); return exit(R_OK) if not __recursive else R_OK
|
||||
|
||||
except Exception as e: print(f"Error while validating {signature_path}: {e}"); exit(R_UNKNOWN_ERROR)
|
||||
|
||||
@@ -530,16 +630,67 @@ def validate(args, identity):
|
||||
|
||||
except Exception as e: print(f"Could not validate signature: {e}"); exit(R_READ_ERROR)
|
||||
|
||||
def sign(args, identity):
|
||||
def validate_message(args, identity, __recursive=False):
|
||||
msg_ext = f".{MSG_EXT}"
|
||||
validate_path = os.path.expanduser(args.validate)
|
||||
path_is_msgfile = validate_path.lower().endswith(msg_ext)
|
||||
if path_is_msgfile: signature_path = validate_path
|
||||
signature_exists = os.path.isfile(signature_path)
|
||||
|
||||
if not signature_exists: print(f"The signature file \"{signature_path}\" does not exist"); exit(R_NO_FILE)
|
||||
|
||||
try:
|
||||
with open(signature_path, "rb") as fh: rsg = fh.read()
|
||||
except Exception as e: print(f"Could not read rsg: {e}"); exit(R_READ_ERROR)
|
||||
|
||||
if type(identity) == str:
|
||||
if not len(identity) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: print("Invalid identity hash length"); exit(R_INVALID_IDENTITY)
|
||||
try: identity = bytes.fromhex(identity)
|
||||
except Exception as e: print(f"Invalid identity hash: {e}"); exit(R_INVALID_IDENTITY)
|
||||
|
||||
try:
|
||||
rsm_contents = extract_signed_rsg_data(rsg)
|
||||
if not "message" in rsm_contents: print(f"No embedded message in {signature_path}"); exit(R_INVALID_SIGNATURE)
|
||||
valid, signed_data, signing_identity = validate_rsg(rsg, message=rsm_contents["message"], required_signer=identity)
|
||||
identity_str = RNS.prettyhexrep(identity) if type(identity) == bytes else f"{identity}"
|
||||
signer_description = f"\nThe message was NOT signed by {identity_str or signing_identity}" if identity else ""
|
||||
if not valid: print(f"Invalid signature in {signature_path}{signer_description}"); exit(R_INVALID_SIGNATURE)
|
||||
else:
|
||||
print(f"\nSignature is valid, the following message was signed by {signing_identity}:\n")
|
||||
print(signed_data["message"].decode("utf-8"))
|
||||
|
||||
return exit(R_OK) if not __recursive else R_OK
|
||||
|
||||
except Exception as e: print(f"Error while validating {signature_path}: {e}"); exit(R_UNKNOWN_ERROR)
|
||||
|
||||
def sign(args, identity, __recursive=False):
|
||||
if type(args.sign) == list:
|
||||
paths = args.sign.copy()
|
||||
signed = 0
|
||||
for path in paths:
|
||||
args.sign = path
|
||||
code = sign(args, identity, __recursive=True)
|
||||
if code != 0: print(f"Sequence error on recursive signature creation"); exit(R_SEQUENCE_ERROR)
|
||||
else: signed += 1
|
||||
|
||||
if len(paths) != signed: print(f"Sequence error on recursive signature creation"); exit(R_SEQUENCE_ERROR)
|
||||
else: exit(R_OK)
|
||||
|
||||
sig_ext = f".{SIG_EXT}"
|
||||
sign_path = os.path.expanduser(args.sign)
|
||||
rsg_path = f"{sign_path}{sig_ext}"
|
||||
file_exists = os.path.isfile(sign_path)
|
||||
signature_exists = os.path.isfile(rsg_path)
|
||||
|
||||
if args.base32: output = "base32"
|
||||
elif args.base64: output = "base64"
|
||||
elif args.base256: output = "base256"
|
||||
elif args.hex: output = "hex"
|
||||
else: output = "bin"
|
||||
|
||||
if not identity.get_private_key(): print(f"Cannot sign \"{sign_path}\", the identity does not hold a private key"); exit(R_NO_PRVKEY)
|
||||
if not file_exists: print(f"The file \"{sign_path}\" does not exist"); exit(R_NO_FILE)
|
||||
if signature_exists and not args.force:
|
||||
if output == "bin" and signature_exists and not args.force:
|
||||
print(f"The signature file \"{rsg_path}\" already exists, not overwriting"); exit(R_FILE_EXISTS)
|
||||
|
||||
try:
|
||||
@@ -548,21 +699,72 @@ def sign(args, identity):
|
||||
with open(rsg_path, "wb") as fh: fh.write(identity.sign(data))
|
||||
|
||||
else:
|
||||
with open(sign_path, "rb") as in_file:
|
||||
rsg = create_rsg(identity, in_file)
|
||||
if not rsg: print(f"No signature created, not writing"); exit(R_UNKNOWN_ERROR)
|
||||
with open(sign_path, "rb") as in_file: rsg = create_rsg(identity, in_file, output=output)
|
||||
if not rsg: print(f"No signature created, not writing"); exit(R_UNKNOWN_ERROR)
|
||||
|
||||
if output == "bin":
|
||||
with open(rsg_path, "wb") as out_file: out_file.write(rsg)
|
||||
|
||||
print(f"Signed file {sign_path} with {identity}"); exit(R_OK)
|
||||
elif output in ["base32", "base64", "base256", "hex"]: print(f"\n{wrap_rsg(rsg)}\n")
|
||||
else: print("No valid output format specified"); exit(R_INVALID_ARGS)
|
||||
|
||||
print(f"Signed file {sign_path} with {identity}"); return exit(R_OK) if not __recursive else R_OK
|
||||
|
||||
except Exception as e: print(f"Could not sign {sign_path}: {e}"); exit(R_UNKNOWN_ERROR)
|
||||
|
||||
def sign_message(args, identity):
|
||||
message = args.sign_message
|
||||
|
||||
if args.base32: output = "base32"
|
||||
elif args.base64: output = "base64"
|
||||
elif args.base256: output = "base256"
|
||||
elif args.hex: output = "hex"
|
||||
else: output = "bin"
|
||||
|
||||
if output == "bin" and not args.write: print("No write path specified"); exit(R_INVALID_ARGS)
|
||||
if not identity.get_private_key(): print(f"Cannot sign \"{sign_path}\", the identity does not hold a private key"); exit(R_NO_PRVKEY)
|
||||
|
||||
if message == NO_MESSAGE: message = get_editor_content()
|
||||
if not message: print("No message specified"); exit(R_INVALID_ARGS)
|
||||
|
||||
try:
|
||||
rsg = create_rsg(identity, message, embed=True, output=output)
|
||||
if not rsg: print(f"No signature created, not writing"); exit(R_UNKNOWN_ERROR)
|
||||
|
||||
if output == "bin":
|
||||
sig_ext = f".{MSG_EXT}"
|
||||
rsg_path = os.path.expanduser(args.write)
|
||||
rsg_path = f"{rsg_path}{sig_ext}" if not rsg_path.endswith(sig_ext) else rsg_path
|
||||
signature_exists = os.path.isfile(rsg_path)
|
||||
if signature_exists and not args.force: print(f"The signature file \"{rsg_path}\" already exists, not overwriting"); exit(R_FILE_EXISTS)
|
||||
with open(rsg_path, "wb") as out_file: out_file.write(rsg)
|
||||
print(f"Message signed with {identity} saved to {rsg_path}"); exit(R_OK)
|
||||
|
||||
elif output in ["base32", "base64", "base256", "hex"]: print(f"\n{wrap_rsg(rsg)}\n")
|
||||
else: print("No valid output format specified"); exit(R_INVALID_ARGS)
|
||||
|
||||
print(f"Message signed with {identity}"); exit(R_OK)
|
||||
|
||||
except Exception as e: print(f"Could not sign message: {e}"); exit(R_UNKNOWN_ERROR)
|
||||
|
||||
|
||||
######################################
|
||||
# Encryption & Decryption Operations #
|
||||
######################################
|
||||
|
||||
def encrypt(args, identity):
|
||||
def encrypt(args, identity, __recursive=False):
|
||||
if type(args.encrypt) == list:
|
||||
paths = args.encrypt.copy()
|
||||
encrypted = 0
|
||||
for path in paths:
|
||||
args.encrypt = path
|
||||
code = encrypt(args, identity, __recursive=True)
|
||||
if code != 0: print(f"Sequence error on recursive file encryption"); exit(R_SEQUENCE_ERROR)
|
||||
else: encrypted += 1
|
||||
|
||||
if len(paths) != encrypted: print(f"Sequence error on recursive file encryption"); exit(R_SEQUENCE_ERROR)
|
||||
else: exit(R_OK)
|
||||
|
||||
enc_ext = f".{ENCRYPT_EXT}"
|
||||
encrypt_path = os.path.expanduser(args.encrypt)
|
||||
rfe_path = args.write if args.write else f"{encrypt_path}{enc_ext}"
|
||||
@@ -591,9 +793,21 @@ def encrypt(args, identity):
|
||||
except Exception as e: print(f"\nError writing encrypted output to {rfe_path}: {e}"); exit(R_WRITE_ERROR)
|
||||
except Exception as e: print(f"\nError reading {encrypt_path} for encryption: {e}"); exit(R_WRITE_ERROR)
|
||||
|
||||
print(f"\nFile {encrypt_path} encrypted for {identity} to {rfe_path}"); exit(R_OK)
|
||||
print(f"\nFile {encrypt_path} encrypted for {identity} to {rfe_path}"); return exit(R_OK) if not __recursive else R_OK
|
||||
|
||||
def decrypt(args, identity, __recursive=False):
|
||||
if type(args.decrypt) == list:
|
||||
paths = args.decrypt.copy()
|
||||
decrypted = 0
|
||||
for path in paths:
|
||||
args.decrypt = path
|
||||
code = decrypt(args, identity, __recursive=True)
|
||||
if code != 0: print(f"Sequence error on recursive file decryption"); exit(R_SEQUENCE_ERROR)
|
||||
else: decrypted += 1
|
||||
|
||||
if len(paths) != decrypted: print(f"Sequence error on recursive file decryption"); exit(R_SEQUENCE_ERROR)
|
||||
else: exit(R_OK)
|
||||
|
||||
def decrypt(args, identity):
|
||||
enc_ext = f".{ENCRYPT_EXT}"
|
||||
rfe_path = os.path.expanduser(args.decrypt)
|
||||
if not rfe_path.endswith(enc_ext): print(f"The file {rfe_path} does not appear to be a Reticulum encrypted file"); exit(R_INVALID_FILE)
|
||||
@@ -630,7 +844,7 @@ def decrypt(args, identity):
|
||||
except Exception as e: print(f"\nError writing decrypted output to {decrypt_path}: {e}"); exit(R_WRITE_ERROR)
|
||||
except Exception as e: print(f"\nError reading {rfe_path} for decryption: {e}"); exit(R_WRITE_ERROR)
|
||||
|
||||
print(f"\nFile {rfe_path} decrypted to {decrypt_path}"); exit(R_OK)
|
||||
print(f"\nFile {rfe_path} decrypted to {decrypt_path}"); return exit(R_OK) if not __recursive else R_OK
|
||||
|
||||
|
||||
################
|
||||
@@ -725,6 +939,33 @@ def export_prv_identity(args, identity):
|
||||
# Helper & Utility Functions #
|
||||
##############################
|
||||
|
||||
def get_editor_content():
|
||||
import subprocess
|
||||
from tempfile import NamedTemporaryFile
|
||||
template = ""
|
||||
editor = os.environ.get("EDITOR", "")
|
||||
if not editor:
|
||||
for fallback in ["nano", "vim", "vi"]:
|
||||
try:
|
||||
subprocess.run(["which", fallback], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
editor = fallback
|
||||
break
|
||||
except subprocess.CalledProcessError: continue
|
||||
|
||||
if not editor: print("Could not launch editor"); exit(R_READ_ERROR);
|
||||
try:
|
||||
with NamedTemporaryFile(mode="w+", suffix=".tmp", delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
tmp.write(template)
|
||||
|
||||
result = subprocess.run([editor, tmp_path])
|
||||
if result.returncode != 0: print(f"Editor exited with error code {result.returncode}"); os.unlink(tmp_path); exit(R_READ_ERROR)
|
||||
with open(tmp_path, "r") as f: content = f.read()
|
||||
os.unlink(tmp_path)
|
||||
return content.encode("utf-8")
|
||||
|
||||
except Exception as e: print(f"Could not get content from editor: {e}"); exit(R_READ_ERROR)
|
||||
|
||||
def spin(until=None, msg=None, timeout=None):
|
||||
i = 0
|
||||
syms = "⢄⢂⢁⡁⡈⡐⡠"
|
||||
|
||||
@@ -59,7 +59,8 @@ def connect_remote(destination_hash, auth_identity, timeout, no_output = False,
|
||||
remote_identity = RNS.Identity.recall(destination_hash)
|
||||
|
||||
def remote_link_closed(link):
|
||||
if link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if link.teardown_reason == RNS.Link.INITIATOR_CLOSED: return
|
||||
elif link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("The link timed out, exiting now")
|
||||
|
||||
+122
-53
@@ -60,8 +60,11 @@ def size_str(num, suffix='B'):
|
||||
|
||||
request_result = None
|
||||
request_concluded = False
|
||||
first_remote_req = True
|
||||
remote_destination = None
|
||||
remote_link = None
|
||||
def get_remote_status(destination_hash, include_lstats, identity, no_output=False, timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
|
||||
global request_result, request_concluded
|
||||
global request_result, request_concluded, first_remote_req, remote_destination, remote_link
|
||||
link_count = None
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
@@ -81,7 +84,8 @@ def get_remote_status(destination_hash, include_lstats, identity, no_output=Fals
|
||||
remote_identity = RNS.Identity.recall(destination_hash)
|
||||
|
||||
def remote_link_closed(link):
|
||||
if link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if link.teardown_reason == RNS.Link.INITIATOR_CLOSED: return
|
||||
elif link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print("The link timed out, exiting now")
|
||||
@@ -107,44 +111,50 @@ def get_remote_status(destination_hash, include_lstats, identity, no_output=Fals
|
||||
response = request_receipt.response
|
||||
if isinstance(response, list):
|
||||
status = response[0]
|
||||
if len(response) > 1:
|
||||
link_count = response[1]
|
||||
else:
|
||||
link_count = None
|
||||
if len(response) > 1: link_count = response[1]
|
||||
else: link_count = None
|
||||
|
||||
request_result = (status, link_count)
|
||||
|
||||
request_concluded = True
|
||||
|
||||
def remote_link_established(link):
|
||||
if not no_output:
|
||||
global first_remote_req
|
||||
if not no_output and first_remote_req:
|
||||
print("\r \r", end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
link.identify(identity)
|
||||
link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed)
|
||||
first_remote_req = False
|
||||
|
||||
if not no_output:
|
||||
if not remote_link and not no_output:
|
||||
print("\r \r", end="")
|
||||
print("Establishing link with remote transport instance...", end=" ")
|
||||
sys.stdout.flush()
|
||||
|
||||
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
|
||||
link = RNS.Link(remote_destination)
|
||||
link.set_link_established_callback(remote_link_established)
|
||||
link.set_link_closed_callback(remote_link_closed)
|
||||
if not remote_destination:
|
||||
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
|
||||
|
||||
if remote_link and remote_link.status == RNS.Link.ACTIVE:
|
||||
request_concluded = False
|
||||
remote_link.request("/status", data = [include_lstats], response_callback = got_response, failed_callback = request_failed)
|
||||
|
||||
while not request_concluded:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
remote_link = RNS.Link(remote_destination)
|
||||
remote_link.set_link_established_callback(remote_link_established)
|
||||
remote_link.set_link_closed_callback(remote_link_closed)
|
||||
|
||||
while not request_concluded: time.sleep(0.1)
|
||||
|
||||
if request_result != None:
|
||||
print("\r \r", end="")
|
||||
|
||||
return request_result
|
||||
|
||||
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, lstats=False, sorting=None, sort_reverse=False,
|
||||
remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True, rns_instance=None,
|
||||
traffic_totals=False, discovered_interfaces=False, config_entries=False):
|
||||
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, pstats=False, lstats=False, sorting=None,
|
||||
sort_reverse=False, remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True,
|
||||
rns_instance=None, traffic_totals=False, discovered_interfaces=False, config_entries=False, burst_filter=False):
|
||||
|
||||
if remote: require_shared = False
|
||||
else: require_shared = True
|
||||
@@ -300,28 +310,22 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
|
||||
if remote:
|
||||
try:
|
||||
if management_identity is None:
|
||||
raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.")
|
||||
if management_identity is None: raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.")
|
||||
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(remote) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
if len(remote) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
identity_hash = bytes.fromhex(remote)
|
||||
destination_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
|
||||
identity = RNS.Identity.from_file(os.path.expanduser(management_identity))
|
||||
if identity == None:
|
||||
raise ValueError("Could not load management identity from "+str(management_identity))
|
||||
if identity == None: raise ValueError("Could not load management identity from "+str(management_identity))
|
||||
|
||||
try:
|
||||
remote_status = get_remote_status(destination_hash, lstats, identity, no_output=json, timeout=remote_timeout)
|
||||
if remote_status != None:
|
||||
stats, link_count = remote_status
|
||||
except Exception as e:
|
||||
raise e
|
||||
if remote_status != None: stats, link_count = remote_status
|
||||
except Exception as e: raise e
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
@@ -375,6 +379,10 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
interfaces.sort(key=lambda i: i["incoming_announce_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "atx":
|
||||
interfaces.sort(key=lambda i: i["outgoing_announce_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "prx":
|
||||
interfaces.sort(key=lambda i: i["incoming_pr_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "ptx":
|
||||
interfaces.sort(key=lambda i: i["outgoing_pr_frequency"], reverse=not sort_reverse)
|
||||
if sorting == "held":
|
||||
interfaces.sort(key=lambda i: i["held_announces"], reverse=not sort_reverse)
|
||||
|
||||
@@ -393,7 +401,18 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
):
|
||||
|
||||
if not (name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False)):
|
||||
if name_filter == None or name_filter.lower() in name.lower():
|
||||
if name_filter == None and burst_filter == None: show_if = True
|
||||
elif not burst_filter:
|
||||
if not name_filter or name_filter.lower() in name.lower(): show_if = True
|
||||
else: show_if = False
|
||||
elif burst_filter:
|
||||
burst_act = True if ("burst_active" in ifstat and "pr_burst_active" in ifstat) and (ifstat["burst_active"] or ifstat["pr_burst_active"]) else False
|
||||
nfilt = name_filter.lower() in name.lower() if name_filter else False
|
||||
if burst_act or nfilt: show_if = True
|
||||
else: show_if = False
|
||||
else: show_if = True
|
||||
|
||||
if show_if:
|
||||
print("")
|
||||
|
||||
if ifstat["status"]: ss = "Up"
|
||||
@@ -542,26 +561,71 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
elif art and arp != None: art_str = f"(t:{RNS.prettytime(art)}/p:{RNS.prettytime(arp)})"
|
||||
elif art: art_str = f"(t:{RNS.prettytime(art)})"
|
||||
else: art_str = ""
|
||||
|
||||
burst_str = ""
|
||||
if "burst_active" in ifstat and ifstat["burst_active"]:
|
||||
for_str = RNS.prettytime(time.time()-ifstat["burst_activated"])
|
||||
burst_str = f" burst for {for_str}"
|
||||
|
||||
pburst_str = ""
|
||||
if "pr_burst_active" in ifstat and ifstat["pr_burst_active"]:
|
||||
for_str = RNS.prettytime(time.time()-ifstat["pr_burst_activated"])
|
||||
pburst_str = f"burst for {for_str}"
|
||||
|
||||
rxb_str = "↓"+RNS.prettysize(ifstat["rxb"])
|
||||
txb_str = "↑"+RNS.prettysize(ifstat["txb"])
|
||||
strdiff = len(rxb_str)-len(txb_str)
|
||||
if strdiff > 0: txb_str += " "*strdiff
|
||||
elif strdiff < 0: rxb_str += " "*-strdiff
|
||||
|
||||
|
||||
asr = False
|
||||
if astats and "incoming_announce_frequency" in ifstat and ifstat["incoming_announce_frequency"] != None:
|
||||
oaf = RNS.prettyfrequency(ifstat["outgoing_announce_frequency"])+"↑"
|
||||
iaf = RNS.prettyfrequency(ifstat["incoming_announce_frequency"])+"↓"
|
||||
if clients != None and clients > 0: pc_str = f"{RNS.prettyfrequency(ifstat["outgoing_announce_frequency"]/clients)}/c"
|
||||
oan = ifstat["outgoing_announce_frequency"]
|
||||
ian = ifstat["incoming_announce_frequency"]
|
||||
if name.startswith("Shared Instance[") and clients and clients > 0: oan = oan-(oan/clients) # Sub rnstatus own part
|
||||
oaf = RNS.prettyfrequency(oan, d=1, lpf=True)
|
||||
iaf = RNS.prettyfrequency(ian, d=1, lpf=True)
|
||||
|
||||
cspec = "c"
|
||||
if clients == None and "peers" in ifstat and ifstat["peers"]: clients = ifstat["peers"]; cspec = "p"
|
||||
if clients != None and clients > 0: pc_str = f"{RNS.prettyfrequency(ifstat['outgoing_announce_frequency']/clients, d=1, lpf=True)}/{cspec}"
|
||||
else: pc_str = ""
|
||||
strdiff = len(oaf)-len(iaf)
|
||||
if strdiff > 0: iaf += " "*strdiff
|
||||
elif strdiff < 0: oaf += " "*-strdiff
|
||||
strdiff = len(rxb_str)-len(oaf)
|
||||
if strdiff > 0: oaf += " "*strdiff
|
||||
elif strdiff < 0: txb_str += " "*-strdiff; rxb_str += " "*-strdiff
|
||||
asr = True
|
||||
|
||||
psr = False
|
||||
if pstats and "incoming_pr_frequency" in ifstat and ifstat["incoming_pr_frequency"] != None:
|
||||
opn = ifstat["outgoing_pr_frequency"]
|
||||
ipn = ifstat["incoming_pr_frequency"]
|
||||
if name.startswith("Shared Instance[") and clients and clients > 0: opn = opn-(opn/clients) # Sub rnstatus own part
|
||||
if astats:
|
||||
opf = "↑"+RNS.prettyfrequency(opn, d=1, lpf=True)
|
||||
ipf = "↓"+RNS.prettyfrequency(ipn, d=1, lpf=True)
|
||||
else:
|
||||
opf = RNS.prettyfrequency(opn,d=1, lpf=True)+"↑"
|
||||
ipf = RNS.prettyfrequency(ipn,d=1, lpf=True)+"↓"
|
||||
cspec = "c"
|
||||
if clients == None and "peers" in ifstat and ifstat["peers"]: clients = ifstat["peers"]; cspec = "p"
|
||||
if clients != None and clients > 0: rpc_str = f"{RNS.prettyfrequency(ifstat['outgoing_pr_frequency']/clients, d=1, lpf=True)}/{cspec}"
|
||||
else: rpc_str = ""
|
||||
psr = True
|
||||
|
||||
if not asr: iaf = ""; oaf = ""
|
||||
if not psr: ipf = ""; opf = ""
|
||||
amlen = max(len(iaf), len(oaf))
|
||||
iaf += (amlen-len(iaf))*" "+"↓"
|
||||
oaf += (amlen-len(oaf))*" "+"↑"
|
||||
mlen = max(max(len(iaf), len(oaf), len(rxb_str), len(txb_str), len(ipf), len(opf)), 10)
|
||||
iaf += (mlen-len(iaf))*" "
|
||||
oaf += (mlen-len(oaf))*" "
|
||||
ipf += (mlen-len(ipf))*" "
|
||||
opf += (mlen-len(opf))*" "
|
||||
rxb_str += (mlen-len(rxb_str))*" "
|
||||
txb_str += (mlen-len(txb_str))*" "
|
||||
|
||||
if psr:
|
||||
print(f" Path Rqs. : {opf} {rpc_str}")
|
||||
print(f" {ipf} {pburst_str}")
|
||||
|
||||
if asr:
|
||||
print(f" Announces : {oaf} {pc_str}")
|
||||
print(f" {iaf} {art_str}")
|
||||
print(f" {iaf} {art_str}{burst_str}")
|
||||
|
||||
rxstat = rxb_str
|
||||
txstat = txb_str
|
||||
@@ -624,9 +688,11 @@ def main(must_exit=True, rns_instance=None):
|
||||
|
||||
parser.add_argument("-a", "--all", action="store_true", help="show all interfaces", default=False)
|
||||
parser.add_argument("-A", "--announce-stats", action="store_true", help="show announce stats", default=False)
|
||||
parser.add_argument("-P", "--pr-stats", action="store_true", help="show path request stats", default=False)
|
||||
parser.add_argument("-l", "--link-stats", action="store_true", help="show link stats", default=False)
|
||||
parser.add_argument("-B", "--burst", action="store_true", help="only show interfaces with active bursts", default=False)
|
||||
parser.add_argument("-t", "--totals", action="store_true", help="display traffic totals", default=False)
|
||||
parser.add_argument("-s", "--sort", action="store", help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, held]", default=None, type=str)
|
||||
parser.add_argument("-s", "--sort", action="store", help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, prx, ptx, held]", default=None, type=str)
|
||||
parser.add_argument("-r", "--reverse", action="store_true", help="reverse sorting", default=False)
|
||||
parser.add_argument("-j", "--json", action="store_true", help="output in JSON format", default=False)
|
||||
parser.add_argument("-R", action="store", metavar="hash", help="transport identity hash of remote instance to get status from", default=None, type=str)
|
||||
@@ -654,15 +720,16 @@ def main(must_exit=True, rns_instance=None):
|
||||
exit(1)
|
||||
|
||||
while True:
|
||||
st = time.time()
|
||||
buffer = io.StringIO()
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = buffer
|
||||
|
||||
try:
|
||||
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json,
|
||||
astats=args.announce_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, remote=args.R,
|
||||
management_identity=args.i, remote_timeout=args.w, must_exit=False, rns_instance=reticulum, traffic_totals=args.totals,
|
||||
discovered_interfaces=args.discovered, config_entries=args.D)
|
||||
astats=args.announce_stats, pstats=args.pr_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse,
|
||||
remote=args.R, management_identity=args.i, remote_timeout=args.w, must_exit=False, rns_instance=reticulum,
|
||||
traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst)
|
||||
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
@@ -670,14 +737,16 @@ def main(must_exit=True, rns_instance=None):
|
||||
output = buffer.getvalue()
|
||||
print("\033[H\033[2J", end="")
|
||||
print(output, end="", flush=True)
|
||||
|
||||
time.sleep(args.monitor_interval)
|
||||
|
||||
td = time.time()-st
|
||||
sleeptime = max(args.monitor_interval-td, 0.2)
|
||||
time.sleep(sleeptime)
|
||||
|
||||
else:
|
||||
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json,
|
||||
astats=args.announce_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, remote=args.R,
|
||||
management_identity=args.i, remote_timeout=args.w, must_exit=must_exit, rns_instance=rns_instance, traffic_totals=args.totals,
|
||||
discovered_interfaces=args.discovered, config_entries=args.D)
|
||||
astats=args.announce_stats, pstats=args.pr_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse,
|
||||
remote=args.R, management_identity=args.i, remote_timeout=args.w, must_exit=must_exit, rns_instance=rns_instance,
|
||||
traffic_totals=args.totals, discovered_interfaces=args.discovered, config_entries=args.D, burst_filter=args.burst)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
|
||||
+95
-114
@@ -94,22 +94,14 @@ _always_override_destination = False
|
||||
logging_lock = threading.Lock()
|
||||
|
||||
def loglevelname(level):
|
||||
if (level == LOG_CRITICAL):
|
||||
return "[Critical]"
|
||||
if (level == LOG_ERROR):
|
||||
return "[Error] "
|
||||
if (level == LOG_WARNING):
|
||||
return "[Warning] "
|
||||
if (level == LOG_NOTICE):
|
||||
return "[Notice] "
|
||||
if (level == LOG_INFO):
|
||||
return "[Info] "
|
||||
if (level == LOG_VERBOSE):
|
||||
return "[Verbose] "
|
||||
if (level == LOG_DEBUG):
|
||||
return "[Debug] "
|
||||
if (level == LOG_EXTREME):
|
||||
return "[Extra] "
|
||||
if (level == LOG_CRITICAL): return "[Critical]"
|
||||
if (level == LOG_ERROR): return "[Error] "
|
||||
if (level == LOG_WARNING): return "[Warning] "
|
||||
if (level == LOG_NOTICE): return "[Notice] "
|
||||
if (level == LOG_INFO): return "[Info] "
|
||||
if (level == LOG_VERBOSE): return "[Verbose] "
|
||||
if (level == LOG_DEBUG): return "[Debug] "
|
||||
if (level == LOG_EXTREME): return "[Extra] "
|
||||
|
||||
return "Unknown"
|
||||
|
||||
@@ -133,13 +125,10 @@ def log(msg, level=3, _override_destination = False, pt=False):
|
||||
global _always_override_destination, compact_log_fmt
|
||||
msg = str(msg)
|
||||
if loglevel >= level:
|
||||
if pt:
|
||||
logstring = "["+precise_timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
||||
if pt: logstring = "["+precise_timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
||||
else:
|
||||
if not compact_log_fmt:
|
||||
logstring = "["+timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
||||
else:
|
||||
logstring = "["+timestamp_str(time.time())+"] "+msg
|
||||
if not compact_log_fmt: logstring = "["+timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
||||
else: logstring = "["+timestamp_str(time.time())+"] "+msg
|
||||
|
||||
with logging_lock:
|
||||
if (logdest == LOG_STDOUT or _always_override_destination or _override_destination):
|
||||
@@ -182,14 +171,11 @@ def trace_exception(e):
|
||||
log(exception_info, LOG_ERROR)
|
||||
|
||||
def hexrep(data, delimit=True):
|
||||
try:
|
||||
iter(data)
|
||||
except TypeError:
|
||||
data = [data]
|
||||
try: iter(data)
|
||||
except TypeError: data = [data]
|
||||
|
||||
delimiter = ":"
|
||||
if not delimit:
|
||||
delimiter = ""
|
||||
if not delimit: delimiter = ""
|
||||
hexrep = delimiter.join("{:02x}".format(c) for c in data)
|
||||
return hexrep
|
||||
|
||||
@@ -198,11 +184,6 @@ def prettyhexrep(data):
|
||||
hexrep = "<"+delimiter.join("{:02x}".format(c) for c in data)+">"
|
||||
return hexrep
|
||||
|
||||
def prettyb256rep(data):
|
||||
delimiter = ""
|
||||
b256rep = "<"+delimiter.join(b256_rep(c) for c in data)+">"
|
||||
return b256rep
|
||||
|
||||
def prettyspeed(num, suffix="b"):
|
||||
return prettysize(num/8, suffix=suffix)+"ps"
|
||||
|
||||
@@ -217,23 +198,24 @@ def prettysize(num, suffix='B'):
|
||||
|
||||
for unit in units:
|
||||
if abs(num) < 1000.0:
|
||||
if unit == "":
|
||||
return "%.0f %s%s" % (num, unit, suffix)
|
||||
else:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
if unit == "": return "%.0f %s%s" % (num, unit, suffix)
|
||||
else: return "%.2f %s%s" % (num, unit, suffix)
|
||||
num /= 1000.0
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
|
||||
def prettyfrequency(hz, suffix="Hz"):
|
||||
def prettyfrequency(hz, suffix="Hz", d=2, lpf=False):
|
||||
if hz == 0: return "0 Hz"
|
||||
num = hz*1e6
|
||||
units = ["µ", "m", "", "K","M","G","T","P","E","Z"]
|
||||
if not lpf: num = hz*1e6
|
||||
else: num = hz
|
||||
if not lpf: units = ["µ", "m", "", "K","M","G","T","P","E","Z"]
|
||||
else: units = ["", "K","M","G","T","P","E","Z"]
|
||||
last_unit = "Y"
|
||||
|
||||
for unit in units:
|
||||
if abs(num) < 1000.0:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
if d == 2: return "%.2f %s%s" % (num, unit, suffix)
|
||||
else: return "%s %s%s" % (str(round(num,d)), unit, suffix)
|
||||
num /= 1000.0
|
||||
|
||||
return "%.2f%s%s" % (num, last_unit, suffix)
|
||||
@@ -248,8 +230,7 @@ def prettydistance(m, suffix="m"):
|
||||
if unit == "m": divisor = 10
|
||||
if unit == "c": divisor = 100
|
||||
|
||||
if abs(num) < divisor:
|
||||
return "%.2f %s%s" % (num, unit, suffix)
|
||||
if abs(num) < divisor: return "%.2f %s%s" % (num, unit, suffix)
|
||||
num /= divisor
|
||||
|
||||
return "%.2f %s%s" % (num, last_unit, suffix)
|
||||
@@ -266,10 +247,8 @@ def prettytime(time, verbose=False, compact=False):
|
||||
time %= 3600
|
||||
minutes = int(time // 60)
|
||||
time %= 60
|
||||
if compact:
|
||||
seconds = int(time)
|
||||
else:
|
||||
seconds = round(time, 2)
|
||||
if compact: seconds = int(time)
|
||||
else: seconds = round(time, 2)
|
||||
|
||||
ss = "" if seconds == 1 else "s"
|
||||
sm = "" if minutes == 1 else "s"
|
||||
@@ -298,22 +277,16 @@ def prettytime(time, verbose=False, compact=False):
|
||||
tstr = ""
|
||||
for c in components:
|
||||
i += 1
|
||||
if i == 1:
|
||||
pass
|
||||
elif i < len(components):
|
||||
tstr += ", "
|
||||
elif i == len(components):
|
||||
tstr += " and "
|
||||
if i == 1: pass
|
||||
elif i < len(components): tstr += ", "
|
||||
elif i == len(components): tstr += " and "
|
||||
|
||||
tstr += c
|
||||
|
||||
if tstr == "":
|
||||
return "0s"
|
||||
if tstr == "": return "0s"
|
||||
else:
|
||||
if not neg:
|
||||
return tstr
|
||||
else:
|
||||
return f"-{tstr}"
|
||||
if not neg: return tstr
|
||||
else: return f"-{tstr}"
|
||||
|
||||
def prettyshorttime(time, verbose=False, compact=False):
|
||||
neg = False
|
||||
@@ -325,10 +298,8 @@ def prettyshorttime(time, verbose=False, compact=False):
|
||||
seconds = int(time // 1e6); time %= 1e6
|
||||
milliseconds = int(time // 1e3); time %= 1e3
|
||||
|
||||
if compact:
|
||||
microseconds = int(time)
|
||||
else:
|
||||
microseconds = round(time, 2)
|
||||
if compact: microseconds = int(time)
|
||||
else: microseconds = round(time, 2)
|
||||
|
||||
ss = "" if seconds == 1 else "s"
|
||||
sms = "" if milliseconds == 1 else "s"
|
||||
@@ -352,22 +323,16 @@ def prettyshorttime(time, verbose=False, compact=False):
|
||||
tstr = ""
|
||||
for c in components:
|
||||
i += 1
|
||||
if i == 1:
|
||||
pass
|
||||
elif i < len(components):
|
||||
tstr += ", "
|
||||
elif i == len(components):
|
||||
tstr += " and "
|
||||
if i == 1: pass
|
||||
elif i < len(components): tstr += ", "
|
||||
elif i == len(components): tstr += " and "
|
||||
|
||||
tstr += c
|
||||
|
||||
if tstr == "":
|
||||
return "0us"
|
||||
if tstr == "": return "0us"
|
||||
else:
|
||||
if not neg:
|
||||
return tstr
|
||||
else:
|
||||
return f"-{tstr}"
|
||||
if not neg: return tstr
|
||||
else: return f"-{tstr}"
|
||||
|
||||
def phyparams():
|
||||
print("Required Physical Layer MTU : "+str(Reticulum.MTU)+" bytes")
|
||||
@@ -378,8 +343,7 @@ def phyparams():
|
||||
print("Link Public Key Size : "+str(Link.ECPUBSIZE*8)+" bits")
|
||||
print("Link Private Key Size : "+str(Link.KEYSIZE*8)+" bits")
|
||||
|
||||
def panic():
|
||||
os._exit(255)
|
||||
def panic(): os._exit(255)
|
||||
|
||||
exit_called = False
|
||||
def exit(code=0):
|
||||
@@ -400,8 +364,7 @@ class Profiler:
|
||||
|
||||
@staticmethod
|
||||
def get_profiler(tag=None, super_tag=None):
|
||||
if tag in Profiler.profilers:
|
||||
return Profiler.profilers[tag]
|
||||
if tag in Profiler.profilers: return Profiler.profilers[tag]
|
||||
else:
|
||||
profiler = Profiler(tag, super_tag)
|
||||
Profiler.profilers[tag] = profiler
|
||||
@@ -413,13 +376,14 @@ class Profiler:
|
||||
self.pause_started = None
|
||||
self.tag = tag
|
||||
self.super_tag = super_tag
|
||||
|
||||
if self.super_tag in Profiler.profilers:
|
||||
self.super_profiler = Profiler.profilers[self.super_tag]
|
||||
self.pause_super = self.super_profiler.pause
|
||||
self.resume_super = self.super_profiler.resume
|
||||
|
||||
else:
|
||||
def noop(self=None):
|
||||
pass
|
||||
def noop(self=None): pass
|
||||
self.super_profiler = None
|
||||
self.pause_super = noop
|
||||
self.resume_super = noop
|
||||
@@ -429,8 +393,7 @@ class Profiler:
|
||||
tag = self.tag
|
||||
super_tag = self.super_tag
|
||||
thread_ident = threading.get_ident()
|
||||
if not tag in Profiler.tags:
|
||||
Profiler.tags[tag] = {"threads": {}, "super": super_tag}
|
||||
if not tag in Profiler.tags: Profiler.tags[tag] = {"threads": {}, "super": super_tag}
|
||||
if not thread_ident in Profiler.tags[tag]["threads"]:
|
||||
Profiler.tags[tag]["threads"][thread_ident] = {"current_start": None, "captures": []}
|
||||
|
||||
@@ -466,8 +429,7 @@ class Profiler:
|
||||
self.resume_super()
|
||||
|
||||
@staticmethod
|
||||
def ran():
|
||||
return Profiler._ran
|
||||
def ran(): return Profiler._ran
|
||||
|
||||
@staticmethod
|
||||
def results():
|
||||
@@ -484,41 +446,35 @@ class Profiler:
|
||||
sample_count = len(thread_captures)
|
||||
|
||||
if sample_count > 1:
|
||||
thread_results = {
|
||||
"count": sample_count,
|
||||
"mean": mean(thread_captures),
|
||||
"median": median(thread_captures),
|
||||
"stdev": stdev(thread_captures)
|
||||
}
|
||||
thread_results = { "count": sample_count,
|
||||
"mean": mean(thread_captures),
|
||||
"median": median(thread_captures),
|
||||
"stdev": stdev(thread_captures) }
|
||||
|
||||
elif sample_count == 1:
|
||||
thread_results = {
|
||||
"count": sample_count,
|
||||
"mean": mean(thread_captures),
|
||||
"median": median(thread_captures),
|
||||
"stdev": None
|
||||
}
|
||||
thread_results = { "count": sample_count,
|
||||
"mean": mean(thread_captures),
|
||||
"median": median(thread_captures),
|
||||
"stdev": None }
|
||||
|
||||
tag_captures.extend(thread_captures)
|
||||
|
||||
sample_count = len(tag_captures)
|
||||
if sample_count > 1:
|
||||
tag_results = {
|
||||
"name": tag,
|
||||
"super": tag_entry["super"],
|
||||
"count": len(tag_captures),
|
||||
"mean": mean(tag_captures),
|
||||
"median": median(tag_captures),
|
||||
"stdev": stdev(tag_captures)
|
||||
}
|
||||
tag_results = { "name": tag,
|
||||
"super": tag_entry["super"],
|
||||
"count": len(tag_captures),
|
||||
"mean": mean(tag_captures),
|
||||
"median": median(tag_captures),
|
||||
"stdev": stdev(tag_captures) }
|
||||
|
||||
elif sample_count == 1:
|
||||
tag_results = {
|
||||
"name": tag,
|
||||
"super": tag_entry["super"],
|
||||
"count": len(tag_captures),
|
||||
"mean": mean(tag_captures),
|
||||
"median": median(tag_captures),
|
||||
"stdev": None
|
||||
}
|
||||
tag_results = { "name": tag,
|
||||
"super": tag_entry["super"],
|
||||
"count": len(tag_captures),
|
||||
"mean": mean(tag_captures),
|
||||
"median": median(tag_captures),
|
||||
"stdev": None }
|
||||
|
||||
results[tag] = tag_results
|
||||
|
||||
@@ -552,6 +508,8 @@ class Profiler:
|
||||
|
||||
profile = Profiler.get_profiler
|
||||
|
||||
# The base-256 table is likely to change. Currently, it is just
|
||||
# experimental, so don't count on it too much just yet.
|
||||
b256 = [
|
||||
# 0 1 2 3 4 5 6 7 8 9 A B C D F F
|
||||
"a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p", # 0x0 Latin & numerals
|
||||
@@ -572,4 +530,27 @@ b256 = [
|
||||
"𐌳","𐌸","𐌾","𐐀","𐐁","𐐂","𐐆","𐐇","𐐈","𐐉","𐐊","𐐋","𐐌","𐐍","𐐎","𐐏", # 0xF Gothic & Deseret
|
||||
]
|
||||
|
||||
def b256_rep(input_byte): return b256[int(input_byte)]
|
||||
def b256rep(data): return "".join(bytes_to_b256(data))
|
||||
def prettyb256rep(data): return f"<{b256rep(data)}>"
|
||||
|
||||
def b256_to_byte(point):
|
||||
if not type(point) == str or not len(point) == 1: raise TypeError("Invalid input data for base256 byte decode")
|
||||
try: return b256.index(point)
|
||||
except Exception as e: raise ValueError(f"Could not decode base256 byte: {e}")
|
||||
|
||||
def b256_to_bytes(b256rep):
|
||||
if not type(b256rep) == str: raise TypeError("Invalid input data for base256 decode")
|
||||
try: return bytes([b256.index(c) for c in b256rep])
|
||||
except Exception as e: raise ValueError(f"Could not decode base256: {e}")
|
||||
|
||||
def byte_to_b256(input_byte):
|
||||
if type(input_byte) == bytes and not len(input_byte) == 1: TypeError("Invalid input data for base256 byte encode")
|
||||
if type(input_byte) == bytes and len(input_byte) == 1: input_byte = ord(input_byte)
|
||||
if not type(input_byte) == int: raise TypeError("Invalid input data for base256 byte encode")
|
||||
try: return b256[int(input_byte)]
|
||||
except Exception as e: raise TypeError(f"Could not encode byte to base256: {e}")
|
||||
|
||||
def bytes_to_b256(data):
|
||||
if not type(data) == bytes: raise TypeError("Invalid input data for base256 encode")
|
||||
try: return [byte_to_b256(c) for c in data]
|
||||
except Exception as e: raise TypeError(f"Could not encode to base256: {e}")
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "1.2.4"
|
||||
__version__ = "1.2.6"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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: 33b573b4fa6e799ddf8fd65e522e0b14
|
||||
config: 6d7f4aac8313ba495ab156ec11ab15c0
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB |
@@ -402,6 +402,9 @@ Only releases with ``published`` status are visible through the Nomad Network in
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
Work Documents
|
||||
==============
|
||||
|
||||
@@ -1372,11 +1372,12 @@ a large amount of bogus destinations, and then disconnect, these destination wil
|
||||
never make it into path tables and waste network bandwidth on retransmitted
|
||||
announces.
|
||||
|
||||
**It's important to note** that the ingress control works at the level of *individual
|
||||
sub-interfaces*. As an example, this means that one client on a :ref:`TCP Server Interface<interfaces-tcps>`
|
||||
cannot disrupt processing of incoming announces for other connected clients on the same
|
||||
:ref:`TCP Server Interface<interfaces-tcps>`. All other clients on the same interface will still have new announces
|
||||
processed without interruption.
|
||||
.. note::
|
||||
It's important to remember that the ingress control works at the level of *individual
|
||||
sub-interfaces*. As an example, this means that one client on a :ref:`TCP Server Interface<interfaces-tcps>`
|
||||
cannot disrupt processing of incoming announces for other connected clients on the same
|
||||
:ref:`TCP Server Interface<interfaces-tcps>`. All other clients on the same interface
|
||||
will still have new announces processed without interruption.
|
||||
|
||||
By default, Reticulum will handle this automatically, and ingress announce
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
@@ -1384,8 +1385,7 @@ generally not be neccessary to modify the ingress control configuration,
|
||||
but all the parameters are exposed for configuration if needed.
|
||||
|
||||
* | The ``ingress_control`` option tells Reticulum whether or not
|
||||
to enable announce ingress control on the interface. Defaults to
|
||||
``True``.
|
||||
to enable ingress control on the interface. Defaults to ``True``.
|
||||
|
||||
* | The ``ic_new_time`` option configures how long (in seconds) an
|
||||
interface is considered newly spawned. Defaults to ``2*60*60`` seconds. This
|
||||
@@ -1422,3 +1422,59 @@ but all the parameters are exposed for configuration if needed.
|
||||
must pass between releasing each held announce from the queue. Defaults
|
||||
to ``30`` seconds.
|
||||
|
||||
All of the above settings can be configured both as instance-wide defaults
|
||||
under the ``[reticulum]`` section of the configuration file, or on a per-
|
||||
interface basis under the relevant interface configuration section.
|
||||
|
||||
|
||||
Path Request Burst Control
|
||||
==========================
|
||||
|
||||
In addition the announce controls for newly created destination, Reticulum will also
|
||||
monitor incoming path request activity, and enforce burst controls if per-client rates
|
||||
exceed configured limits. Once path request burst control is activated on an
|
||||
interface, path requests will no longer be propagated further on the network.
|
||||
As with announce burst control, this happens on a per sub-interface basis. One
|
||||
client connecting to a public gateway will not be able to disrupt path request
|
||||
processing for other clients.
|
||||
|
||||
.. warning::
|
||||
Applications that send large amounts of unnecessary path requests will very
|
||||
quickly get rate limited by transport nodes, and the entire system they are
|
||||
running on will not be able to resolve any paths on the network, until the
|
||||
burst subsides and hold period expires. **Do not** write applications like
|
||||
this. Only request paths for destinations you need to communicate with.
|
||||
|
||||
By default, Reticulum will handle this automatically, and ingress path request
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
generally not be neccessary to modify the ingress control configuration,
|
||||
but all the parameters are exposed for configuration if needed.
|
||||
|
||||
* | The ``ingress_control`` option tells Reticulum whether or not
|
||||
to enable ingress control on the interface. Defaults to ``True``.
|
||||
|
||||
* | The ``ic_new_time`` option configures how long (in seconds) an
|
||||
interface is considered newly spawned. Defaults to ``2*60*60`` seconds. This
|
||||
option is useful on publicly accessible interfaces that spawn new
|
||||
sub-interfaces when a new client connects.
|
||||
|
||||
* | The ``ic_pr_burst_freq_new`` option sets the maximum path request
|
||||
ingress frequency for newly spawned interfaces. Defaults to ``3``
|
||||
path requests per second.
|
||||
|
||||
* | The ``ic_pr_burst_freq`` option sets the maximum path request
|
||||
ingress frequency for other interfaces. Defaults to ``8`` path requests
|
||||
per second.
|
||||
|
||||
*If an interface exceeds its burst frequency, incoming path requests
|
||||
from that system will not traverse the network further.*
|
||||
|
||||
* | The ``egress_control`` option enables hard-limiting path request egress
|
||||
control per-interface. Defaults to ``False``
|
||||
|
||||
* | The ``ec_pr_freq`` option sets the hard limit for outbound path requests
|
||||
per second on a given interface.
|
||||
|
||||
All of the above settings can be configured both as instance-wide defaults
|
||||
under the ``[reticulum]`` section of the configuration file, or on a per-
|
||||
interface basis under the relevant interface configuration section.
|
||||
@@ -110,7 +110,7 @@ plugin system for expandability.
|
||||
MeshChatX
|
||||
^^^^^^^^
|
||||
|
||||
A `Reticulum MeshChat fork from the future <https://git.quad4.io/RNS-Things/MeshChatX>`_, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original Reticulum MeshChat project, and is not affiliated with the original project.
|
||||
A `Reticulum MeshChat fork from the future <https://git.quad4.io/RNS-Things/MeshChatX>`_, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork.
|
||||
|
||||
.. only:: html
|
||||
|
||||
@@ -127,56 +127,6 @@ A `Reticulum MeshChat fork from the future <https://git.quad4.io/RNS-Things/Mesh
|
||||
|
||||
Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
MeshChat
|
||||
^^^^^^^^
|
||||
|
||||
The `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ application
|
||||
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
|
||||
page browser and other interesting functionality.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/meshchat_1.webp
|
||||
:align: center
|
||||
:target: https://github.com/liamcottle/reticulum-meshchat
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/meshchat_1.png
|
||||
:align: center
|
||||
:target: https://github.com/liamcottle/reticulum-meshchat
|
||||
|
||||
Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or
|
||||
any other LXMF client.
|
||||
|
||||
Columba
|
||||
^^^^^^^
|
||||
|
||||
`Columba <https://github.com/torlando-tech/columba/>`_ is a simple and familiar LXMF
|
||||
messaging app Android, built with a native Android interface and Material Design 3.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/columba.webp
|
||||
:align: center
|
||||
:width: 25%
|
||||
:target: https://github.com/torlando-tech/columba/
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/columba.png
|
||||
:align: center
|
||||
:width: 25%
|
||||
:target: https://github.com/torlando-tech/columba/
|
||||
|
||||
While still in early and very active development, it is of course also compatible
|
||||
with all other LXMF clients, and allows you to message seamlessly with anyone else
|
||||
using LXMF.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
@@ -31,6 +31,10 @@ Donations are gratefully accepted via the following channels:
|
||||
Are certain features in the development roadmap are important to you or your
|
||||
organisation? Make them a reality quickly by sponsoring their implementation.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
Provide Feedback
|
||||
================
|
||||
Feedback on the usage, functioning and potential dysfunctioning of any and
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const DOCUMENTATION_OPTIONS = {
|
||||
VERSION: '1.2.4',
|
||||
VERSION: '1.2.6',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Code Examples - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -3664,7 +3664,7 @@ will be fully on-par with natively included interfaces, including all supported
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -295,7 +295,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -178,7 +178,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -202,7 +202,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -839,7 +839,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Getting Started Fast - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -967,7 +967,7 @@ All other available modules will still be loaded when needed.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Git Over Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -782,7 +782,7 @@ options:
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Communications Hardware - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -675,7 +675,7 @@ can be used with Reticulum. This includes virtual software modems such as
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -378,8 +378,6 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#retipedia">Retipedia</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#sideband">Sideband</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#meshchatx">MeshChatX</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#meshchat">MeshChat</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#columba">Columba</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#reticulum-relay-chat">Reticulum Relay Chat</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#retibbs">RetiBBS</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#rbrowser">RBrowser</a></li>
|
||||
@@ -394,7 +392,7 @@ to participate in the development of Reticulum itself.</p>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="software.html#protocols">Protocols</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#lxmf">LXMF</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#id17">LXST</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#id16">LXST</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="software.html#rrc">RRC</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -511,6 +509,7 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#interfaces-modes">Interface Modes</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#announce-rate-control">Announce Rate Control</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#new-destination-rate-limiting">New Destination Rate Limiting</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#path-request-burst-control">Path Request Burst Control</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="networks.html">Building Networks</a><ul>
|
||||
@@ -644,7 +643,7 @@ to participate in the development of Reticulum itself.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Configuring Interfaces - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -1533,11 +1533,14 @@ also means, that should a node decide to connect to a public interface, announce
|
||||
a large amount of bogus destinations, and then disconnect, these destination will
|
||||
never make it into path tables and waste network bandwidth on retransmitted
|
||||
announces.</p>
|
||||
<p><strong>It’s important to note</strong> that the ingress control works at the level of <em>individual
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>It’s important to remember that the ingress control works at the level of <em>individual
|
||||
sub-interfaces</em>. As an example, this means that one client on a <a class="reference internal" href="#interfaces-tcps"><span class="std std-ref">TCP Server Interface</span></a>
|
||||
cannot disrupt processing of incoming announces for other connected clients on the same
|
||||
<a class="reference internal" href="#interfaces-tcps"><span class="std std-ref">TCP Server Interface</span></a>. All other clients on the same interface will still have new announces
|
||||
processed without interruption.</p>
|
||||
<a class="reference internal" href="#interfaces-tcps"><span class="std std-ref">TCP Server Interface</span></a>. All other clients on the same interface
|
||||
will still have new announces processed without interruption.</p>
|
||||
</div>
|
||||
<p>By default, Reticulum will handle this automatically, and ingress announce
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
generally not be neccessary to modify the ingress control configuration,
|
||||
@@ -1546,8 +1549,7 @@ but all the parameters are exposed for configuration if needed.</p>
|
||||
<div><ul>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ingress_control</span></code> option tells Reticulum whether or not
|
||||
to enable announce ingress control on the interface. Defaults to
|
||||
<code class="docutils literal notranslate"><span class="pre">True</span></code>.</div>
|
||||
to enable ingress control on the interface. Defaults to <code class="docutils literal notranslate"><span class="pre">True</span></code>.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
@@ -1602,6 +1604,76 @@ to <code class="docutils literal notranslate"><span class="pre">30</span></code>
|
||||
</li>
|
||||
</ul>
|
||||
</div></blockquote>
|
||||
<p>All of the above settings can be configured both as instance-wide defaults
|
||||
under the <code class="docutils literal notranslate"><span class="pre">[reticulum]</span></code> section of the configuration file, or on a per-
|
||||
interface basis under the relevant interface configuration section.</p>
|
||||
</section>
|
||||
<section id="path-request-burst-control">
|
||||
<h2>Path Request Burst Control<a class="headerlink" href="#path-request-burst-control" title="Link to this heading">¶</a></h2>
|
||||
<p>In addition the announce controls for newly created destination, Reticulum will also
|
||||
monitor incoming path request activity, and enforce burst controls if per-client rates
|
||||
exceed configured limits. Once path request burst control is activated on an
|
||||
interface, path requests will no longer be propagated further on the network.
|
||||
As with announce burst control, this happens on a per sub-interface basis. One
|
||||
client connecting to a public gateway will not be able to disrupt path request
|
||||
processing for other clients.</p>
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Warning</p>
|
||||
<p>Applications that send large amounts of unnecessary path requests will very
|
||||
quickly get rate limited by transport nodes, and the entire system they are
|
||||
running on will not be able to resolve any paths on the network, until the
|
||||
burst subsides and hold period expires. <strong>Do not</strong> write applications like
|
||||
this. Only request paths for destinations you need to communicate with.</p>
|
||||
</div>
|
||||
<p>By default, Reticulum will handle this automatically, and ingress path request
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
generally not be neccessary to modify the ingress control configuration,
|
||||
but all the parameters are exposed for configuration if needed.</p>
|
||||
<blockquote>
|
||||
<div><ul>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ingress_control</span></code> option tells Reticulum whether or not
|
||||
to enable ingress control on the interface. Defaults to <code class="docutils literal notranslate"><span class="pre">True</span></code>.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ic_new_time</span></code> option configures how long (in seconds) an
|
||||
interface is considered newly spawned. Defaults to <code class="docutils literal notranslate"><span class="pre">2*60*60</span></code> seconds. This
|
||||
option is useful on publicly accessible interfaces that spawn new
|
||||
sub-interfaces when a new client connects.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ic_pr_burst_freq_new</span></code> option sets the maximum path request
|
||||
ingress frequency for newly spawned interfaces. Defaults to <code class="docutils literal notranslate"><span class="pre">3</span></code>
|
||||
path requests per second.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ic_pr_burst_freq</span></code> option sets the maximum path request
|
||||
ingress frequency for other interfaces. Defaults to <code class="docutils literal notranslate"><span class="pre">8</span></code> path requests
|
||||
per second.</div>
|
||||
</div>
|
||||
<blockquote>
|
||||
<div><p><em>If an interface exceeds its burst frequency, incoming path requests
|
||||
from that system will not traverse the network further.</em></p>
|
||||
</div></blockquote>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">egress_control</span></code> option enables hard-limiting path request egress
|
||||
control per-interface. Defaults to <code class="docutils literal notranslate"><span class="pre">False</span></code></div>
|
||||
</div>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">ec_pr_freq</span></code> option sets the hard limit for outbound path requests
|
||||
per second on a given interface.</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div></blockquote>
|
||||
<p>All of the above settings can be configured both as instance-wide defaults
|
||||
under the <code class="docutils literal notranslate"><span class="pre">[reticulum]</span></code> section of the configuration file, or on a per-
|
||||
interface basis under the relevant interface configuration section.</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -1689,6 +1761,7 @@ to <code class="docutils literal notranslate"><span class="pre">30</span></code>
|
||||
<li><a class="reference internal" href="#interfaces-modes">Interface Modes</a></li>
|
||||
<li><a class="reference internal" href="#announce-rate-control">Announce Rate Control</a></li>
|
||||
<li><a class="reference internal" href="#new-destination-rate-limiting">New Destination Rate Limiting</a></li>
|
||||
<li><a class="reference internal" href="#path-request-burst-control">Path Request Burst Control</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -1700,7 +1773,7 @@ to <code class="docutils literal notranslate"><span class="pre">30</span></code>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Reticulum License - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -344,7 +344,7 @@ SOFTWARE.
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Building Networks - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -663,7 +663,7 @@ differently than a mobile device roaming between radio cells.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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.
@@ -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.4 documentation</title>
|
||||
<title>API Reference - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -2484,7 +2484,7 @@ will announce it.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<title>Search - Reticulum Network Stack 1.2.6 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?v=8dab3a3b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="#" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -303,7 +303,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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
@@ -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.4 documentation</title>
|
||||
<title>Programs Using Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -328,31 +328,11 @@ plugin system for expandability.</p>
|
||||
</section>
|
||||
<section id="meshchatx">
|
||||
<h3>MeshChatX<a class="headerlink" href="#meshchatx" title="Link to this heading">¶</a></h3>
|
||||
<p>A <a class="reference external" href="https://git.quad4.io/RNS-Things/MeshChatX">Reticulum MeshChat fork from the future</a>, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original Reticulum MeshChat project, and is not affiliated with the original project.</p>
|
||||
<p>A <a class="reference external" href="https://git.quad4.io/RNS-Things/MeshChatX">Reticulum MeshChat fork from the future</a>, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original <a class="reference external" href="https://github.com/liamcottle/reticulum-meshchat">Reticulum MeshChat</a> project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork.</p>
|
||||
<a class="reference external image-reference" href="https://git.quad4.io/RNS-Things/MeshChatX"><img alt="_images/meshchatx.webp" class="align-center" src="_images/meshchatx.webp" />
|
||||
</a>
|
||||
<p>Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security.</p>
|
||||
</section>
|
||||
<section id="meshchat">
|
||||
<h3>MeshChat<a class="headerlink" href="#meshchat" title="Link to this heading">¶</a></h3>
|
||||
<p>The <a class="reference external" href="https://github.com/liamcottle/reticulum-meshchat">Reticulum MeshChat</a> application
|
||||
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
|
||||
page browser and other interesting functionality.</p>
|
||||
<a class="reference external image-reference" href="https://github.com/liamcottle/reticulum-meshchat"><img alt="_images/meshchat_1.webp" class="align-center" src="_images/meshchat_1.webp" />
|
||||
</a>
|
||||
<p>Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or
|
||||
any other LXMF client.</p>
|
||||
</section>
|
||||
<section id="columba">
|
||||
<h3>Columba<a class="headerlink" href="#columba" title="Link to this heading">¶</a></h3>
|
||||
<p><a class="reference external" href="https://github.com/torlando-tech/columba/">Columba</a> is a simple and familiar LXMF
|
||||
messaging app Android, built with a native Android interface and Material Design 3.</p>
|
||||
<a class="reference external image-reference" href="https://github.com/torlando-tech/columba/"><img alt="_images/columba.webp" class="align-center" src="_images/columba.webp" style="width: 25%;" />
|
||||
</a>
|
||||
<p>While still in early and very active development, it is of course also compatible
|
||||
with all other LXMF clients, and allows you to message seamlessly with anyone else
|
||||
using LXMF.</p>
|
||||
</section>
|
||||
<section id="reticulum-relay-chat">
|
||||
<h3>Reticulum Relay Chat<a class="headerlink" href="#reticulum-relay-chat" title="Link to this heading">¶</a></h3>
|
||||
<p><a class="reference external" href="https://rrc.kc1awv.net/">Reticulum Relay Chat</a> is a live chat system built on top of the Reticulum Network Stack. It exists to let people talk to each other in real time over Reticulum without dragging in message databases, synchronization engines, or architectural commitments they did not ask for.</p>
|
||||
@@ -417,8 +397,8 @@ using LXMF.</p>
|
||||
<p>LXMF is efficient enough that it can deliver messages over extremely low-bandwidth systems such as packet radio or LoRa. Encrypted LXMF messages can also be encoded as QR-codes or text-based URIs, allowing completely analog paper message transport.</p>
|
||||
<p>Using Propagation Nodes, LXMF also offer a way to store and forward messages to users or endpoints that are not directly reachable at the time of message emission.</p>
|
||||
</section>
|
||||
<section id="id17">
|
||||
<h3>LXST<a class="headerlink" href="#id17" title="Link to this heading">¶</a></h3>
|
||||
<section id="id16">
|
||||
<h3>LXST<a class="headerlink" href="#id16" title="Link to this heading">¶</a></h3>
|
||||
<p><a class="reference external" href="https://github.com/markqvist/lxst">LXST</a> is a simple and flexible real-time streaming format and delivery protocol that allows a wide variety of applications, while using as little bandwidth as possible. It is built on top of Reticulum and offers zero-conf stream routing, end-to-end encryption and Forward Secrecy, and can be transported over any kind of medium that Reticulum supports. It currently powers real-time voice and telephony applications over Reticulum.</p>
|
||||
</section>
|
||||
<section id="rrc">
|
||||
@@ -502,8 +482,6 @@ using LXMF.</p>
|
||||
<li><a class="reference internal" href="#retipedia">Retipedia</a></li>
|
||||
<li><a class="reference internal" href="#sideband">Sideband</a></li>
|
||||
<li><a class="reference internal" href="#meshchatx">MeshChatX</a></li>
|
||||
<li><a class="reference internal" href="#meshchat">MeshChat</a></li>
|
||||
<li><a class="reference internal" href="#columba">Columba</a></li>
|
||||
<li><a class="reference internal" href="#reticulum-relay-chat">Reticulum Relay Chat</a></li>
|
||||
<li><a class="reference internal" href="#retibbs">RetiBBS</a></li>
|
||||
<li><a class="reference internal" href="#rbrowser">RBrowser</a></li>
|
||||
@@ -518,7 +496,7 @@ using LXMF.</p>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#protocols">Protocols</a><ul>
|
||||
<li><a class="reference internal" href="#lxmf">LXMF</a></li>
|
||||
<li><a class="reference internal" href="#id17">LXST</a></li>
|
||||
<li><a class="reference internal" href="#id16">LXST</a></li>
|
||||
<li><a class="reference internal" href="#rrc">RRC</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -534,7 +512,7 @@ using LXMF.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Support Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -382,7 +382,7 @@ circumstances, so we rely on old-fashioned human feedback.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Understanding Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -1337,7 +1337,7 @@ those risks are acceptable to you.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Using Reticulum on Your System - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -1635,7 +1635,7 @@ systemctl --user enable rnsd.service
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>What is Reticulum? - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -504,7 +504,7 @@ network, and vice versa.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -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.4 documentation</title>
|
||||
<title>Zen of Reticulum - Reticulum Network Stack 1.2.6 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?v=580074bf" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css?v=76b2166b" />
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.2.6 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -204,7 +204,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.4 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.2.6 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -676,7 +676,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=928db92d"></script>
|
||||
</div><script src="_static/documentation_options.js?v=010db75e"></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>
|
||||
|
||||
@@ -82,8 +82,6 @@ to participate in the development of Reticulum itself.
|
||||
* [Retipedia](software.md#retipedia)
|
||||
* [Sideband](software.md#sideband)
|
||||
* [MeshChatX](software.md#meshchatx)
|
||||
* [MeshChat](software.md#meshchat)
|
||||
* [Columba](software.md#columba)
|
||||
* [Reticulum Relay Chat](software.md#reticulum-relay-chat)
|
||||
* [RetiBBS](software.md#retibbs)
|
||||
* [RBrowser](software.md#rbrowser)
|
||||
@@ -96,7 +94,7 @@ to participate in the development of Reticulum itself.
|
||||
* [RNMon](software.md#rnmon)
|
||||
* [Protocols](software.md#protocols)
|
||||
* [LXMF](software.md#lxmf)
|
||||
* [LXST](software.md#id17)
|
||||
* [LXST](software.md#id16)
|
||||
* [RRC](software.md#rrc)
|
||||
* [Interface Modules & Connectivity Resources](software.md#interface-modules-connectivity-resources)
|
||||
* [Using Reticulum on Your System](using.md)
|
||||
@@ -183,6 +181,7 @@ to participate in the development of Reticulum itself.
|
||||
* [Interface Modes](interfaces.md#interfaces-modes)
|
||||
* [Announce Rate Control](interfaces.md#announce-rate-control)
|
||||
* [New Destination Rate Limiting](interfaces.md#new-destination-rate-limiting)
|
||||
* [Path Request Burst Control](interfaces.md#path-request-burst-control)
|
||||
* [Building Networks](networks.md)
|
||||
* [Concepts & Overview](networks.md#concepts-overview)
|
||||
* [Introductory Considerations](networks.md#introductory-considerations)
|
||||
|
||||
@@ -1295,11 +1295,12 @@ a large amount of bogus destinations, and then disconnect, these destination wil
|
||||
never make it into path tables and waste network bandwidth on retransmitted
|
||||
announces.
|
||||
|
||||
**It’s important to note** that the ingress control works at the level of *individual
|
||||
#### NOTE
|
||||
It’s important to remember that the ingress control works at the level of *individual
|
||||
sub-interfaces*. As an example, this means that one client on a [TCP Server Interface](#interfaces-tcps)
|
||||
cannot disrupt processing of incoming announces for other connected clients on the same
|
||||
[TCP Server Interface](#interfaces-tcps). All other clients on the same interface will still have new announces
|
||||
processed without interruption.
|
||||
[TCP Server Interface](#interfaces-tcps). All other clients on the same interface
|
||||
will still have new announces processed without interruption.
|
||||
|
||||
By default, Reticulum will handle this automatically, and ingress announce
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
@@ -1307,8 +1308,7 @@ generally not be neccessary to modify the ingress control configuration,
|
||||
but all the parameters are exposed for configuration if needed.
|
||||
|
||||
> * The `ingress_control` option tells Reticulum whether or not
|
||||
> to enable announce ingress control on the interface. Defaults to
|
||||
> `True`.
|
||||
> to enable ingress control on the interface. Defaults to `True`.
|
||||
> <br/>
|
||||
> * The `ic_new_time` option configures how long (in seconds) an
|
||||
> interface is considered newly spawned. Defaults to `2*60*60` seconds. This
|
||||
@@ -1343,4 +1343,59 @@ but all the parameters are exposed for configuration if needed.
|
||||
> * The `ic_held_release_interval` option sets how much time (in seconds)
|
||||
> must pass between releasing each held announce from the queue. Defaults
|
||||
> to `30` seconds.
|
||||
> <br/>
|
||||
> <br/>
|
||||
|
||||
All of the above settings can be configured both as instance-wide defaults
|
||||
under the `[reticulum]` section of the configuration file, or on a per-
|
||||
interface basis under the relevant interface configuration section.
|
||||
|
||||
## Path Request Burst Control
|
||||
|
||||
In addition the announce controls for newly created destination, Reticulum will also
|
||||
monitor incoming path request activity, and enforce burst controls if per-client rates
|
||||
exceed configured limits. Once path request burst control is activated on an
|
||||
interface, path requests will no longer be propagated further on the network.
|
||||
As with announce burst control, this happens on a per sub-interface basis. One
|
||||
client connecting to a public gateway will not be able to disrupt path request
|
||||
processing for other clients.
|
||||
|
||||
#### WARNING
|
||||
Applications that send large amounts of unnecessary path requests will very
|
||||
quickly get rate limited by transport nodes, and the entire system they are
|
||||
running on will not be able to resolve any paths on the network, until the
|
||||
burst subsides and hold period expires. **Do not** write applications like
|
||||
this. Only request paths for destinations you need to communicate with.
|
||||
|
||||
By default, Reticulum will handle this automatically, and ingress path request
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
generally not be neccessary to modify the ingress control configuration,
|
||||
but all the parameters are exposed for configuration if needed.
|
||||
|
||||
> * The `ingress_control` option tells Reticulum whether or not
|
||||
> to enable ingress control on the interface. Defaults to `True`.
|
||||
> <br/>
|
||||
> * The `ic_new_time` option configures how long (in seconds) an
|
||||
> interface is considered newly spawned. Defaults to `2*60*60` seconds. This
|
||||
> option is useful on publicly accessible interfaces that spawn new
|
||||
> sub-interfaces when a new client connects.
|
||||
> <br/>
|
||||
> * The `ic_pr_burst_freq_new` option sets the maximum path request
|
||||
> ingress frequency for newly spawned interfaces. Defaults to `3`
|
||||
> path requests per second.
|
||||
> <br/>
|
||||
> * The `ic_pr_burst_freq` option sets the maximum path request
|
||||
> ingress frequency for other interfaces. Defaults to `8` path requests
|
||||
> per second.
|
||||
> <br/>
|
||||
> > *If an interface exceeds its burst frequency, incoming path requests
|
||||
> > from that system will not traverse the network further.*
|
||||
> * The `egress_control` option enables hard-limiting path request egress
|
||||
> control per-interface. Defaults to `False`
|
||||
> <br/>
|
||||
> * The `ec_pr_freq` option sets the hard limit for outbound path requests
|
||||
> per second on a given interface.
|
||||
> <br/>
|
||||
|
||||
All of the above settings can be configured both as instance-wide defaults
|
||||
under the `[reticulum]` section of the configuration file, or on a per-
|
||||
interface basis under the relevant interface configuration section.
|
||||
@@ -72,28 +72,10 @@ plugin system for expandability.
|
||||
|
||||
### MeshChatX
|
||||
|
||||
A [Reticulum MeshChat fork from the future](https://git.quad4.io/RNS-Things/MeshChatX), with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original Reticulum MeshChat project, and is not affiliated with the original project.
|
||||
A [Reticulum MeshChat fork from the future](https://git.quad4.io/RNS-Things/MeshChatX), with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original [Reticulum MeshChat](https://github.com/liamcottle/reticulum-meshchat) project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork.
|
||||
|
||||
Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security.
|
||||
|
||||
### MeshChat
|
||||
|
||||
The [Reticulum MeshChat](https://github.com/liamcottle/reticulum-meshchat) application
|
||||
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
|
||||
page browser and other interesting functionality.
|
||||
|
||||
Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or
|
||||
any other LXMF client.
|
||||
|
||||
### Columba
|
||||
|
||||
[Columba](https://github.com/torlando-tech/columba/) is a simple and familiar LXMF
|
||||
messaging app Android, built with a native Android interface and Material Design 3.
|
||||
|
||||
While still in early and very active development, it is of course also compatible
|
||||
with all other LXMF clients, and allows you to message seamlessly with anyone else
|
||||
using LXMF.
|
||||
|
||||
### Reticulum Relay Chat
|
||||
|
||||
[Reticulum Relay Chat](https://rrc.kc1awv.net/) is a live chat system built on top of the Reticulum Network Stack. It exists to let people talk to each other in real time over Reticulum without dragging in message databases, synchronization engines, or architectural commitments they did not ask for.
|
||||
|
||||
@@ -402,6 +402,9 @@ Only releases with ``published`` status are visible through the Nomad Network in
|
||||
-q, --quiet
|
||||
--version show program's version number and exit
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
Work Documents
|
||||
==============
|
||||
|
||||
@@ -1372,11 +1372,12 @@ a large amount of bogus destinations, and then disconnect, these destination wil
|
||||
never make it into path tables and waste network bandwidth on retransmitted
|
||||
announces.
|
||||
|
||||
**It's important to note** that the ingress control works at the level of *individual
|
||||
sub-interfaces*. As an example, this means that one client on a :ref:`TCP Server Interface<interfaces-tcps>`
|
||||
cannot disrupt processing of incoming announces for other connected clients on the same
|
||||
:ref:`TCP Server Interface<interfaces-tcps>`. All other clients on the same interface will still have new announces
|
||||
processed without interruption.
|
||||
.. note::
|
||||
It's important to remember that the ingress control works at the level of *individual
|
||||
sub-interfaces*. As an example, this means that one client on a :ref:`TCP Server Interface<interfaces-tcps>`
|
||||
cannot disrupt processing of incoming announces for other connected clients on the same
|
||||
:ref:`TCP Server Interface<interfaces-tcps>`. All other clients on the same interface
|
||||
will still have new announces processed without interruption.
|
||||
|
||||
By default, Reticulum will handle this automatically, and ingress announce
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
@@ -1384,8 +1385,7 @@ generally not be neccessary to modify the ingress control configuration,
|
||||
but all the parameters are exposed for configuration if needed.
|
||||
|
||||
* | The ``ingress_control`` option tells Reticulum whether or not
|
||||
to enable announce ingress control on the interface. Defaults to
|
||||
``True``.
|
||||
to enable ingress control on the interface. Defaults to ``True``.
|
||||
|
||||
* | The ``ic_new_time`` option configures how long (in seconds) an
|
||||
interface is considered newly spawned. Defaults to ``2*60*60`` seconds. This
|
||||
@@ -1422,3 +1422,59 @@ but all the parameters are exposed for configuration if needed.
|
||||
must pass between releasing each held announce from the queue. Defaults
|
||||
to ``30`` seconds.
|
||||
|
||||
All of the above settings can be configured both as instance-wide defaults
|
||||
under the ``[reticulum]`` section of the configuration file, or on a per-
|
||||
interface basis under the relevant interface configuration section.
|
||||
|
||||
|
||||
Path Request Burst Control
|
||||
==========================
|
||||
|
||||
In addition the announce controls for newly created destination, Reticulum will also
|
||||
monitor incoming path request activity, and enforce burst controls if per-client rates
|
||||
exceed configured limits. Once path request burst control is activated on an
|
||||
interface, path requests will no longer be propagated further on the network.
|
||||
As with announce burst control, this happens on a per sub-interface basis. One
|
||||
client connecting to a public gateway will not be able to disrupt path request
|
||||
processing for other clients.
|
||||
|
||||
.. warning::
|
||||
Applications that send large amounts of unnecessary path requests will very
|
||||
quickly get rate limited by transport nodes, and the entire system they are
|
||||
running on will not be able to resolve any paths on the network, until the
|
||||
burst subsides and hold period expires. **Do not** write applications like
|
||||
this. Only request paths for destinations you need to communicate with.
|
||||
|
||||
By default, Reticulum will handle this automatically, and ingress path request
|
||||
control will be enabled on interface where it is sensible to do so. It should
|
||||
generally not be neccessary to modify the ingress control configuration,
|
||||
but all the parameters are exposed for configuration if needed.
|
||||
|
||||
* | The ``ingress_control`` option tells Reticulum whether or not
|
||||
to enable ingress control on the interface. Defaults to ``True``.
|
||||
|
||||
* | The ``ic_new_time`` option configures how long (in seconds) an
|
||||
interface is considered newly spawned. Defaults to ``2*60*60`` seconds. This
|
||||
option is useful on publicly accessible interfaces that spawn new
|
||||
sub-interfaces when a new client connects.
|
||||
|
||||
* | The ``ic_pr_burst_freq_new`` option sets the maximum path request
|
||||
ingress frequency for newly spawned interfaces. Defaults to ``3``
|
||||
path requests per second.
|
||||
|
||||
* | The ``ic_pr_burst_freq`` option sets the maximum path request
|
||||
ingress frequency for other interfaces. Defaults to ``8`` path requests
|
||||
per second.
|
||||
|
||||
*If an interface exceeds its burst frequency, incoming path requests
|
||||
from that system will not traverse the network further.*
|
||||
|
||||
* | The ``egress_control`` option enables hard-limiting path request egress
|
||||
control per-interface. Defaults to ``False``
|
||||
|
||||
* | The ``ec_pr_freq`` option sets the hard limit for outbound path requests
|
||||
per second on a given interface.
|
||||
|
||||
All of the above settings can be configured both as instance-wide defaults
|
||||
under the ``[reticulum]`` section of the configuration file, or on a per-
|
||||
interface basis under the relevant interface configuration section.
|
||||
@@ -110,7 +110,7 @@ plugin system for expandability.
|
||||
MeshChatX
|
||||
^^^^^^^^
|
||||
|
||||
A `Reticulum MeshChat fork from the future <https://git.quad4.io/RNS-Things/MeshChatX>`_, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original Reticulum MeshChat project, and is not affiliated with the original project.
|
||||
A `Reticulum MeshChat fork from the future <https://git.quad4.io/RNS-Things/MeshChatX>`_, with the goal of providing everything you need for Reticulum, LXMF, and LXST in one beautiful and feature-rich application. This project is separate from the original `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ project, and is not affiliated with the original project, but is a much more up-to-date, comprehensive and well-maintained fork.
|
||||
|
||||
.. only:: html
|
||||
|
||||
@@ -127,56 +127,6 @@ A `Reticulum MeshChat fork from the future <https://git.quad4.io/RNS-Things/Mesh
|
||||
|
||||
Features include full LXST support, custom voicemail, phonebook, contact sharing, and ringtone support, multi-identity handling, modern UI/UX, offline documentation, expanded tools, page archiving, integrated maps, telemetry and improved application security.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
MeshChat
|
||||
^^^^^^^^
|
||||
|
||||
The `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ application
|
||||
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
|
||||
page browser and other interesting functionality.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/meshchat_1.webp
|
||||
:align: center
|
||||
:target: https://github.com/liamcottle/reticulum-meshchat
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/meshchat_1.png
|
||||
:align: center
|
||||
:target: https://github.com/liamcottle/reticulum-meshchat
|
||||
|
||||
Reticulum MeshChat is of course also compatible with Sideband and Nomad Network, or
|
||||
any other LXMF client.
|
||||
|
||||
Columba
|
||||
^^^^^^^
|
||||
|
||||
`Columba <https://github.com/torlando-tech/columba/>`_ is a simple and familiar LXMF
|
||||
messaging app Android, built with a native Android interface and Material Design 3.
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. image:: screenshots/columba.webp
|
||||
:align: center
|
||||
:width: 25%
|
||||
:target: https://github.com/torlando-tech/columba/
|
||||
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/columba.png
|
||||
:align: center
|
||||
:width: 25%
|
||||
:target: https://github.com/torlando-tech/columba/
|
||||
|
||||
While still in early and very active development, it is of course also compatible
|
||||
with all other LXMF clients, and allows you to message seamlessly with anyone else
|
||||
using LXMF.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
@@ -31,6 +31,10 @@ Donations are gratefully accepted via the following channels:
|
||||
Are certain features in the development roadmap are important to you or your
|
||||
organisation? Make them a reality quickly by sponsoring their implementation.
|
||||
|
||||
.. raw:: latex
|
||||
|
||||
\newpage
|
||||
|
||||
Provide Feedback
|
||||
================
|
||||
Feedback on the usage, functioning and potential dysfunctioning of any and
|
||||
|
||||
Reference in New Issue
Block a user