mirror of
https://github.com/markqvist/Reticulum.git
synced 2026-07-30 03:18:10 -07:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b48b96e616 | |||
| 529a9fd460 | |||
| 64fee86ebc | |||
| e3f1a5e7cd | |||
| 0416c41956 | |||
| 4760103aa6 | |||
| b2188ce9a7 | |||
| 4631d78bea | |||
| 0d16e2305e | |||
| e5d37355b8 | |||
| 224124aac7 | |||
| e29b839429 | |||
| 12c21d4e25 | |||
| 6a761a768c | |||
| 1ecf845cea | |||
| a29a08716f | |||
| e46b012e95 | |||
| 0f33d71966 | |||
| 00d57a22ee | |||
| c25b56db9a | |||
| a4b6129875 | |||
| bce5f859da | |||
| 93526c17f5 | |||
| 6d4523c27e | |||
| c4297d318f | |||
| 90e1dbb912 | |||
| cbf50460c2 | |||
| 889c5ff55f | |||
| 566aa68fc0 | |||
| 3ca71527d9 | |||
| 5577e781fb | |||
| 7611fca6bf | |||
| c10c465c02 | |||
| 9bc73819f6 | |||
| bebf211b01 | |||
| 1af173e8a5 | |||
| b051e76d00 | |||
| 4838875691 | |||
| 122f17fad6 | |||
| be36abd857 | |||
| fa4d4c6716 | |||
| a5728be421 | |||
| 032b1aa3b2 | |||
| fb7479a6f6 | |||
| f81b267509 | |||
| 6527423526 | |||
| d8bc20d4cb | |||
| 6c6238ce29 | |||
| cbba3502b7 | |||
| 2b79db03c0 | |||
| 2c9edc43f1 | |||
| ef9244f79f | |||
| 2d811dc398 | |||
| e64d815033 | |||
| bfade97051 | |||
| 3c4ef622cc | |||
| 3898d63689 | |||
| ec8d43a548 | |||
| 5081255317 | |||
| 13a538168f | |||
| a5ed0a4321 | |||
| d93f9798ae | |||
| 88833f170b | |||
| cd6911ed53 | |||
| b95c51b96e | |||
| 3a36c367fe | |||
| cf6010da59 | |||
| bb2897445f | |||
| 406b141370 | |||
| 00b2f81a82 | |||
| 9adf045a31 | |||
| de0f399a16 | |||
| dca2a92829 | |||
| b1b3ff7143 | |||
| a0f0f31807 | |||
| 72db6e0ef2 | |||
| b70688883a | |||
| dd3ddb9d8a | |||
| 4ba1750c01 | |||
| eacff56fe2 | |||
| 8768e163ae | |||
| c0bc19d998 | |||
| c99ec922aa | |||
| 0a6f76e41b | |||
| 156ce9cd34 | |||
| 176092ebf9 | |||
| 98817e3f9d | |||
| 276b7bb92f | |||
| d1813e1ff0 | |||
| 5355a9bb20 | |||
| 48d17a8616 | |||
| 7e603f9606 | |||
| ac6f0199f5 | |||
| 71f6f721fd | |||
| 51b7dc7cd4 | |||
| 03ddb08248 | |||
| 422dc05549 | |||
| 50c0a354c9 | |||
| e98487c1cf | |||
| 50e03a24e8 | |||
| 0cc42568c7 | |||
| 41790ca707 | |||
| 20b1bfd01e | |||
| 1be5e10abe | |||
| 13a9ebed83 | |||
| aee8c3be0c | |||
| ef8ccf67ed | |||
| 91d531b053 | |||
| 0de169836e | |||
| a2ef978208 | |||
| 5b3bb050e7 | |||
| 9b9efe5fac | |||
| 482d5ac4cb | |||
| 804a7ddbe1 | |||
| 739fdd387e |
@@ -14,3 +14,4 @@ tests/rnsconfig/logfile*
|
||||
*.data
|
||||
*.result
|
||||
.buildinfo.bak
|
||||
docs/Reticulum Manual.*
|
||||
|
||||
+126
-116
@@ -1,14 +1,10 @@
|
||||
### 2026-05-28: RNS 1.3.2
|
||||
### 2026-07-26: RNS 1.4.2
|
||||
|
||||
This release adds commit signing and validation support to the `rngit` system, as well as improvements to the blackhole functionality.
|
||||
This maintenance release fixes bugs in blackholed identity filtering and recursive path request handling for RNode interfaces.
|
||||
|
||||
**Changes**
|
||||
- Extended blackhole functionality to immediately terminate links from blackholed identities
|
||||
- Added commit signing and validation to `rngit`
|
||||
- Added commit hash inclusion in generated release manifest to `rngit`
|
||||
- Added local `verify` operation shorthand to `rngit release`
|
||||
- Added option to configure blackhole update interval
|
||||
- Added configuration option to log without timestamps
|
||||
- Fixed a bug where recursive path requests would cause a division by zero error on uninitialized RNode interfaces.
|
||||
- Fixed slow blackholed filtering for discovered interfaces on Android in some cases.
|
||||
|
||||
**Verified Retrieval**
|
||||
You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To retrieve only the installation `.whl` package, and the release manifest for future updates, you can use:
|
||||
@@ -38,6 +34,128 @@ rnid -i bc7291552be7a58f361522990465165c -V rns_*.rsm *.rsg
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
### 2026-07-24: RNS 1.4.1
|
||||
|
||||
This release significantly improves path convergence and stability with the new path re-balancing functionality. It also introduces interface gravity configuration, new API functionality and fixes a range of bugs and inefficiencies.
|
||||
|
||||
- Added dynamic path re-balancing
|
||||
- Added `set_max_request_size` to `Destination` API
|
||||
- Added `max_response_size` to request API
|
||||
- Added interface gravity handling
|
||||
- Added `autoconnect_interface_mode` option
|
||||
- Added `autoconnect_announces_to_internal` option
|
||||
- Added `autoconnect_interface_gravity` option
|
||||
- Added `default_gravity` option
|
||||
- Added `announces_to_internal` interface option
|
||||
- Added `gravity` interface option
|
||||
- Added gravity display and sorting option to `rnstatus`
|
||||
- Added boundary -> boundary and boundary -> gateway path requests
|
||||
- Fixed `I2PInterface` tasks getting garbage-collected prematurely, thanks to **welo**
|
||||
- Fixed various minor bugs in `I2PInterface`
|
||||
- Fixed ingress control burst active flag deadlocking until new announces arrived under burst timing patterns
|
||||
- Fixed various memory inefficiencies
|
||||
- Fixed loglevel `LOG_EXTREME` not being usable in some cases
|
||||
- Fixed historical interface discoveries not being cleaned according to blackholed identities
|
||||
### 2026-07-20: RNS 1.4.0
|
||||
|
||||
This release focuses on performance, stability and reliability improvements, particularly with resource-constrained systems in mind. It also fixes several bugs, reduces logging noise, and makes information about blocked `BackboneInterface` clients available in `ifstats`.
|
||||
|
||||
**Changes**
|
||||
- Optimized transport data persistence to avoid CPU spikes on low-powered systems
|
||||
- Optimized interface hashes calculation and access to reduce overall processing load
|
||||
- Implemented known destinations cleaning as background priority to avoid lock contention and reduce load on CPU-constrained systems
|
||||
- Deprecated known destination on-disk recombination on background data persist to alleviate lock contention on CPU-constrained systems
|
||||
- Implemented caching of invalid discovery stamps to reduce CPU load on low-powered systems
|
||||
- Implemented valid discovery announce caching and sequential validation lock to reduce processing load on CPU-constrained systems
|
||||
- Fixed link stale teardown erroneously occurring due to missing keepalive from initiator side when destination continously sends packets but initiator stays silent
|
||||
- Fixed race condition in link watchdog timing
|
||||
- Fixed missing None-check in `BackboneInterface` fast-flap detection
|
||||
- Fixed invalid exception log handler in `BackboneInterface`
|
||||
- Increased default discovery stamp value to 16
|
||||
- Added blocked IPs list to ifstats
|
||||
- Improved backbone interface logging
|
||||
- Reduced overall logging noise
|
||||
|
||||
### 2026-07-19: RNS 1.3.9
|
||||
|
||||
**Critical Security Update**: This release fixes a severe security flaw in `rnsh`. Due to the nature of the issue, I will not disclose any further details for the time being. Once operators have had time to update, I will provide a full report for transparency. If you use `rnsh`, update **right now**.
|
||||
|
||||
**Important**: The new version of `rnsh` changes default identity file locations, and these will now be sourced from `~/.rnsh/identity` (initiator) and `~/.rnsh/identity.default` (listener). Make sure you copy your old files to this directory, or specify a custom identity path using the command line arguments. The `--config` argument has also been renamed to `--rnsconfig`, and the `--config` argument will now specify the `rnsh` configuration directory instead of the RNS configuration directory, bringing the behavior into alignment with other RNS utilities.
|
||||
|
||||
Additionally, this release includes automated blocking of fast-flapping clients on `BackboneInterface` listeners (see the Interfaces chapter of the manual for details), and a number of improvements to resource handling. The logging system has also been improved, and path and destination information moved to a new `LOG_PATHING` loglevel, to decrease log noise.
|
||||
|
||||
**Changes**
|
||||
- Fixed a critical security issue in `rnsh`
|
||||
- Added automated blocking of fast-flapping clients to `BackboneInterface`
|
||||
- Added new `LOG_PATHING` loglevel, improved logging
|
||||
- Added ability to make internal-mode interfaces discoverable
|
||||
- Added ability to get discoverable interface location from external script
|
||||
- Added `RESOURCE_RCL` signal on resource receiver cancel
|
||||
- Improved resource handling and reliability
|
||||
- Updated `rnsh` config args to work similarly to other RNS utilities
|
||||
|
||||
### 2026-07-10: RNS 1.3.8
|
||||
|
||||
This release fixes various inconsistencies in link and hop-count related APIs.
|
||||
|
||||
**Changes**
|
||||
- Fixed inconsistent link traffic stats calculation
|
||||
- Fixed link hop-count metric only being available on initiator side
|
||||
- Fixed potential hop-count serialization error on transport
|
||||
- Updated `WeaveInterface` to support latest Weave firmware
|
||||
|
||||
### 2026-07-03: RNS 1.3.7
|
||||
|
||||
This maintenance release improves announces propagation logic, and adds additional options for configuring announce propagation and interface behavior in transport mode.
|
||||
|
||||
**Changes**
|
||||
- Added `internal` interface mode
|
||||
- Added `recursive_prs` interface option
|
||||
- Added `announces_from_internal` interface option
|
||||
- Added new options to the manual
|
||||
- Improved and cleaned up announce propagation logic
|
||||
|
||||
### 2026-07-03: RNS 1.3.6
|
||||
|
||||
This release contained a bug in the local instance transport handling, and was superseded by version `1.3.7`.
|
||||
|
||||
### 2026-06-01: RNS 1.3.5
|
||||
|
||||
This maintenance release contains an important fix for `AutoInterface` reliability when roaming between different physical networks.
|
||||
|
||||
**Changes**
|
||||
- Fixed UDP listener replacement deadlocking inbound AutoInterface traffic when fast-roaming between physical interfaces or WiFi APs
|
||||
- Fixed some paths never resolving when using other interfaces at the same time as a deadlocked AutoInterface
|
||||
|
||||
### 2026-05-29: RNS 1.3.4
|
||||
|
||||
This release fixes a regression that could cause sub-optimal path selection under conditions where the same announce was received within a very short timespan on different interfaces, as well as a few other bugs and inefficiencies.
|
||||
|
||||
**Changes**
|
||||
- Fixed regression in inbound announce de-duplication
|
||||
- Fixed missing check for shared instance type configuration conflict
|
||||
- Fixed superfluous path state configuration on new announce from new destinations
|
||||
- Improved cleanup of stale known destinations
|
||||
- Improved shared instance RPC handling
|
||||
|
||||
### 2026-05-28: RNS 1.3.3
|
||||
|
||||
This release fixes a regression in persistence of known destination on Windows.
|
||||
|
||||
**Changes**
|
||||
- Fixed regression in known destinations persist on Windows
|
||||
|
||||
### 2026-05-28: RNS 1.3.2
|
||||
|
||||
This release adds commit signing and validation support to the `rngit` system, as well as improvements to the blackhole functionality.
|
||||
|
||||
**Changes**
|
||||
- Extended blackhole functionality to immediately terminate links from blackholed identities
|
||||
- Added commit signing and validation to `rngit`
|
||||
- Added commit hash inclusion in generated release manifest to `rngit`
|
||||
- Added local `verify` operation shorthand to `rngit release`
|
||||
- Added option to configure blackhole update interval
|
||||
- Added configuration option to log without timestamps
|
||||
|
||||
### 2026-05-22: RNS 1.3.1
|
||||
|
||||
@@ -46,34 +164,6 @@ This maintenance release fixes a single bug.
|
||||
**Changes**
|
||||
- Fixed regression in request response transfer size accumulator
|
||||
|
||||
**Verified Retrieval**
|
||||
You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To download all artifacts, and the release manifest for future updates, you can use the following command:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:all --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
To retrieve only the `.whl` package for installation, and the release manifest, you can use:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch "latest:rns-*-py3-none-any.whl" --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rngit` or `rnid`. To perform an offline verification of all release artifacts using a manifest:
|
||||
|
||||
```sh
|
||||
rngit release rns_*.rsm fetch latest:all --offline --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
To verify releases using individual `rsg` files, while also verifying the manifest itself, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V rns_*.rsm *.rsg
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
### 2026-05-21: RNS 1.3.0
|
||||
|
||||
This maintenance release fixes a number of bugs.
|
||||
@@ -88,28 +178,6 @@ This maintenance release fixes a number of bugs.
|
||||
- Adjusted timeouts for remote operations in `rngit`
|
||||
- Updated documentation
|
||||
|
||||
**Verified Retrieval**
|
||||
You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To download all artifacts, and the release manifest for future updates, you can use the following command:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:all --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
To retrieve only the `.whl` package for installation, you can use:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:rns-1.3.0-py3-none-any.whl --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V manifest.rsm *.rsg
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
### 2026-05-19: RNS 1.2.9
|
||||
|
||||
This release completes the operational functionality of the `rngit` system, which now has full release creation, fetch and verified update support using the `rngit release` command. Additionally, two chapters have been added to the manual should cover all the things that `rngit` is currently capable of.
|
||||
@@ -125,28 +193,6 @@ This release completes the operational functionality of the `rngit` system, whic
|
||||
- Improved page node ref link handling in `rngit`
|
||||
- Improved logging in various locations
|
||||
|
||||
**Verified Retrieval**
|
||||
You can retrieve and verify this release over Reticulum using the built-in `rngit release` utility. To download all artifacts, and the release manifest for future updates, you can use the following command:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:all --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
To retrieve only the `.whl` package for installation, you can use:
|
||||
|
||||
```sh
|
||||
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum fetch latest:rns-1.2.8-py3-none-any.whl --signer bc7291552be7a58f361522990465165c
|
||||
```
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V manifest.rsm *.rsg
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
### 2026-05-18: RNS 1.2.8
|
||||
|
||||
This release improves the `rngit` system with signed release manifest generation and automatic artifact signing. It also includes several additions to `rnid` and various minor fixes and improvements to the `rngit` system.
|
||||
@@ -167,15 +213,6 @@ This release improves the `rngit` system with signed release manifest generation
|
||||
- Fixed various minor bugs and inconsistencies in `rngit`
|
||||
- Dropped `note` metadata field requirement from `rsg` structure
|
||||
|
||||
**Release Signatures**
|
||||
Release artifacts include a signed `rsm` release manifest and `rsg` signature files that can be validated against the RNS release signing identity `<bc7291552be7a58f361522990465165c>` using `rnid`. To verify files, download the `rsm` and `rsg` signatures, make sure they are in the same folder as the release artifact, and run `rnid` signature verification with the release identity as the required signer:
|
||||
|
||||
```sh
|
||||
rnid -i bc7291552be7a58f361522990465165c -V manifest.rsm *.rsg
|
||||
```
|
||||
|
||||
The `rnid` utility will then verify the signatures, and display whether they are valid. If the signature cannot be verified, the release has been tampered with and should be discarded.
|
||||
|
||||
### 2026-05-17: RNS 1.2.7
|
||||
|
||||
This release significantly improves the `rngit` system with fork, mirroring and empty repository creation functionality, a new work document proposals feature, improvements to the transport core reliability and efficiency and various other tweaks and improvements.
|
||||
@@ -193,15 +230,6 @@ This release significantly improves the `rngit` system with fork, mirroring and
|
||||
- Improved transfer completed feedback in `rncp`, thanks to **neutral**
|
||||
- Improved interface transport insertion and removal
|
||||
|
||||
**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-14: RNS 1.2.6
|
||||
|
||||
This release adds further improvements to the `rnid` and `rngit` utilities, and includes several bugfixes and other improvements.
|
||||
@@ -223,15 +251,6 @@ This release adds further improvements to the `rnid` and `rngit` utilities, and
|
||||
- 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.
|
||||
@@ -260,15 +279,6 @@ For all node ops out there, I'd recomment updating to this at some sort of semi-
|
||||
- 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!
|
||||
|
||||
@@ -57,7 +57,7 @@ manual:
|
||||
make -C docs latexpdf epub
|
||||
|
||||
distcollect:
|
||||
cp docs/Reticulum\ Manual.* dist
|
||||
mv docs/Reticulum\ Manual.* dist
|
||||
|
||||
build_spkg: remove_symlinks build_sdist create_symlinks
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ userland, and can run on practically any system that runs Python 3.
|
||||
## Read The Manual
|
||||
The full documentation for Reticulum is available at [markqvist.github.io/Reticulum/manual/](https://markqvist.github.io/Reticulum/manual/).
|
||||
|
||||
You can also download the [Reticulum manual as a PDF](https://github.com/markqvist/Reticulum/raw/master/docs/Reticulum%20Manual.pdf) or [as an e-book in EPUB format](https://github.com/markqvist/Reticulum/raw/master/docs/Reticulum%20Manual.epub).
|
||||
You can also download the [Reticulum manual as a PDF](https://github.com/markqvist/Reticulum/releases/latest/download/Reticulum.Manual.pdf) or [as an e-book in EPUB format](https://github.com/markqvist/Reticulum/releases/latest/download/Reticulum.Manual.epub).
|
||||
|
||||
For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ section of the wiki](https://github.com/markqvist/Reticulum/wiki/Frequently-Asked-Questions).
|
||||
|
||||
@@ -85,6 +85,12 @@ The Reticulum Protocol is defined entirely and authoritatively by this reference
|
||||
implementation, and its associated manual. It is maintained by Mark Qvist,
|
||||
identified by the Reticulum Identity `<bc7291552be7a58f361522990465165c>`.
|
||||
|
||||
No second-order protocol specification derivatives currently do, or will ever exist. You can't get a better specification than one that is executable, functional, testable, deployed and operating 24/7 across the planet.
|
||||
|
||||
There is not, and never will be a separate, derived "formal spec", "RFC" or anything similar for Reticulum:
|
||||
|
||||
**The reference implementation *is* the final, authoritative specification.**
|
||||
|
||||
Compatibility with the Reticulum Protocol is defined as having full interoperability,
|
||||
and sufficient functional parity with this reference implementation. Any specific protocol
|
||||
implementation that achieves this is Reticulum. Any that does not is not Reticulum.
|
||||
|
||||
@@ -16,7 +16,7 @@ No kernel modules or drivers are required. Reticulum runs completely in userland
|
||||
|
||||
The full documentation for Reticulum is available on `_`!`[this node`:/page/blob.mu`g=reticulum|r=reticulum|ref=HEAD|path=docs/markdown/index.md]`!`_.
|
||||
|
||||
You can also download the `_`!`[Reticulum manual as a PDF`:/file/download`g=reticulum|r=reticulum|ref=HEAD|path=docs%2FReticulum+Manual.pdf]`!`_ or `_`!`[as an e-book in EPUB format`:/file/download`g=reticulum|r=reticulum|ref=HEAD|path=docs%2FReticulum+Manual.pdf]`!`_.
|
||||
You can also download the `_`!`[Reticulum manual as a PDF`:/file/artifact`g=reticulum|r=reticulum|t=latest|a=Reticulum+Manual.pdf]`!`_ or `_`!`[as an e-book in EPUB format`:/file/artifact`g=reticulum|r=reticulum|t=latest|a=Reticulum+Manual.epub]`!`_.
|
||||
|
||||
>> Notable Features
|
||||
|
||||
@@ -58,6 +58,12 @@ You can also download the `_`!`[Reticulum manual as a PDF`:/file/download`g=reti
|
||||
|
||||
The Python code in this repository is the Reference Implementation of Reticulum. The Reticulum Protocol is defined entirely and authoritatively by this reference implementation, and its associated manual. It is maintained by Mark Qvist, identified by the Reticulum Identity `B333<bc7291552be7a58f361522990465165c>`b.
|
||||
|
||||
No second-order protocol specification derivatives currently do, or will ever exist. You can't get a better specification than one that is executable, functional, testable, deployed and operating 24/7 across the planet.
|
||||
|
||||
There is not, and never will be a separate, derived "formal spec", "RFC" or anything similar for Reticulum:
|
||||
|
||||
`!The reference implementation `*is`* the final, authoritative specification.`!
|
||||
|
||||
Compatibility with the Reticulum Protocol is defined as having full interoperability, and sufficient functional parity with this reference implementation. Any specific protocol implementation that achieves this is Reticulum. Any that does not is not Reticulum.
|
||||
|
||||
The reference implementation is licensed under the Reticulum License.
|
||||
|
||||
+74
-205
@@ -45,63 +45,6 @@ TPacket = TypeVar("TPacket")
|
||||
class SystemMessageTypes(enum.IntEnum):
|
||||
SMT_STREAM_DATA = 0xff00
|
||||
|
||||
class ChannelOutletBase(ABC, Generic[TPacket]):
|
||||
"""
|
||||
An abstract transport layer interface used by Channel.
|
||||
|
||||
DEPRECATED: This was created for testing; eventually
|
||||
Channel will use Link or a LinkBase interface
|
||||
directly.
|
||||
"""
|
||||
@abstractmethod
|
||||
def send(self, raw: bytes) -> TPacket:
|
||||
raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def resend(self, packet: TPacket) -> TPacket:
|
||||
raise NotImplemented()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def mdu(self):
|
||||
raise NotImplemented()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def rtt(self):
|
||||
raise NotImplemented()
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def is_usable(self):
|
||||
raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def get_packet_state(self, packet: TPacket) -> MessageState:
|
||||
raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def timed_out(self):
|
||||
raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def set_packet_timeout_callback(self, packet: TPacket, callback: Callable[[TPacket], None] | None,
|
||||
timeout: float | None = None):
|
||||
raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def set_packet_delivered_callback(self, packet: TPacket, callback: Callable[[TPacket], None] | None):
|
||||
raise NotImplemented()
|
||||
|
||||
@abstractmethod
|
||||
def get_packet_id(self, packet: TPacket) -> any:
|
||||
raise NotImplemented()
|
||||
|
||||
|
||||
class CEType(enum.IntEnum):
|
||||
"""
|
||||
ChannelException type codes
|
||||
@@ -113,7 +56,6 @@ class CEType(enum.IntEnum):
|
||||
ME_ALREADY_SENT = 4
|
||||
ME_TOO_BIG = 5
|
||||
|
||||
|
||||
class ChannelException(Exception):
|
||||
"""
|
||||
An exception thrown by Channel, with a type code.
|
||||
@@ -122,7 +64,6 @@ class ChannelException(Exception):
|
||||
super().__init__(args)
|
||||
self.type = ce_type
|
||||
|
||||
|
||||
class MessageState(enum.IntEnum):
|
||||
"""
|
||||
Set of possible states for a Message
|
||||
@@ -132,7 +73,6 @@ class MessageState(enum.IntEnum):
|
||||
MSGSTATE_DELIVERED = 2
|
||||
MSGSTATE_FAILED = 3
|
||||
|
||||
|
||||
class MessageBase(abc.ABC):
|
||||
"""
|
||||
Base type for any messages sent or received on a Channel.
|
||||
@@ -170,7 +110,6 @@ class MessageBase(abc.ABC):
|
||||
|
||||
MessageCallbackType = NewType("MessageCallbackType", Callable[[MessageBase], bool])
|
||||
|
||||
|
||||
class Envelope:
|
||||
"""
|
||||
Internal wrapper used to transport messages over a channel and
|
||||
@@ -180,8 +119,7 @@ class Envelope:
|
||||
msgtype, self.sequence, length = struct.unpack(">HHH", self.raw[:6])
|
||||
raw = self.raw[6:]
|
||||
ctor = message_factories.get(msgtype, None)
|
||||
if ctor is None:
|
||||
raise ChannelException(CEType.ME_NOT_REGISTERED, f"Unable to find constructor for Channel MSGTYPE {hex(msgtype)}")
|
||||
if ctor is None: raise ChannelException(CEType.ME_NOT_REGISTERED, f"Unable to find constructor for Channel MSGTYPE {hex(msgtype)}")
|
||||
message = ctor()
|
||||
message.unpack(raw)
|
||||
self.unpacked = True
|
||||
@@ -190,14 +128,13 @@ class Envelope:
|
||||
return message
|
||||
|
||||
def pack(self) -> bytes:
|
||||
if self.message.__class__.MSGTYPE is None:
|
||||
raise ChannelException(CEType.ME_NO_MSG_TYPE, f"{self.message.__class__} lacks MSGTYPE")
|
||||
if self.message.__class__.MSGTYPE is None: raise ChannelException(CEType.ME_NO_MSG_TYPE, f"{self.message.__class__} lacks MSGTYPE")
|
||||
data = self.message.pack()
|
||||
self.raw = struct.pack(">HHH", self.message.MSGTYPE, self.sequence, len(data)) + data
|
||||
self.packed = True
|
||||
return self.raw
|
||||
|
||||
def __init__(self, outlet: ChannelOutletBase, message: MessageBase = None, raw: bytes = None, sequence: int = None):
|
||||
def __init__(self, outlet: LinkChannelOutlet, message: MessageBase = None, raw: bytes = None, sequence: int = None):
|
||||
self.ts = time.time()
|
||||
self.id = id(self)
|
||||
self.message = message
|
||||
@@ -278,7 +215,7 @@ class Channel(contextlib.AbstractContextManager):
|
||||
SEQ_MAX = 0xFFFF
|
||||
SEQ_MODULUS = SEQ_MAX+1
|
||||
|
||||
def __init__(self, outlet: ChannelOutletBase):
|
||||
def __init__(self, outlet: LinkChannelOutlet):
|
||||
"""
|
||||
|
||||
@param outlet:
|
||||
@@ -307,11 +244,9 @@ class Channel(contextlib.AbstractContextManager):
|
||||
self.window_min = Channel.WINDOW_MIN
|
||||
self.window_flexibility = Channel.WINDOW_FLEXIBILITY
|
||||
|
||||
def __enter__(self) -> Channel:
|
||||
return self
|
||||
def __enter__(self) -> Channel: return self
|
||||
|
||||
def __exit__(self, __exc_type: Type[BaseException] | None, __exc_value: BaseException | None,
|
||||
__traceback: TracebackType | None) -> bool | None:
|
||||
def __exit__(self, __exc_type: Type[BaseException] | None, __exc_value: BaseException | None, __traceback: TracebackType | None) -> bool | None:
|
||||
self._shutdown()
|
||||
return False
|
||||
|
||||
@@ -327,20 +262,13 @@ class Channel(contextlib.AbstractContextManager):
|
||||
|
||||
def _register_message_type(self, message_class: Type[MessageBase], *, is_system_type: bool = False):
|
||||
with self._lock:
|
||||
if not issubclass(message_class, MessageBase):
|
||||
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
|
||||
f"{message_class} is not a subclass of {MessageBase}.")
|
||||
if message_class.MSGTYPE is None:
|
||||
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
|
||||
f"{message_class} has invalid MSGTYPE class attribute.")
|
||||
if message_class.MSGTYPE >= 0xf000 and not is_system_type:
|
||||
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
|
||||
f"{message_class} has system-reserved message type.")
|
||||
try:
|
||||
message_class()
|
||||
if not issubclass(message_class, MessageBase): raise ChannelException(CEType.ME_INVALID_MSG_TYPE, f"{message_class} is not a subclass of {MessageBase}.")
|
||||
if message_class.MSGTYPE is None: raise ChannelException(CEType.ME_INVALID_MSG_TYPE, f"{message_class} has invalid MSGTYPE class attribute.")
|
||||
if message_class.MSGTYPE >= 0xf000 and not is_system_type: raise ChannelException(CEType.ME_INVALID_MSG_TYPE, f"{message_class} has system-reserved message type.")
|
||||
|
||||
try: message_class()
|
||||
except Exception as ex:
|
||||
raise ChannelException(CEType.ME_INVALID_MSG_TYPE,
|
||||
f"{message_class} raised an exception when constructed with no arguments: {ex}")
|
||||
raise ChannelException(CEType.ME_INVALID_MSG_TYPE, f"{message_class} raised an exception when constructed with no arguments: {ex}")
|
||||
|
||||
self._message_factories[message_class.MSGTYPE] = message_class
|
||||
|
||||
@@ -384,8 +312,10 @@ class Channel(contextlib.AbstractContextManager):
|
||||
self._outlet.set_packet_timeout_callback(envelope.packet, None)
|
||||
self._outlet.set_packet_delivered_callback(envelope.packet, None)
|
||||
envelope.tracked = False
|
||||
|
||||
for envelope in self._rx_ring:
|
||||
envelope.tracked = False
|
||||
|
||||
self._tx_ring.clear()
|
||||
self._rx_ring.clear()
|
||||
|
||||
@@ -394,14 +324,12 @@ class Channel(contextlib.AbstractContextManager):
|
||||
i = 0
|
||||
|
||||
for existing in ring:
|
||||
|
||||
if envelope.sequence == existing.sequence:
|
||||
RNS.log(f"Envelope: Emplacement of duplicate envelope with sequence "+str(envelope.sequence), RNS.LOG_EXTREME)
|
||||
RNS.log(f"Envelope: Emplacement of duplicate envelope with sequence "+str(envelope.sequence), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
return False
|
||||
|
||||
if envelope.sequence < existing.sequence and not (self._next_rx_sequence - envelope.sequence) > (Channel.SEQ_MAX//2):
|
||||
ring.insert(i, envelope)
|
||||
|
||||
envelope.tracked = True
|
||||
return True
|
||||
|
||||
@@ -417,10 +345,8 @@ class Channel(contextlib.AbstractContextManager):
|
||||
|
||||
for cb in cbs:
|
||||
try:
|
||||
if cb(message):
|
||||
return
|
||||
except Exception as e:
|
||||
RNS.log("Channel "+str(self)+" experienced an error while running a message callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
if cb(message): return
|
||||
except Exception as e: RNS.log(f"Channel {str(self)} experienced an error while running a message callback. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def _receive(self, raw: bytes):
|
||||
try:
|
||||
@@ -432,16 +358,20 @@ class Channel(contextlib.AbstractContextManager):
|
||||
window_overflow = (self._next_rx_sequence+Channel.WINDOW_MAX) % Channel.SEQ_MODULUS
|
||||
if window_overflow < self._next_rx_sequence:
|
||||
if envelope.sequence > window_overflow:
|
||||
RNS.log("Invalid packet sequence ("+str(envelope.sequence)+") received on channel "+str(self), RNS.LOG_EXTREME)
|
||||
RNS.log(f"Invalid packet sequence ({envelope.sequence}) received on channel {self}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return
|
||||
else:
|
||||
RNS.log("Invalid packet sequence ("+str(envelope.sequence)+") received on channel "+str(self), RNS.LOG_EXTREME)
|
||||
RNS.log(f"Invalid packet sequence ({envelope.sequence}) received on channel {self}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return
|
||||
|
||||
elif envelope.sequence > self._next_rx_sequence + self.WINDOW_MAX:
|
||||
RNS.log(f"Invalid packet sequence ({envelope.sequence}) received on channel {self}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return
|
||||
|
||||
is_new = self._emplace_envelope(envelope, self._rx_ring)
|
||||
|
||||
if not is_new:
|
||||
RNS.log("Duplicate message received on channel "+str(self), RNS.LOG_EXTREME)
|
||||
RNS.log("Duplicate message received on channel "+str(self), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
return
|
||||
else:
|
||||
with self._lock:
|
||||
@@ -457,10 +387,8 @@ class Channel(contextlib.AbstractContextManager):
|
||||
self._next_rx_sequence = (self._next_rx_sequence + 1) % Channel.SEQ_MODULUS
|
||||
|
||||
for e in contigous:
|
||||
if not e.unpacked:
|
||||
m = e.unpack(self._message_factories)
|
||||
else:
|
||||
m = e.message
|
||||
if not e.unpacked: m = e.unpack(self._message_factories)
|
||||
else: m = e.message
|
||||
|
||||
self._rx_ring.remove(e)
|
||||
self._run_callbacks(m)
|
||||
@@ -474,9 +402,7 @@ class Channel(contextlib.AbstractContextManager):
|
||||
|
||||
:return: True if ready
|
||||
"""
|
||||
if not self._outlet.is_usable:
|
||||
return False
|
||||
|
||||
if not self._outlet.is_usable: return False
|
||||
with self._lock:
|
||||
outstanding = 0
|
||||
for envelope in self._tx_ring:
|
||||
@@ -484,59 +410,38 @@ class Channel(contextlib.AbstractContextManager):
|
||||
if not envelope.packet or not self._outlet.get_packet_state(envelope.packet) == MessageState.MSGSTATE_DELIVERED:
|
||||
outstanding += 1
|
||||
|
||||
if outstanding >= self.window:
|
||||
return False
|
||||
if outstanding >= self.window: return False
|
||||
|
||||
return True
|
||||
|
||||
def _packet_tx_op(self, packet: TPacket, op: Callable[[TPacket], bool]):
|
||||
target_id = self._outlet.get_packet_id(packet)
|
||||
with self._lock:
|
||||
envelope = next(filter(lambda e: e.packet is not None
|
||||
and self._outlet.get_packet_id(e.packet) == target_id,
|
||||
self._tx_ring), None)
|
||||
|
||||
envelope = next(filter(lambda e: e.packet is not None and self._outlet.get_packet_id(e.packet) == target_id, self._tx_ring), None)
|
||||
if envelope and op(envelope):
|
||||
envelope.tracked = False
|
||||
if envelope in self._tx_ring:
|
||||
self._tx_ring.remove(envelope)
|
||||
|
||||
if self.window < self.window_max:
|
||||
self.window += 1
|
||||
|
||||
# TODO: Remove at some point
|
||||
# RNS.log("Increased "+str(self)+" window to "+str(self.window), RNS.LOG_DEBUG)
|
||||
if self.window < self.window_max: self.window += 1
|
||||
|
||||
if self._outlet.rtt != 0:
|
||||
if self._outlet.rtt > Channel.RTT_FAST:
|
||||
self.fast_rate_rounds = 0
|
||||
|
||||
if self._outlet.rtt > Channel.RTT_MEDIUM:
|
||||
self.medium_rate_rounds = 0
|
||||
|
||||
if self._outlet.rtt > Channel.RTT_MEDIUM: self.medium_rate_rounds = 0
|
||||
else:
|
||||
self.medium_rate_rounds += 1
|
||||
if self.window_max < Channel.WINDOW_MAX_MEDIUM and self.medium_rate_rounds == Channel.FAST_RATE_THRESHOLD:
|
||||
self.window_max = Channel.WINDOW_MAX_MEDIUM
|
||||
self.window_min = Channel.WINDOW_MIN_LIMIT_MEDIUM
|
||||
# TODO: Remove at some point
|
||||
# RNS.log("Increased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
|
||||
# RNS.log("Increased "+str(self)+" min window to "+str(self.window_min), RNS.LOG_DEBUG)
|
||||
|
||||
else:
|
||||
self.fast_rate_rounds += 1
|
||||
if self.window_max < Channel.WINDOW_MAX_FAST and self.fast_rate_rounds == Channel.FAST_RATE_THRESHOLD:
|
||||
self.window_max = Channel.WINDOW_MAX_FAST
|
||||
self.window_min = Channel.WINDOW_MIN_LIMIT_FAST
|
||||
# TODO: Remove at some point
|
||||
# RNS.log("Increased "+str(self)+" max window to "+str(self.window_max), RNS.LOG_DEBUG)
|
||||
# RNS.log("Increased "+str(self)+" min window to "+str(self.window_min), RNS.LOG_DEBUG)
|
||||
|
||||
|
||||
else:
|
||||
RNS.log("Envelope not found in TX ring for "+str(self), RNS.LOG_EXTREME)
|
||||
if not envelope:
|
||||
RNS.log("Spurious message received on "+str(self), RNS.LOG_EXTREME)
|
||||
else: RNS.log("Envelope not found in TX ring for "+str(self), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
if not envelope: RNS.log("Spurious message received on "+str(self), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
|
||||
def _packet_delivered(self, packet: TPacket):
|
||||
self._packet_tx_op(packet, lambda env: True)
|
||||
@@ -545,29 +450,23 @@ class Channel(contextlib.AbstractContextManager):
|
||||
for envelope in self._tx_ring:
|
||||
updated_timeout = self._get_packet_timeout_time(envelope.tries)
|
||||
if envelope.packet and hasattr(envelope.packet, "receipt") and envelope.packet.receipt and envelope.packet.receipt.timeout:
|
||||
if updated_timeout > envelope.packet.receipt.timeout:
|
||||
envelope.packet.receipt.set_timeout(updated_timeout)
|
||||
if updated_timeout > envelope.packet.receipt.timeout: envelope.packet.receipt.set_timeout(updated_timeout)
|
||||
|
||||
def _get_packet_timeout_time(self, tries: int) -> float:
|
||||
to = pow(1.5, tries - 1) * max(self._outlet.rtt*2.5, 0.025) * (len(self._tx_ring)+1.5)
|
||||
return to
|
||||
|
||||
def _packet_timeout(self, packet: TPacket):
|
||||
if self._outlet.get_packet_state(packet) == MessageState.MSGSTATE_DELIVERED:
|
||||
return
|
||||
if self._outlet.get_packet_state(packet) == MessageState.MSGSTATE_DELIVERED: return
|
||||
|
||||
target_id = self._outlet.get_packet_id(packet)
|
||||
envelope_to_resend: Envelope | None = None
|
||||
should_teardown = False
|
||||
with self._lock:
|
||||
envelope = next(filter(
|
||||
lambda e: e.packet is not None and self._outlet.get_packet_id(e.packet) == target_id,
|
||||
self._tx_ring), None)
|
||||
if envelope is None:
|
||||
return
|
||||
envelope = next(filter(lambda e: e.packet is not None and self._outlet.get_packet_id(e.packet) == target_id, self._tx_ring), None)
|
||||
if envelope is None: return
|
||||
|
||||
if envelope.tries >= self._max_tries:
|
||||
should_teardown = True
|
||||
if envelope.tries >= self._max_tries: should_teardown = True
|
||||
else:
|
||||
envelope.tries += 1
|
||||
envelope_to_resend = envelope
|
||||
@@ -587,14 +486,11 @@ class Channel(contextlib.AbstractContextManager):
|
||||
self._outlet.resend(envelope_to_resend.packet)
|
||||
with self._lock:
|
||||
self._outlet.set_packet_delivered_callback(envelope_to_resend.packet, self._packet_delivered)
|
||||
self._outlet.set_packet_timeout_callback(
|
||||
envelope_to_resend.packet, self._packet_timeout,
|
||||
self._get_packet_timeout_time(envelope_to_resend.tries))
|
||||
self._outlet.set_packet_timeout_callback(envelope_to_resend.packet, self._packet_timeout, self._get_packet_timeout_time(envelope_to_resend.tries))
|
||||
self._update_packet_timeouts()
|
||||
already_delivered = (self._outlet.get_packet_state(envelope_to_resend.packet) == MessageState.MSGSTATE_DELIVERED)
|
||||
|
||||
if already_delivered:
|
||||
self._packet_delivered(envelope_to_resend.packet)
|
||||
if already_delivered: self._packet_delivered(envelope_to_resend.packet)
|
||||
|
||||
def send(self, message: MessageBase) -> Envelope:
|
||||
"""
|
||||
@@ -605,24 +501,19 @@ class Channel(contextlib.AbstractContextManager):
|
||||
"""
|
||||
with self._send_lock:
|
||||
with self._lock:
|
||||
if not self.is_ready_to_send():
|
||||
raise ChannelException(CEType.ME_LINK_NOT_READY, f"Link is not ready")
|
||||
if not self.is_ready_to_send(): raise ChannelException(CEType.ME_LINK_NOT_READY, f"Link is not ready")
|
||||
|
||||
reserved_sequence = self._next_sequence
|
||||
envelope = Envelope(self._outlet, message=message, sequence=reserved_sequence)
|
||||
envelope.pack()
|
||||
if len(envelope.raw) > self._outlet.mdu:
|
||||
raise ChannelException(CEType.ME_TOO_BIG,
|
||||
f"Packed message too big for packet: {len(envelope.raw)} > {self._outlet.mdu}")
|
||||
raise ChannelException(CEType.ME_TOO_BIG, f"Packed message too big for packet: {len(envelope.raw)} > {self._outlet.mdu}")
|
||||
self._next_sequence = (reserved_sequence + 1) % Channel.SEQ_MODULUS
|
||||
|
||||
envelope.packet = self._outlet.send(envelope.raw)
|
||||
|
||||
if (envelope.packet is None
|
||||
or getattr(envelope.packet, "raw", None) is None
|
||||
or (hasattr(envelope.packet, "receipt") and envelope.packet.receipt is None)):
|
||||
with self._lock:
|
||||
self._next_sequence = reserved_sequence
|
||||
if envelope.packet is None or getattr(envelope.packet, "raw", None) is None or (hasattr(envelope.packet, "receipt") and envelope.packet.receipt is None):
|
||||
with self._lock: self._next_sequence = reserved_sequence
|
||||
raise ChannelException(CEType.ME_LINK_NOT_READY, "Outlet did not transmit packet")
|
||||
|
||||
with self._lock:
|
||||
@@ -633,9 +524,8 @@ class Channel(contextlib.AbstractContextManager):
|
||||
self._update_packet_timeouts()
|
||||
already_delivered = (self._outlet.get_packet_state(envelope.packet) == MessageState.MSGSTATE_DELIVERED)
|
||||
|
||||
# prevent _tx_ring envelope leak
|
||||
if already_delivered:
|
||||
self._packet_delivered(envelope.packet)
|
||||
# Prevent _tx_ring envelope leak
|
||||
if already_delivered: self._packet_delivered(envelope.packet)
|
||||
|
||||
return envelope
|
||||
|
||||
@@ -650,14 +540,13 @@ class Channel(contextlib.AbstractContextManager):
|
||||
:return: number of bytes available
|
||||
"""
|
||||
mdu = self._outlet.mdu - 6 # sizeof(msgtype) + sizeof(length) + sizeof(sequence)
|
||||
if mdu > 0xFFFF:
|
||||
mdu = 0xFFFF
|
||||
if mdu > 0xFFFF: mdu = 0xFFFF
|
||||
return mdu
|
||||
|
||||
|
||||
class LinkChannelOutlet(ChannelOutletBase):
|
||||
class LinkChannelOutlet(ABC, Generic[TPacket]):
|
||||
"""
|
||||
An implementation of ChannelOutletBase for RNS.Link.
|
||||
A channel outlet implementation for RNS.Link.
|
||||
Allows Channel to send packets over an RNS Link with
|
||||
Packets.
|
||||
|
||||
@@ -668,71 +557,51 @@ class LinkChannelOutlet(ChannelOutletBase):
|
||||
|
||||
def send(self, raw: bytes) -> RNS.Packet:
|
||||
packet = RNS.Packet(self.link, raw, context=RNS.Packet.CHANNEL)
|
||||
if self.link.status == RNS.Link.ACTIVE:
|
||||
packet.send()
|
||||
if self.link.status == RNS.Link.ACTIVE: packet.send()
|
||||
return packet
|
||||
|
||||
def resend(self, packet: RNS.Packet) -> RNS.Packet:
|
||||
receipt = packet.resend()
|
||||
if not receipt:
|
||||
RNS.log("Failed to resend packet", RNS.LOG_ERROR)
|
||||
if not receipt: RNS.log("Failed to resend packet", RNS.LOG_ERROR)
|
||||
return packet
|
||||
|
||||
@property
|
||||
def mdu(self):
|
||||
return self.link.mdu
|
||||
def mdu(self): return self.link.mdu
|
||||
|
||||
@property
|
||||
def rtt(self):
|
||||
return self.link.rtt
|
||||
def rtt(self): return self.link.rtt
|
||||
|
||||
# TODO: Initial implementation had issues
|
||||
# proxying this to link status. Should
|
||||
# return False when link is not ACTIVE.
|
||||
# Investigate what happened here.
|
||||
@property
|
||||
def is_usable(self):
|
||||
return True # had issues looking at Link.status
|
||||
def is_usable(self): return True
|
||||
|
||||
def get_packet_state(self, packet: TPacket) -> MessageState:
|
||||
if packet.receipt == None:
|
||||
return MessageState.MSGSTATE_FAILED
|
||||
|
||||
status = packet.receipt.get_status()
|
||||
if status == RNS.PacketReceipt.SENT:
|
||||
return MessageState.MSGSTATE_SENT
|
||||
if status == RNS.PacketReceipt.DELIVERED:
|
||||
return MessageState.MSGSTATE_DELIVERED
|
||||
if status == RNS.PacketReceipt.FAILED:
|
||||
return MessageState.MSGSTATE_FAILED
|
||||
else:
|
||||
raise Exception(f"Unexpected receipt state: {status}")
|
||||
if status == RNS.PacketReceipt.SENT: return MessageState.MSGSTATE_SENT
|
||||
if status == RNS.PacketReceipt.DELIVERED: return MessageState.MSGSTATE_DELIVERED
|
||||
if status == RNS.PacketReceipt.FAILED: return MessageState.MSGSTATE_FAILED
|
||||
else: raise Exception(f"Unexpected receipt state: {status}")
|
||||
|
||||
def timed_out(self):
|
||||
self.link.teardown()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}({self.link})"
|
||||
|
||||
def set_packet_timeout_callback(self, packet: RNS.Packet, callback: Callable[[RNS.Packet], None] | None,
|
||||
timeout: float | None = None):
|
||||
if timeout and packet.receipt:
|
||||
packet.receipt.set_timeout(timeout)
|
||||
|
||||
def inner(receipt: RNS.PacketReceipt):
|
||||
callback(packet)
|
||||
|
||||
if packet and packet.receipt:
|
||||
packet.receipt.set_timeout_callback(inner if callback else None)
|
||||
def set_packet_timeout_callback(self, packet: RNS.Packet, callback: Callable[[RNS.Packet], None] | None, timeout: float | None = None):
|
||||
if timeout and packet.receipt: packet.receipt.set_timeout(timeout)
|
||||
def inner(receipt: RNS.PacketReceipt): callback(packet)
|
||||
if packet and packet.receipt: packet.receipt.set_timeout_callback(inner if callback else None)
|
||||
|
||||
def set_packet_delivered_callback(self, packet: RNS.Packet, callback: Callable[[RNS.Packet], None] | None):
|
||||
def inner(receipt: RNS.PacketReceipt):
|
||||
callback(packet)
|
||||
|
||||
if packet and packet.receipt:
|
||||
packet.receipt.set_delivery_callback(inner if callback else None)
|
||||
def inner(receipt: RNS.PacketReceipt): callback(packet)
|
||||
if packet and packet.receipt: packet.receipt.set_delivery_callback(inner if callback else None)
|
||||
|
||||
def get_packet_id(self, packet: RNS.Packet) -> any:
|
||||
if (packet
|
||||
and getattr(packet, "raw", None) is not None
|
||||
and hasattr(packet, "get_hash")
|
||||
and callable(packet.get_hash)):
|
||||
if packet and getattr(packet, "raw", None) is not None and hasattr(packet, "get_hash") and callable(packet.get_hash):
|
||||
return packet.get_hash()
|
||||
else:
|
||||
return None
|
||||
else: return None
|
||||
|
||||
def timed_out(self): self.link.teardown()
|
||||
|
||||
def __str__(self): return f"{self.__class__.__name__}({self.link})"
|
||||
+15
-4
@@ -155,6 +155,7 @@ class Destination:
|
||||
self.accept_link_requests = True
|
||||
self.callbacks = Callbacks()
|
||||
self.request_handlers = {}
|
||||
self.max_request_size = None
|
||||
self.type = type
|
||||
self.direction = direction
|
||||
self.proof_strategy = Destination.PROVE_NONE
|
||||
@@ -362,10 +363,20 @@ class Destination:
|
||||
|
||||
:param proof_strategy: One of ``RNS.Destination.PROVE_NONE``, ``RNS.Destination.PROVE_ALL`` or ``RNS.Destination.PROVE_APP``. If ``RNS.Destination.PROVE_APP`` is set, the `proof_requested_callback` will be called to determine whether a proof should be sent or not.
|
||||
"""
|
||||
if not proof_strategy in Destination.proof_strategies:
|
||||
raise TypeError("Unsupported proof strategy")
|
||||
else:
|
||||
self.proof_strategy = proof_strategy
|
||||
if not proof_strategy in Destination.proof_strategies: raise TypeError("Unsupported proof strategy")
|
||||
else: self.proof_strategy = proof_strategy
|
||||
|
||||
def set_max_request_size(self, max_request_size):
|
||||
"""
|
||||
Sets the maximum accepted request size for registered request handlers.
|
||||
|
||||
:param max_request_size: The maximum accepted request size in bytes, as an integer.
|
||||
:raises: ``TypeError`` or ``ValueError`` if any of the argument is invalid.
|
||||
"""
|
||||
try: max_request_size = int(max_request_size)
|
||||
except: raise TypeError("Invalid maximum request size specified")
|
||||
if max_request_size < 0: raise ValueError("Maximum request size cannot be negative")
|
||||
self.max_request_size = max_request_size
|
||||
|
||||
def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None, auto_compress = True):
|
||||
"""
|
||||
|
||||
+80
-5
@@ -8,6 +8,7 @@ import ipaddress
|
||||
import subprocess
|
||||
from threading import Lock
|
||||
from .vendor import umsgpack as msgpack
|
||||
from collections import deque
|
||||
|
||||
NAME = 0xFF
|
||||
TRANSPORT_ID = 0xFE
|
||||
@@ -31,7 +32,7 @@ APP_NAME = "rnstransport"
|
||||
|
||||
class InterfaceAnnouncer():
|
||||
JOB_INTERVAL = 60
|
||||
DEFAULT_STAMP_VALUE = 14
|
||||
DEFAULT_STAMP_VALUE = 16
|
||||
WORKBLOCK_EXPAND_ROUNDS = 20
|
||||
|
||||
DISCOVERABLE_INTERFACE_TYPES = ["BackboneInterface", "TCPServerInterface", "TCPClientInterface",
|
||||
@@ -99,6 +100,33 @@ class InterfaceAnnouncer():
|
||||
|
||||
if not interface_type in self.DISCOVERABLE_INTERFACE_TYPES: return None
|
||||
else:
|
||||
if not RNS.vendor.platformutils.is_windows() and interface.discovery_location:
|
||||
try:
|
||||
discovery_location = self.sanitize(interface.discovery_location)
|
||||
exec_path = os.path.expanduser(discovery_location)
|
||||
if os.path.isfile(exec_path) and os.access(exec_path, os.X_OK):
|
||||
RNS.log(f"Evaluating discovery location from executable at {exec_path}", RNS.LOG_DEBUG)
|
||||
exec_result = subprocess.run([exec_path], stdout=subprocess.PIPE)
|
||||
exec_stdout = exec_result.stdout.decode("utf-8")
|
||||
if exec_result.returncode != 0: raise ValueError("Non-zero exit code from subprocess")
|
||||
discovery_location = self.sanitize(exec_stdout)
|
||||
location_components = discovery_location.replace(" ", "").split(",")
|
||||
if len(location_components) != 3: raise ValueError(f"Invalid location component count: {len(location_components)}")
|
||||
dlat = float(location_components[0])
|
||||
dlon = float(location_components[1])
|
||||
dhgt = float(location_components[2])
|
||||
if dlat < -90 or dlat > 90: raise ValueError(f"Invalid latitude: {dlat}")
|
||||
if dlon < -180 or dlat > 180: raise ValueError(f"Invalid longitude: {dlon}")
|
||||
if dhgt < -4000 or dhgt > 1e6: raise ValueError(f"Invalid height: {dhgt}")
|
||||
interface.discovery_latitude = dlat
|
||||
interface.discovery_longitude = dlon
|
||||
interface.discovery_height = dhgt
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while getting reachable_on from executable at {interface.reachable_on}: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"Aborting discovery announce", RNS.LOG_ERROR)
|
||||
return None
|
||||
|
||||
flags = 0x00
|
||||
info = {INTERFACE_TYPE: interface_type,
|
||||
TRANSPORT: RNS.Reticulum.transport_enabled(),
|
||||
@@ -201,6 +229,10 @@ class InterfaceAnnounceHandler:
|
||||
self.required_value = required_value
|
||||
self.callback = callback
|
||||
self.stamper = LXStamper
|
||||
self.valid_cache = {}
|
||||
self.valid_cache_max = 2048
|
||||
self.invalid_cache = deque(maxlen=2048)
|
||||
self.validation_lock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def sanitize_name(name):
|
||||
@@ -223,21 +255,43 @@ class InterfaceAnnounceHandler:
|
||||
app_data = app_data[1:]
|
||||
signed = flags & self.FLAG_SIGNED
|
||||
encrypted = flags & self.FLAG_ENCRYPTED
|
||||
fullhash = RNS.Identity.full_hash(app_data)
|
||||
|
||||
if fullhash in self.invalid_cache:
|
||||
RNS.log(f"Ignored previously discovered interface with insufficient stamp value", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
|
||||
return
|
||||
|
||||
if fullhash in self.valid_cache:
|
||||
RNS.log(f"Discovery announce cache hit", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
|
||||
cache_entry = self.valid_cache[fullhash]
|
||||
stamp = cache_entry["stamp"]
|
||||
packed = cache_entry["packed"]
|
||||
value = cache_entry["value"]
|
||||
valid = cache_entry["valid"]
|
||||
|
||||
else:
|
||||
if encrypted:
|
||||
if not RNS.Transport.has_network_identity(): return
|
||||
app_data = RNS.Transport.network_identity.decrypt(app_data)
|
||||
if not app_data: return
|
||||
|
||||
if self.validation_lock.locked():
|
||||
RNS.log(f"Dropping received interface discovery announce, already validating other discovery stamp", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return
|
||||
|
||||
with self.validation_lock:
|
||||
stamp = app_data[-self.stamper.STAMP_SIZE:]
|
||||
packed = app_data[:-self.stamper.STAMP_SIZE]
|
||||
infohash = RNS.Identity.full_hash(packed)
|
||||
workblock = self.stamper.stamp_workblock(infohash, expand_rounds=InterfaceAnnouncer.WORKBLOCK_EXPAND_ROUNDS)
|
||||
value = self.stamper.stamp_value(workblock, stamp)
|
||||
valid = self.stamper.stamp_valid(stamp, self.required_value, workblock)
|
||||
self.valid_cache[fullhash] = {"valid": valid, "value": value, "packed": packed, "stamp": stamp}
|
||||
while len(self.valid_cache) > self.valid_cache_max: self.valid_cache.pop(next(iter(self.valid_cache)))
|
||||
|
||||
if not valid:
|
||||
RNS.log(f"Ignored discovered interface with invalid stamp", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Ignored discovered interface with insufficient stamp value {value}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.invalid_cache.append(fullhash)
|
||||
return
|
||||
|
||||
if value < self.required_value: RNS.log(f"Ignored discovered interface with stamp value {value}", RNS.LOG_DEBUG)
|
||||
@@ -277,7 +331,7 @@ class InterfaceAnnounceHandler:
|
||||
if IFAC_NETKEY in unpacked: info["ifac_netkey"] = str(unpacked[IFAC_NETKEY])
|
||||
|
||||
if interface_type in ["BackboneInterface", "TCPServerInterface"]:
|
||||
backbone_support = not RNS.vendor.platformutils.is_windows()
|
||||
backbone_support = not RNS.vendor.platformutils.is_windows() and not RNS.vendor.platformutils.is_darwin()
|
||||
info["reachable_on"] = unpacked[REACHABLE_ON]
|
||||
info["port"] = unpacked[PORT]
|
||||
connection_interface = "BackboneInterface" if backbone_support else "TCPClientInterface"
|
||||
@@ -376,6 +430,9 @@ class InterfaceDiscovery():
|
||||
AUTOCONNECT_TYPES = ["BackboneInterface", "TCPServerInterface"]
|
||||
DISCOVERABLE_TYPES = ["BackboneInterface", "TCPServerInterface", "I2PInterface", "RNodeInterface", "WeaveInterface", "KISSInterface"]
|
||||
|
||||
AC_TRANSPORT_MODE = RNS.Interfaces.Interface.Interface.MODE_GATEWAY
|
||||
AC_GRAVITY = 0
|
||||
|
||||
discovery_lock = Lock()
|
||||
|
||||
def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None, discover_interfaces=True):
|
||||
@@ -389,6 +446,8 @@ class InterfaceDiscovery():
|
||||
self.monitor_interval = self.MONITOR_INTERVAL
|
||||
self.detach_threshold = self.DETACH_THRESHOLD
|
||||
self.initial_autoconnect_ran = False
|
||||
self.blackholed_updated = 0
|
||||
self.__blackholed = {}
|
||||
|
||||
if not self.rns_instance: raise SystemError("Attempt to start interface discovery listener without an active RNS instance")
|
||||
self.storagepath = os.path.join(RNS.Reticulum.storagepath, "discovery", "interfaces")
|
||||
@@ -399,10 +458,18 @@ class InterfaceDiscovery():
|
||||
RNS.Transport.register_announce_handler(self.handler)
|
||||
threading.Thread(target=self.connect_discovered, daemon=True).start()
|
||||
|
||||
def __blackholed_identities(self):
|
||||
st = time.time()
|
||||
if not self.__blackholed or time.time() > self.blackholed_updated+60:
|
||||
self.__blackholed = self.rns_instance.get_blackholed_identities()
|
||||
self.blackholed_updated = time.time()
|
||||
return self.__blackholed
|
||||
|
||||
def list_discovered_interfaces(self, only_available=False, only_transport=False):
|
||||
now = time.time()
|
||||
discovered_interfaces = []
|
||||
discovery_sources = RNS.Reticulum.interface_discovery_sources()
|
||||
blackholed_identities = self.__blackholed_identities()
|
||||
for filename in os.listdir(self.storagepath):
|
||||
try:
|
||||
with self.discovery_lock:
|
||||
@@ -414,9 +481,13 @@ class InterfaceDiscovery():
|
||||
info["name"] = InterfaceAnnounceHandler.sanitize_name(info["name"])
|
||||
|
||||
if heard_delta > self.THRESHOLD_REMOVE: should_remove = True
|
||||
elif not "transport_id" in info or not info["transport_id"]: should_remove = True
|
||||
elif not "network_id" in info or not info["network_id"]: should_remove = True
|
||||
elif discovery_sources and not "network_id" in info: should_remove = True
|
||||
elif discovery_sources and not bytes.fromhex(info["network_id"]) in discovery_sources: should_remove = True
|
||||
elif not "type" in info or ("type" in info and not info["type"] in self.DISCOVERABLE_TYPES): should_remove = True
|
||||
elif bytes.fromhex(info["network_id"]) in blackholed_identities: should_remove = True
|
||||
elif bytes.fromhex(info["transport_id"]) in blackholed_identities: should_remove = True
|
||||
elif "reachable_on" in info:
|
||||
if not (is_ip_address(info["reachable_on"]) or is_hostname(info["reachable_on"])): should_remove = True
|
||||
|
||||
@@ -668,12 +739,16 @@ 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
|
||||
if RNS.Reticulum.autoconnect_interface_mode(): mode = RNS.Reticulum.autoconnect_interface_mode()
|
||||
else: mode = self.AC_TRANSPORT_MODE if RNS.Reticulum.transport_enabled() else None
|
||||
internal_a = True if RNS.Reticulum.autoconnect_announces_to_internal() else None
|
||||
gravity = RNS.Reticulum.autoconnect_interface_gravity() or self.AC_GRAVITY
|
||||
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, 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)
|
||||
announce_rate_target=ar_target, announce_rate_grace=ar_grace, announce_rate_penalty=ar_penalty,
|
||||
announces_to_internal=internal_a, gravity=gravity)
|
||||
self.monitor_interface(interface)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
+26
-43
@@ -174,12 +174,9 @@ class Identity:
|
||||
else: return None
|
||||
|
||||
@staticmethod
|
||||
def save_known_destinations(background=False, recombine=True):
|
||||
# TODO: Improve the storage method so we don't have to
|
||||
# deserialize and serialize the entire table on every
|
||||
# save, but the only changes. It might be possible to
|
||||
# simply overwrite on exit now that every local client
|
||||
# disconnect triggers a data persist.
|
||||
def save_known_destinations(background=False, recombine=False):
|
||||
if recombine: RNS.log(f"Recombining known destinations from disk cache on persist is deprecated, argument ignored", RNS.LOG_WARNING)
|
||||
if RNS.Transport.owner.is_connected_to_shared_instance: return
|
||||
|
||||
try:
|
||||
if hasattr(Identity, "saving_known_destinations"):
|
||||
@@ -194,31 +191,12 @@ class Identity:
|
||||
|
||||
Identity.saving_known_destinations = True
|
||||
save_start = time.time()
|
||||
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
if recombine:
|
||||
storage_known_destinations = {}
|
||||
if os.path.isfile(RNS.Reticulum.storagepath+"/known_destinations"):
|
||||
try:
|
||||
with open(RNS.Reticulum.storagepath+"/known_destinations","rb") as file:
|
||||
storage_known_destinations = umsgpack.load(file)
|
||||
|
||||
except: pass
|
||||
|
||||
try:
|
||||
for destination_hash in storage_known_destinations:
|
||||
if not destination_hash in Identity.known_destinations:
|
||||
with Identity.known_destinations_lock:
|
||||
Identity.known_destinations[destination_hash] = storage_known_destinations[destination_hash]
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Skipped recombining known destinations from disk, since an error occurred: "+str(e), RNS.LOG_WARNING)
|
||||
|
||||
RNS.log("Saving "+str(len(Identity.known_destinations))+" known destinations to storage...", RNS.LOG_VERBOSE)
|
||||
temp_file = RNS.Reticulum.storagepath+f"/known_destinations.tmp.{time.time()}"
|
||||
|
||||
try:
|
||||
with open(temp_file,"wb") as file: umsgpack.dump(Identity.known_destinations.copy(), file)
|
||||
os.rename(temp_file, RNS.Reticulum.storagepath+f"/known_destinations")
|
||||
os.replace(temp_file, RNS.Reticulum.storagepath+f"/known_destinations")
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while serializing and writing known destinations: {e}", RNS.LOG_ERROR)
|
||||
@@ -226,17 +204,13 @@ class Identity:
|
||||
except Exception as e: RNS.log(f"Could not clean up temporary file {temp_file}: {e}", RNS.LOG_WARNING)
|
||||
raise e
|
||||
|
||||
save_time = time.time() - save_start
|
||||
if save_time < 1: time_str = str(round(save_time*1000,2))+"ms"
|
||||
else: time_str = str(round(save_time,2))+"s"
|
||||
|
||||
RNS.log("Saved known destinations to storage in "+time_str, RNS.LOG_VERBOSE)
|
||||
RNS.log(f"Saved known destinations to storage in {RNS.prettyshorttime(time.time()-save_start)}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while saving known destinations to disk, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
Identity.saving_known_destinations = False
|
||||
finally: Identity.saving_known_destinations = False
|
||||
|
||||
@staticmethod
|
||||
def load_known_destinations():
|
||||
@@ -308,7 +282,7 @@ class Identity:
|
||||
except Exception as e: RNS.log(f"Error while retaining identity {RNS.prettyhexrep(identity_hash)}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
@staticmethod
|
||||
def clean_known_destinations():
|
||||
def clean_known_destinations(background=False):
|
||||
now = time.time()
|
||||
st = now
|
||||
total = len(Identity.known_destinations)
|
||||
@@ -316,10 +290,15 @@ class Identity:
|
||||
no_path = 0
|
||||
retained = 0
|
||||
never_used = 0
|
||||
ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
|
||||
|
||||
RNS.log(f"Cleaning known destinations{' at background priority' if background else ''}...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
with Identity.known_destinations_lock: destination_hashes = list(Identity.known_destinations.keys())
|
||||
for destination_hash in destination_hashes:
|
||||
try:
|
||||
if background: time.sleep(0.001) # Low priority, yield thread
|
||||
RNS.Transport.destinations_last_cleaned = time.time()
|
||||
if RNS.Transport.has_path(destination_hash): has_path = True
|
||||
else:
|
||||
has_path = False
|
||||
@@ -359,7 +338,14 @@ class Identity:
|
||||
Identity.known_destinations.pop(destination_hash)
|
||||
removed += 1
|
||||
|
||||
# RNS.log(f"Total destinations: {total}, stale: {len(stale)}, removed: {removed}, no path: {no_path}, never used: {never_used}, with path: {total-no_path}, used: {total-never_used}, retained: {retained}. Completed in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_WARNING) # TODO: Remove
|
||||
try:
|
||||
hexhash = RNS.hexrep(destination_hash, delimit=False)
|
||||
ratchet_path = f"{ratchetdir}/{hexhash}"
|
||||
if os.path.isfile(ratchet_path): os.unlink(ratchet_path)
|
||||
except Exception as e: RNS.log(f"Could not clean stale ratchets for {RNS.prettyhexrep(destination_hash)}: {e}", RNS.LOG_WARNING)
|
||||
|
||||
RNS.log(f"Cleaned known destinations in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
RNS.log(f"Total: {total}, stale: {len(stale)}, removed: {removed}, no path: {no_path}, never used: {never_used}, with path: {total-no_path}, used: {total-never_used}, retained: {retained}", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
|
||||
if not RNS.Transport.owner.is_connected_to_shared_instance: Identity.save_known_destinations(recombine=False)
|
||||
|
||||
@staticmethod
|
||||
@@ -595,21 +581,18 @@ class Identity:
|
||||
signal_str = " ["
|
||||
if packet.rssi != None:
|
||||
signal_str += "RSSI "+str(packet.rssi)+"dBm"
|
||||
if packet.snr != None:
|
||||
signal_str += ", "
|
||||
if packet.snr != None:
|
||||
signal_str += "SNR "+str(packet.snr)+"dB"
|
||||
if packet.snr != None: signal_str += ", "
|
||||
if packet.snr != None: signal_str += "SNR "+str(packet.snr)+"dB"
|
||||
signal_str += "]"
|
||||
else:
|
||||
signal_str = ""
|
||||
|
||||
else: signal_str = ""
|
||||
|
||||
if hasattr(packet, "transport_id") and packet.transport_id != None:
|
||||
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received via "+RNS.prettyhexrep(packet.transport_id)+" on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
else:
|
||||
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
|
||||
if ratchet:
|
||||
Identity._remember_ratchet(destination_hash, ratchet)
|
||||
if ratchet: Identity._remember_ratchet(destination_hash, ratchet)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -428,14 +428,22 @@ class AutoInterface(Interface):
|
||||
if ifname in self.interface_servers:
|
||||
RNS.log("Shutting down previous UDP listener for "+str(self)+" "+str(ifname), RNS.LOG_DEBUG)
|
||||
previous_server = self.interface_servers[ifname]
|
||||
def shutdown_server():
|
||||
previous_server.shutdown()
|
||||
def shutdown_server(): previous_server.shutdown()
|
||||
threading.Thread(target=shutdown_server, daemon=True).start()
|
||||
|
||||
RNS.log("Starting new UDP listener for "+str(self)+" "+str(ifname), RNS.LOG_DEBUG)
|
||||
|
||||
retry_delay = 1.25
|
||||
listener_started = False
|
||||
while not listener_started:
|
||||
try:
|
||||
time.sleep(retry_delay)
|
||||
udp_server = socketserver.UDPServer(listen_address, self.handler_factory(self.process_incoming))
|
||||
self.interface_servers[ifname] = udp_server
|
||||
listener_started = True
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not start new UDP listener for {self} on {listen_address}: {e}", RNS.LOG_WARNING)
|
||||
RNS.log(f"Retrying in {retry_delay} seconds", RNS.LOG_WARNING)
|
||||
|
||||
thread = threading.Thread(target=udp_server.serve_forever)
|
||||
thread.daemon = True
|
||||
@@ -572,6 +580,7 @@ class AutoInterface(Interface):
|
||||
spawned_interface.announce_rate_grace = self.announce_rate_grace
|
||||
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.gravity = self.gravity
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
|
||||
@@ -54,6 +54,13 @@ class BackboneInterface(Interface):
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
AUTOCONFIGURE_MTU = True
|
||||
|
||||
BLOCK_FAST_FLAPPING = True
|
||||
FAST_FLAP_THRESHOLD = 20
|
||||
FAST_FLAP_GRACE = 5
|
||||
FAST_FLAP_EXPIRY = 12*60*60
|
||||
fast_flapping_lock = threading.Lock()
|
||||
fast_flapping = {}
|
||||
|
||||
epoll = None
|
||||
listener_filenos = {}
|
||||
spawned_interface_filenos = {}
|
||||
@@ -116,6 +123,10 @@ class BackboneInterface(Interface):
|
||||
bindip = c["listen_ip"] if "listen_ip" in c else None
|
||||
bindport = int(c["listen_port"]) if "listen_port" in c else None
|
||||
prefer_ipv6 = c.as_bool("prefer_ipv6") if "prefer_ipv6" in c else False
|
||||
flap_block = c.as_bool("block_fast_flapping") if "block_fast_flapping" in c else BackboneInterface.BLOCK_FAST_FLAPPING
|
||||
flap_threshold = c.as_float("fast_flapping_threshold") if "fast_flapping_threshold" in c else BackboneInterface.FAST_FLAP_THRESHOLD
|
||||
flap_grace = c.as_int("fast_flapping_grace") if "fast_flapping_grace" in c else BackboneInterface.FAST_FLAP_GRACE
|
||||
flap_expiry = c.as_float("fast_flapping_block_time")*60 if "fast_flapping_block_time" in c else BackboneInterface.FAST_FLAP_EXPIRY
|
||||
|
||||
if port != None: bindport = port
|
||||
|
||||
@@ -128,11 +139,13 @@ class BackboneInterface(Interface):
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
self.spawned_interfaces = []
|
||||
self.supports_discovery = True
|
||||
self.block_fast_flapping = flap_block
|
||||
self.fast_flap_threshold = flap_threshold
|
||||
self.fast_flap_grace = flap_grace
|
||||
self.fast_flap_expiry = flap_expiry
|
||||
|
||||
if bindport == None:
|
||||
raise SystemError(f"No TCP port configured for interface \"{name}\"")
|
||||
else:
|
||||
self.bind_port = bindport
|
||||
if bindport == None: raise SystemError(f"No TCP port configured for interface \"{name}\"")
|
||||
else: self.bind_port = bindport
|
||||
|
||||
bind_address = None
|
||||
if device != None:
|
||||
@@ -263,7 +276,9 @@ class BackboneInterface(Interface):
|
||||
|
||||
try: BackboneInterface.epoll.unregister(fileno)
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while deregistering file descriptor {fileno}: {e}", RNS.LOG_DEBUG)
|
||||
if str(e).endswith("No such file or directory"): pass
|
||||
elif str(e).endswith("Bad file descriptor"): pass
|
||||
else: RNS.log(f"An error occurred while deregistering file descriptor {fileno}: {e}", RNS.LOG_DEBUG)
|
||||
|
||||
@staticmethod
|
||||
def deregister_listeners():
|
||||
@@ -282,7 +297,9 @@ class BackboneInterface(Interface):
|
||||
if fileno in BackboneInterface.spawned_interface_filenos:
|
||||
try: BackboneInterface.epoll.modify(fileno, select.EPOLLOUT)
|
||||
except Exception as e:
|
||||
RNS.log(f"Error occurred on {interface} while modifying socket EPOLL state: {e}", RNS.LOG_WARNING)
|
||||
if str(e).endswith("No such file or directory"): pass
|
||||
elif str(e).endswith("Bad file descriptor"): pass
|
||||
else: RNS.log(f"Error occurred on {interface} while modifying socket EPOLL state: {e}", RNS.LOG_WARNING)
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
@@ -302,7 +319,7 @@ class BackboneInterface(Interface):
|
||||
if client_socket and fileno == client_socket.fileno() and (event & select.EPOLLIN):
|
||||
try: received_bytes = client_socket.recv(spawned_interface.HW_MTU)
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while reading from {spawned_interface}: {e}", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Error while reading from {spawned_interface}: {e}", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
|
||||
received_bytes = b""
|
||||
|
||||
if len(received_bytes): spawned_interface.receive(received_bytes)
|
||||
@@ -325,7 +342,13 @@ class BackboneInterface(Interface):
|
||||
try: written = client_socket.send(spawned_interface.transmit_buffer)
|
||||
except Exception as e:
|
||||
written = 0
|
||||
if not spawned_interface.detached: RNS.log(f"Error while writing to {spawned_interface}: {e}", RNS.LOG_DEBUG)
|
||||
if not spawned_interface.detached:
|
||||
if RNS.sl(RNS.LOG_DEBUG):
|
||||
if str(e).endswith("Connection timed out"): pass
|
||||
elif str(e).endswith("Connection reset by peer"): pass
|
||||
elif str(e).endswith("No route to host"): pass
|
||||
elif str(e).endswith("Broken pipe"): pass
|
||||
else: RNS.log(f"Error while writing to {spawned_interface}: {e}", RNS.LOG_DEBUG)
|
||||
BackboneInterface.deregister_fileno(fileno)
|
||||
|
||||
try:
|
||||
@@ -379,7 +402,7 @@ class BackboneInterface(Interface):
|
||||
try: client_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_WARNING)
|
||||
|
||||
except:
|
||||
except Exception as e:
|
||||
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 socket accept: {e}", RNS.LOG_WARNING)
|
||||
@@ -399,8 +422,22 @@ class BackboneInterface(Interface):
|
||||
BackboneInterface.deregister_listeners()
|
||||
|
||||
def incoming_connection(self, socket):
|
||||
RNS.log("Accepting incoming connection", RNS.LOG_VERBOSE)
|
||||
try:
|
||||
remote_ip = socket.getpeername()[0]
|
||||
remote_port = str(socket.getpeername()[1])
|
||||
if self.blocked_ip_count > 0:
|
||||
with BackboneInterface.fast_flapping_lock:
|
||||
if remote_ip in BackboneInterface.fast_flapping:
|
||||
now = time.time()
|
||||
ffe = BackboneInterface.fast_flapping[remote_ip]
|
||||
started_flapping = ffe[0]
|
||||
last_flap = ffe[1]
|
||||
flaps = ffe[2]
|
||||
if flaps > self.fast_flap_grace:
|
||||
RNS.log(f"Ignoring incoming connection from fast-flapping IP {remote_ip}", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
|
||||
return False
|
||||
|
||||
RNS.log("Accepting incoming connection", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
|
||||
spawned_configuration = {"name": "Client on "+self.name, "target_host": None, "target_port": None}
|
||||
spawned_interface = BackboneClientInterface(self.owner, spawned_configuration, connected_socket=socket)
|
||||
spawned_interface.OUT = self.OUT
|
||||
@@ -421,8 +458,8 @@ class BackboneInterface(Interface):
|
||||
spawned_interface.ic_pr_burst_freq = self.ic_pr_burst_freq
|
||||
|
||||
spawned_interface.socket = socket
|
||||
spawned_interface.target_ip = socket.getpeername()[0]
|
||||
spawned_interface.target_port = str(socket.getpeername()[1])
|
||||
spawned_interface.target_ip = remote_ip
|
||||
spawned_interface.target_port = remote_port
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
spawned_interface.optimise_mtu()
|
||||
@@ -432,18 +469,12 @@ class BackboneInterface(Interface):
|
||||
spawned_interface.ifac_netkey = self.ifac_netkey
|
||||
if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
|
||||
ifac_origin = b""
|
||||
if spawned_interface.ifac_netname != None:
|
||||
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
|
||||
if spawned_interface.ifac_netkey != None:
|
||||
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
|
||||
if spawned_interface.ifac_netname != None: ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
|
||||
if spawned_interface.ifac_netkey != None: ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
|
||||
|
||||
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
|
||||
spawned_interface.ifac_key = RNS.Cryptography.hkdf(
|
||||
length=64,
|
||||
derive_from=ifac_origin_hash,
|
||||
salt=RNS.Reticulum.IFAC_SALT,
|
||||
context=None
|
||||
)
|
||||
spawned_interface.ifac_key = RNS.Cryptography.hkdf(length=64, derive_from=ifac_origin_hash,
|
||||
salt=RNS.Reticulum.IFAC_SALT, context=None)
|
||||
spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
|
||||
spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
|
||||
|
||||
@@ -451,13 +482,15 @@ class BackboneInterface(Interface):
|
||||
spawned_interface.announce_rate_grace = self.announce_rate_grace
|
||||
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.gravity = self.gravity
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface)
|
||||
self.spawned_interfaces.append(spawned_interface)
|
||||
BackboneInterface.add_client_socket(socket, spawned_interface)
|
||||
spawned_interface.spawned_at = time.time()
|
||||
spawned_interface.online = True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while accepting incoming connection on {self}: {e}", RNS.LOG_ERROR)
|
||||
@@ -492,14 +525,42 @@ class BackboneInterface(Interface):
|
||||
try: listener_socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
if str(e).endswith("Transport endpoint is not connected"): pass
|
||||
elif str(e).endswith("Bad file descriptor"): pass
|
||||
else: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def __str__(self):
|
||||
if ":" in self.bind_ip:
|
||||
ip_str = f"[{self.bind_ip}]"
|
||||
else:
|
||||
ip_str = f"{self.bind_ip}"
|
||||
@property
|
||||
def blocked_ip_list(self):
|
||||
if not self.block_fast_flapping: return []
|
||||
else: return list(self.fast_flapping.keys())
|
||||
|
||||
@property
|
||||
def blocked_ip_count(self):
|
||||
if not self.block_fast_flapping: return 0
|
||||
else:
|
||||
count = 0
|
||||
expired = []
|
||||
now = time.time()
|
||||
with BackboneInterface.fast_flapping_lock:
|
||||
for remote_ip in BackboneInterface.fast_flapping:
|
||||
ffe = BackboneInterface.fast_flapping[remote_ip]
|
||||
started_flapping = ffe[0]
|
||||
last_flap = ffe[1]
|
||||
flaps = ffe[2]
|
||||
if now - last_flap > self.fast_flap_expiry: expired.append(remote_ip)
|
||||
elif flaps > self.fast_flap_grace: count += 1
|
||||
|
||||
for remote_ip in expired:
|
||||
if remote_ip in BackboneInterface.fast_flapping:
|
||||
try:
|
||||
BackboneInterface.fast_flapping.pop(remote_ip)
|
||||
RNS.log(f"Fast-flapping block expired for {remote_ip}", RNS.LOG_DEBUG)
|
||||
except Exception as e: RNS.log(f"Error while expiring fast-flapping block for {remote_ip}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
return count
|
||||
|
||||
def __str__(self):
|
||||
if ":" in self.bind_ip: ip_str = f"[{self.bind_ip}]"
|
||||
else: ip_str = f"{self.bind_ip}"
|
||||
return "BackboneInterface["+self.name+"/"+ip_str+":"+str(self.bind_port)+"]"
|
||||
|
||||
|
||||
@@ -609,6 +670,7 @@ class BackboneClientInterface(Interface):
|
||||
if self.socket != None: self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
if str(e).endswith("Transport endpoint is not connected"): pass
|
||||
elif str(e).endswith("Bad file descriptor"): pass
|
||||
else: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
try:
|
||||
@@ -710,6 +772,14 @@ class BackboneClientInterface(Interface):
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
self.teardown()
|
||||
|
||||
def check_frame_len(self, frame_len):
|
||||
if frame_len <= RNS.Reticulum.HEADER_MINSIZE: return False
|
||||
elif frame_len > self.HW_MTU + (self.ifac_size or 0): return False
|
||||
else: return True
|
||||
|
||||
def invalid_frame(self, frame_len):
|
||||
RNS.log(f"Invalid HDLC frame of {RNS.prettysize(frame_len)} received on {self}, dropping frame", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
def receive(self, data_in):
|
||||
try:
|
||||
if len(data_in) > 0:
|
||||
@@ -723,12 +793,18 @@ class BackboneClientInterface(Interface):
|
||||
frame = self.frame_buffer[frame_start+1:frame_end]
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
|
||||
if len(frame) > RNS.Reticulum.HEADER_MINSIZE:
|
||||
self.process_incoming(frame)
|
||||
frame_len = len(frame)
|
||||
if frame_len != 0:
|
||||
if self.check_frame_len(frame_len): self.process_incoming(frame)
|
||||
else: self.invalid_frame(len(frame))
|
||||
|
||||
self.frame_buffer = self.frame_buffer[frame_end:]
|
||||
|
||||
else:
|
||||
if len(self.frame_buffer) > self.HW_MTU*2: self.frame_buffer = b""
|
||||
flags_remaining = False
|
||||
else:
|
||||
self.frame_buffer = b""
|
||||
flags_remaining = False
|
||||
|
||||
else:
|
||||
@@ -738,7 +814,7 @@ class BackboneClientInterface(Interface):
|
||||
def job(): self.reconnect()
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
else:
|
||||
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_DEBUG)
|
||||
RNS.log("The socket for remote client "+str(self)+" was closed.", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
|
||||
self.teardown()
|
||||
|
||||
except Exception as e:
|
||||
@@ -755,11 +831,29 @@ class BackboneClientInterface(Interface):
|
||||
def teardown(self):
|
||||
if self.initiator and not self.detached:
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
if RNS.Reticulum.panic_on_interface_error: RNS.panic()
|
||||
|
||||
else:
|
||||
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_VERBOSE)
|
||||
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_PATHING) if RNS.sl(RNS.LOG_PATHING) else None
|
||||
if self.parent_interface and self.parent_interface.block_fast_flapping and hasattr(self, "spawned_at"):
|
||||
connected_time = time.time() - self.spawned_at
|
||||
if connected_time < self.parent_interface.fast_flap_threshold:
|
||||
try:
|
||||
now = time.time()
|
||||
remote_id = f"{self.target_ip}"
|
||||
with BackboneInterface.fast_flapping_lock:
|
||||
if remote_id in BackboneInterface.fast_flapping: ffe = BackboneInterface.fast_flapping[remote_id]
|
||||
else: ffe = [now, now, 0]
|
||||
ffe[1] = now
|
||||
ffe[2] += 1
|
||||
with BackboneInterface.fast_flapping_lock: BackboneInterface.fast_flapping[remote_id] = ffe
|
||||
|
||||
dt = now-ffe[0]; fff = ffe[2]/dt if dt > 0 else None
|
||||
freq_str = f" at {RNS.prettyfrequency(fff)}" if fff else ""
|
||||
RNS.log(f"{self} is fast flapping{freq_str}, connection time was {RNS.prettytime(connected_time)}, {ffe[2]} fast flaps", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
if ffe[2] > self.parent_interface.fast_flap_grace: RNS.log(f"Ignoring further connections from {remote_id} due to fast-flapping", RNS.LOG_WARNING)
|
||||
|
||||
except Exception as e: RNS.log(f"Error while updating fast-flapping interface statistics: {e}", RNS.LOG_ERROR)
|
||||
|
||||
self.online = False
|
||||
self.OUT = False
|
||||
|
||||
+92
-224
@@ -88,8 +88,7 @@ class I2PController:
|
||||
self.ready = False
|
||||
|
||||
self.storagepath = rns_storagepath+"/i2p"
|
||||
if not os.path.isdir(self.storagepath):
|
||||
os.makedirs(self.storagepath)
|
||||
if not os.path.isdir(self.storagepath): os.makedirs(self.storagepath)
|
||||
|
||||
|
||||
def start(self):
|
||||
@@ -110,28 +109,22 @@ class I2PController:
|
||||
except Exception as e:
|
||||
self.ready = False
|
||||
RNS.log("Exception on event loop for "+str(self)+": "+str(e), RNS.LOG_ERROR)
|
||||
finally:
|
||||
self.loop.close()
|
||||
finally: self.loop.close()
|
||||
|
||||
|
||||
def stop(self):
|
||||
for i2ptunnel in self.i2plib_tunnels:
|
||||
if hasattr(i2ptunnel, "stop") and callable(i2ptunnel.stop):
|
||||
i2ptunnel.stop()
|
||||
if hasattr(i2ptunnel, "stop") and callable(i2ptunnel.stop): i2ptunnel.stop()
|
||||
|
||||
if hasattr(asyncio.Task, "all_tasks") and callable(asyncio.Task.all_tasks):
|
||||
for task in asyncio.Task.all_tasks(loop=self.loop):
|
||||
task.cancel()
|
||||
for task in asyncio.Task.all_tasks(loop=self.loop): task.cancel()
|
||||
|
||||
time.sleep(0.2)
|
||||
|
||||
self.loop.stop()
|
||||
|
||||
|
||||
def get_free_port(self):
|
||||
return self.i2plib.utils.get_free_port()
|
||||
|
||||
|
||||
def stop_tunnel(self, i2ptunnel):
|
||||
if hasattr(i2ptunnel, "stop") and callable(i2ptunnel.stop):
|
||||
i2ptunnel.stop()
|
||||
@@ -159,10 +152,9 @@ class I2PController:
|
||||
tn = self.i2plib_tunnels[i2p_destination]
|
||||
if tn != None and hasattr(tn, "status"):
|
||||
|
||||
RNS.log("Waiting for status from I2P control process", RNS.LOG_EXTREME)
|
||||
while not tn.status["setup_ran"]:
|
||||
time.sleep(0.1)
|
||||
RNS.log("Got status from I2P control process", RNS.LOG_EXTREME)
|
||||
RNS.log("Waiting for status from I2P control process", RNS.LOG_DEBUG)
|
||||
while not tn.status["setup_ran"]: time.sleep(0.1)
|
||||
RNS.log("Got status from I2P control process", RNS.LOG_DEBUG)
|
||||
|
||||
if tn.status["setup_failed"]:
|
||||
self.stop_tunnel(tn)
|
||||
@@ -172,29 +164,19 @@ class I2PController:
|
||||
if owner.socket != None:
|
||||
if hasattr(owner.socket, "close"):
|
||||
if callable(owner.socket.close):
|
||||
try:
|
||||
owner.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
RNS.log("Error while shutting down socket for "+str(owner)+": "+str(e))
|
||||
try: owner.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e: RNS.log("Error while shutting down socket for "+str(owner)+": "+str(e))
|
||||
try: owner.socket.close()
|
||||
except Exception as e: RNS.log("Error while closing socket for "+str(owner)+": "+str(e))
|
||||
|
||||
try:
|
||||
owner.socket.close()
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing socket for "+str(owner)+": "+str(e))
|
||||
self.client_tunnels[i2p_destination] = True
|
||||
owner.awaiting_i2p_tunnel = False
|
||||
|
||||
RNS.log(str(owner)+" tunnel setup complete", RNS.LOG_VERBOSE)
|
||||
|
||||
else:
|
||||
raise IOError("Got no status response from SAM API")
|
||||
|
||||
except ConnectionRefusedError as e:
|
||||
raise e
|
||||
|
||||
except ConnectionAbortedError as e:
|
||||
raise e
|
||||
else: raise IOError("Got no status response from SAM API")
|
||||
|
||||
except ConnectionRefusedError as e: raise e
|
||||
except ConnectionAbortedError as e: raise e
|
||||
except Exception as e:
|
||||
RNS.log("Unexpected error type from I2P SAM: "+str(e), RNS.LOG_ERROR)
|
||||
raise e
|
||||
@@ -206,54 +188,33 @@ class I2PController:
|
||||
|
||||
if i2ptunnel.status["setup_ran"] == False:
|
||||
RNS.log(str(self)+" I2P tunnel setup did not complete", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
elif i2p_exception != None:
|
||||
RNS.log("An error ocurred while setting up I2P tunnel to "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
if isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.CantReachPeer):
|
||||
RNS.log("The I2P daemon can't reach peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedDest):
|
||||
RNS.log("The I2P daemon reported that the destination is already in use", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedId):
|
||||
RNS.log("The I2P daemon reported that the ID is arleady in use", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidId):
|
||||
RNS.log("The I2P daemon reported that the stream session ID doesn't exist", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidKey):
|
||||
RNS.log("The I2P daemon reported that the key for "+str(i2p_destination)+" is invalid", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.KeyNotFound):
|
||||
RNS.log("The I2P daemon could not find the key for "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.PeerNotFound):
|
||||
RNS.log("The I2P daemon mould not find the peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.I2PError):
|
||||
RNS.log("The I2P daemon experienced an unspecified error", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.Timeout):
|
||||
RNS.log("I2P daemon timed out while setting up client tunnel to "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
if isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.CantReachPeer): RNS.log("The I2P daemon can't reach peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedDest): RNS.log("The I2P daemon reported that the destination is already in use", RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedId): RNS.log("The I2P daemon reported that the ID is arleady in use", RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidId): RNS.log("The I2P daemon reported that the stream session ID doesn't exist", RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidKey): RNS.log("The I2P daemon reported that the key for "+str(i2p_destination)+" is invalid", RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.KeyNotFound): RNS.log("The I2P daemon could not find the key for "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.PeerNotFound): RNS.log("The I2P daemon could not find the peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.I2PError): RNS.log("The I2P daemon experienced an unspecified error", RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.Timeout): RNS.log("I2P daemon timed out while setting up client tunnel to "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
else: RNS.log(f"Unspecified I2P daemon error: {i2p_exception}", RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Resetting I2P tunnel and retrying later", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
elif i2ptunnel.status["setup_failed"] == True:
|
||||
RNS.log(str(self)+" Unspecified I2P tunnel setup error, resetting I2P tunnel", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
else:
|
||||
RNS.log(str(self)+" Got no status from SAM API, resetting I2P tunnel", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
@@ -274,10 +235,8 @@ class I2PController:
|
||||
i2p_keyfile_nf = self.storagepath+"/"+RNS.hexrep(i2p_dest_hash_nf, delimit=False)+".i2p"
|
||||
|
||||
# Use old format if a key is already present
|
||||
if os.path.isfile(i2p_keyfile_of):
|
||||
i2p_keyfile = i2p_keyfile_of
|
||||
else:
|
||||
i2p_keyfile = i2p_keyfile_nf
|
||||
if os.path.isfile(i2p_keyfile_of): i2p_keyfile = i2p_keyfile_of
|
||||
else: i2p_keyfile = i2p_keyfile_nf
|
||||
|
||||
i2p_dest = None
|
||||
if not os.path.isfile(i2p_keyfile):
|
||||
@@ -312,8 +271,7 @@ class I2PController:
|
||||
asyncio.run_coroutine_threadsafe(tunnel_up(), self.loop).result()
|
||||
self.server_tunnels[i2p_b32] = True
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
except Exception as e: raise e
|
||||
|
||||
else:
|
||||
i2ptunnel = self.i2plib_tunnels[i2p_b32]
|
||||
@@ -322,66 +280,45 @@ class I2PController:
|
||||
|
||||
if i2ptunnel.status["setup_ran"] == False:
|
||||
RNS.log(str(self)+" I2P tunnel setup did not complete", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
elif i2p_exception != None:
|
||||
RNS.log("An error ocurred while setting up I2P tunnel", RNS.LOG_ERROR)
|
||||
|
||||
if isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.CantReachPeer):
|
||||
RNS.log("The I2P daemon can't reach peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedDest):
|
||||
RNS.log("The I2P daemon reported that the destination is already in use", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedId):
|
||||
RNS.log("The I2P daemon reported that the ID is arleady in use", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidId):
|
||||
RNS.log("The I2P daemon reported that the stream session ID doesn't exist", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidKey):
|
||||
RNS.log("The I2P daemon reported that the key for "+str(i2p_destination)+" is invalid", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.KeyNotFound):
|
||||
RNS.log("The I2P daemon could not find the key for "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.PeerNotFound):
|
||||
RNS.log("The I2P daemon mould not find the peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.I2PError):
|
||||
RNS.log("The I2P daemon experienced an unspecified error", RNS.LOG_ERROR)
|
||||
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.Timeout):
|
||||
RNS.log("I2P daemon timed out while setting up client tunnel to "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
if isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.CantReachPeer): RNS.log("The I2P daemon can't reach peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedDest): RNS.log("The I2P daemon reported that the destination is already in use", RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.DuplicatedId): RNS.log("The I2P daemon reported that the ID is arleady in use", RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidId): RNS.log("The I2P daemon reported that the stream session ID doesn't exist", RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.InvalidKey): RNS.log("The I2P daemon reported that the key for "+str(i2p_destination)+" is invalid", RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.KeyNotFound): RNS.log("The I2P daemon could not find the key for "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.PeerNotFound): RNS.log("The I2P daemon could not find the peer "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.I2PError): RNS.log("The I2P daemon experienced an unspecified error", RNS.LOG_ERROR)
|
||||
elif isinstance(i2p_exception, RNS.vendor.i2plib.exceptions.Timeout): RNS.log("I2P daemon timed out while setting up client tunnel to "+str(i2p_destination), RNS.LOG_ERROR)
|
||||
else: RNS.log(f"Unspecified I2P daemon error: {i2p_exception}", RNS.LOG_ERROR)
|
||||
|
||||
RNS.log("Resetting I2P tunnel and retrying later", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
elif i2ptunnel.status["setup_failed"] == True:
|
||||
RNS.log(str(self)+" Unspecified I2P tunnel setup error, resetting I2P tunnel", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
else:
|
||||
RNS.log(str(self)+" Got no status from SAM API, resetting I2P tunnel", RNS.LOG_ERROR)
|
||||
|
||||
self.stop_tunnel(i2ptunnel)
|
||||
return False
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
def get_loop(self):
|
||||
return asyncio.get_event_loop()
|
||||
def get_loop(self): return asyncio.get_event_loop()
|
||||
|
||||
|
||||
class ThreadingI2PServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
pass
|
||||
|
||||
|
||||
class I2PInterfacePeer(Interface):
|
||||
RECONNECT_WAIT = 15
|
||||
RECONNECT_MAX_TRIES = None
|
||||
@@ -431,18 +368,11 @@ class I2PInterfacePeer(Interface):
|
||||
self.ifac_netkey = self.parent_interface.ifac_netkey
|
||||
if self.ifac_netname != None or self.ifac_netkey != None:
|
||||
ifac_origin = b""
|
||||
if self.ifac_netname != None:
|
||||
ifac_origin += RNS.Identity.full_hash(self.ifac_netname.encode("utf-8"))
|
||||
if self.ifac_netkey != None:
|
||||
ifac_origin += RNS.Identity.full_hash(self.ifac_netkey.encode("utf-8"))
|
||||
if self.ifac_netname != None: ifac_origin += RNS.Identity.full_hash(self.ifac_netname.encode("utf-8"))
|
||||
if self.ifac_netkey != None: ifac_origin += RNS.Identity.full_hash(self.ifac_netkey.encode("utf-8"))
|
||||
|
||||
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
|
||||
self.ifac_key = RNS.Cryptography.hkdf(
|
||||
length=64,
|
||||
derive_from=ifac_origin_hash,
|
||||
salt=RNS.Reticulum.IFAC_SALT,
|
||||
context=None
|
||||
)
|
||||
self.ifac_key = RNS.Cryptography.hkdf(length=64, derive_from=ifac_origin_hash, salt=RNS.Reticulum.IFAC_SALT, context=None)
|
||||
self.ifac_identity = RNS.Identity.from_bytes(self.ifac_key)
|
||||
self.ifac_signature = self.ifac_identity.sign(RNS.Identity.full_hash(self.ifac_key))
|
||||
|
||||
@@ -450,10 +380,8 @@ class I2PInterfacePeer(Interface):
|
||||
self.announce_rate_grace = None
|
||||
self.announce_rate_penalty = None
|
||||
|
||||
if max_reconnect_tries == None:
|
||||
self.max_reconnect_tries = I2PInterfacePeer.RECONNECT_MAX_TRIES
|
||||
else:
|
||||
self.max_reconnect_tries = max_reconnect_tries
|
||||
if max_reconnect_tries == None: self.max_reconnect_tries = I2PInterfacePeer.RECONNECT_MAX_TRIES
|
||||
else: self.max_reconnect_tries = max_reconnect_tries
|
||||
|
||||
if connected_socket != None:
|
||||
self.receives = True
|
||||
@@ -461,15 +389,12 @@ class I2PInterfacePeer(Interface):
|
||||
self.target_port = None
|
||||
self.socket = connected_socket
|
||||
|
||||
if platform.system() == "Linux":
|
||||
self.set_timeouts_linux()
|
||||
elif platform.system() == "Darwin":
|
||||
self.set_timeouts_osx()
|
||||
if platform.system() == "Linux": self.set_timeouts_linux()
|
||||
elif platform.system() == "Darwin": self.set_timeouts_osx()
|
||||
|
||||
elif target_i2p_dest != None:
|
||||
self.receives = True
|
||||
self.initiator = True
|
||||
|
||||
self.bind_ip = "127.0.0.1"
|
||||
|
||||
self.awaiting_i2p_tunnel = True
|
||||
@@ -497,12 +422,10 @@ class I2PInterfacePeer(Interface):
|
||||
thread.start()
|
||||
|
||||
def wait_job():
|
||||
while self.awaiting_i2p_tunnel:
|
||||
time.sleep(0.25)
|
||||
while self.awaiting_i2p_tunnel: time.sleep(0.25)
|
||||
time.sleep(2)
|
||||
|
||||
if not self.kiss_framing:
|
||||
self.wants_tunnel = True
|
||||
if not self.kiss_framing: self.wants_tunnel = True
|
||||
|
||||
if not self.connect(initial=True):
|
||||
thread = threading.Thread(target=self.reconnect)
|
||||
@@ -517,7 +440,6 @@ class I2PInterfacePeer(Interface):
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
|
||||
def set_timeouts_linux(self):
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(I2PInterfacePeer.I2P_USER_TIMEOUT * 1000))
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
@@ -526,10 +448,8 @@ class I2PInterfacePeer(Interface):
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(I2PInterfacePeer.I2P_PROBES))
|
||||
|
||||
def set_timeouts_osx(self):
|
||||
if hasattr(socket, "TCP_KEEPALIVE"):
|
||||
TCP_KEEPIDLE = socket.TCP_KEEPALIVE
|
||||
else:
|
||||
TCP_KEEPIDLE = 0x10
|
||||
if hasattr(socket, "TCP_KEEPALIVE"): TCP_KEEPIDLE = socket.TCP_KEEPALIVE
|
||||
else: TCP_KEEPIDLE = 0x10
|
||||
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(I2PInterfacePeer.I2P_PROBE_AFTER))
|
||||
@@ -537,16 +457,12 @@ class I2PInterfacePeer(Interface):
|
||||
def shutdown_socket(self, target_socket):
|
||||
if callable(target_socket.close):
|
||||
try:
|
||||
if socket != None:
|
||||
target_socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
|
||||
if socket != None: target_socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
|
||||
|
||||
try:
|
||||
if socket != None:
|
||||
target_socket.close()
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
|
||||
if socket != None: target_socket.close()
|
||||
except Exception as e: RNS.log("Error while closing socket for "+str(self)+": "+str(e))
|
||||
|
||||
def detach(self):
|
||||
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
|
||||
@@ -555,15 +471,11 @@ class I2PInterfacePeer(Interface):
|
||||
if callable(self.socket.close):
|
||||
self.detached = True
|
||||
|
||||
try:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
|
||||
try: self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
|
||||
|
||||
try:
|
||||
self.socket.close()
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
|
||||
try: self.socket.close()
|
||||
except Exception as e: RNS.log("Error while closing socket for "+str(self)+": "+str(e))
|
||||
|
||||
self.socket = None
|
||||
|
||||
@@ -578,23 +490,17 @@ class I2PInterfacePeer(Interface):
|
||||
if not self.awaiting_i2p_tunnel:
|
||||
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(I2PInterfacePeer.RECONNECT_WAIT)+" seconds.", RNS.LOG_ERROR)
|
||||
|
||||
return False
|
||||
else: raise e
|
||||
|
||||
else:
|
||||
raise e
|
||||
|
||||
if platform.system() == "Linux":
|
||||
self.set_timeouts_linux()
|
||||
elif platform.system() == "Darwin":
|
||||
self.set_timeouts_osx()
|
||||
if platform.system() == "Linux": self.set_timeouts_linux()
|
||||
elif platform.system() == "Darwin": self.set_timeouts_osx()
|
||||
|
||||
self.online = True
|
||||
self.writing = False
|
||||
self.never_connected = False
|
||||
|
||||
if not self.kiss_framing and self.wants_tunnel:
|
||||
RNS.Transport.synthesize_tunnel(self)
|
||||
if not self.kiss_framing and self.wants_tunnel: RNS.Transport.synthesize_tunnel(self)
|
||||
|
||||
return True
|
||||
|
||||
@@ -612,14 +518,10 @@ class I2PInterfacePeer(Interface):
|
||||
self.teardown()
|
||||
break
|
||||
|
||||
try:
|
||||
self.connect()
|
||||
|
||||
try: self.connect()
|
||||
except Exception as e:
|
||||
if not self.awaiting_i2p_tunnel:
|
||||
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
else:
|
||||
RNS.log(str(self)+" still waiting for I2P tunnel to appear", RNS.LOG_VERBOSE)
|
||||
if not self.awaiting_i2p_tunnel: RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
else: RNS.log(str(self)+" still waiting for I2P tunnel to appear", RNS.LOG_VERBOSE)
|
||||
|
||||
if not self.never_connected:
|
||||
RNS.log(str(self)+" Re-established connection via I2P tunnel", RNS.LOG_INFO)
|
||||
@@ -628,8 +530,7 @@ class I2PInterfacePeer(Interface):
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
if not self.kiss_framing:
|
||||
RNS.Transport.synthesize_tunnel(self)
|
||||
if not self.kiss_framing: RNS.Transport.synthesize_tunnel(self)
|
||||
|
||||
else:
|
||||
RNS.log("Attempt to reconnect on a non-initiator I2P interface. This should not happen.", RNS.LOG_ERROR)
|
||||
@@ -639,29 +540,24 @@ class I2PInterfacePeer(Interface):
|
||||
self.rxb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count:
|
||||
self.parent_interface.rxb += len(data)
|
||||
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
def process_outgoing(self, data):
|
||||
if self.online:
|
||||
while self.writing:
|
||||
time.sleep(0.001)
|
||||
while self.writing: time.sleep(0.001)
|
||||
|
||||
try:
|
||||
self.writing = True
|
||||
|
||||
if self.kiss_framing:
|
||||
data = bytes([KISS.FEND])+bytes([KISS.CMD_DATA])+KISS.escape(data)+bytes([KISS.FEND])
|
||||
else:
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
if self.kiss_framing: data = bytes([KISS.FEND])+bytes([KISS.CMD_DATA])+KISS.escape(data)+bytes([KISS.FEND])
|
||||
else: data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
|
||||
self.socket.sendall(data)
|
||||
self.writing = False
|
||||
self.txb += len(data)
|
||||
self.last_write = time.time()
|
||||
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count:
|
||||
self.parent_interface.txb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None and self.parent_count: self.parent_interface.txb += len(data)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
|
||||
@@ -670,8 +566,7 @@ class I2PInterfacePeer(Interface):
|
||||
|
||||
|
||||
def read_watchdog(self):
|
||||
while self.wd_reset:
|
||||
time.sleep(0.25)
|
||||
while self.wd_reset: time.sleep(0.25)
|
||||
|
||||
should_run = True
|
||||
try:
|
||||
@@ -679,17 +574,13 @@ class I2PInterfacePeer(Interface):
|
||||
time.sleep(1)
|
||||
|
||||
if (time.time()-self.last_read > I2PInterfacePeer.I2P_PROBE_AFTER*2):
|
||||
if self.i2p_tunnel_state != I2PInterfacePeer.TUNNEL_STATE_STALE:
|
||||
RNS.log("I2P tunnel became unresponsive", RNS.LOG_DEBUG)
|
||||
|
||||
if self.i2p_tunnel_state != I2PInterfacePeer.TUNNEL_STATE_STALE: RNS.log("I2P tunnel became unresponsive", RNS.LOG_DEBUG)
|
||||
self.i2p_tunnel_state = I2PInterfacePeer.TUNNEL_STATE_STALE
|
||||
else:
|
||||
self.i2p_tunnel_state = I2PInterfacePeer.TUNNEL_STATE_ACTIVE
|
||||
else: self.i2p_tunnel_state = I2PInterfacePeer.TUNNEL_STATE_ACTIVE
|
||||
|
||||
if (time.time()-self.last_write > I2PInterfacePeer.I2P_PROBE_AFTER*1):
|
||||
try:
|
||||
if self.socket != None:
|
||||
self.socket.sendall(bytes([HDLC.FLAG, HDLC.FLAG]))
|
||||
if self.socket != None: self.socket.sendall(bytes([HDLC.FLAG, HDLC.FLAG]))
|
||||
except Exception as e:
|
||||
RNS.log("An error ocurred while sending I2P keepalive. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
self.shutdown_socket(self.socket)
|
||||
@@ -698,22 +589,17 @@ class I2PInterfacePeer(Interface):
|
||||
if (time.time()-self.last_read > I2PInterfacePeer.I2P_READ_TIMEOUT):
|
||||
RNS.log("I2P socket is unresponsive, restarting...", RNS.LOG_WARNING)
|
||||
if self.socket != None:
|
||||
try:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
|
||||
try: self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e))
|
||||
|
||||
try:
|
||||
self.socket.close()
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing socket for "+str(self)+": "+str(e))
|
||||
try: self.socket.close()
|
||||
except Exception as e: RNS.log("Error while closing socket for "+str(self)+": "+str(e))
|
||||
|
||||
should_run = False
|
||||
|
||||
self.wd_reset = False
|
||||
|
||||
finally:
|
||||
self.wd_reset = False
|
||||
finally: self.wd_reset = False
|
||||
|
||||
def read_loop(self):
|
||||
try:
|
||||
@@ -784,7 +670,6 @@ class I2PInterfacePeer(Interface):
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
else:
|
||||
self.online = False
|
||||
|
||||
self.wd_reset = True
|
||||
time.sleep(2)
|
||||
self.wd_reset = False
|
||||
@@ -806,15 +691,12 @@ class I2PInterfacePeer(Interface):
|
||||
if self.initiator:
|
||||
RNS.log("Attempting to reconnect...", RNS.LOG_WARNING)
|
||||
self.reconnect()
|
||||
else:
|
||||
self.teardown()
|
||||
else: self.teardown()
|
||||
|
||||
def teardown(self):
|
||||
if self.initiator and not self.detached:
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down. Restart Reticulum to attempt to open this interface again.", RNS.LOG_ERROR)
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
RNS.panic()
|
||||
|
||||
if RNS.Reticulum.panic_on_interface_error: RNS.panic()
|
||||
else:
|
||||
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_VERBOSE)
|
||||
|
||||
@@ -826,9 +708,7 @@ class I2PInterfacePeer(Interface):
|
||||
while self in self.parent_interface.spawned_interfaces:
|
||||
self.parent_interface.spawned_interfaces.remove(self)
|
||||
|
||||
if not self.initiator:
|
||||
RNS.Transport.remove_interface(self)
|
||||
|
||||
if not self.initiator: RNS.Transport.remove_interface(self)
|
||||
|
||||
def __str__(self):
|
||||
return "I2PInterfacePeer["+str(self.name)+"]"
|
||||
@@ -839,8 +719,7 @@ class I2PInterface(Interface):
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
|
||||
@property
|
||||
def clients(self):
|
||||
return len(self.spawned_interfaces)
|
||||
def clients(self): return len(self.spawned_interfaces)
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
super().__init__()
|
||||
@@ -894,11 +773,9 @@ class I2PInterface(Interface):
|
||||
RNS.log("I2P controller did not become available in time, waiting for controller", RNS.LOG_VERBOSE)
|
||||
i2p_notready_warning = True
|
||||
|
||||
while not self.i2p.ready:
|
||||
time.sleep(0.25)
|
||||
while not self.i2p.ready: time.sleep(0.25)
|
||||
|
||||
if i2p_notready_warning == True:
|
||||
RNS.log("I2P controller ready, continuing setup", RNS.LOG_VERBOSE)
|
||||
if i2p_notready_warning == True: RNS.log("I2P controller ready, continuing setup", RNS.LOG_VERBOSE)
|
||||
|
||||
def handlerFactory(callback):
|
||||
def createHandler(*args, **keys):
|
||||
@@ -971,18 +848,11 @@ class I2PInterface(Interface):
|
||||
spawned_interface.ifac_netkey = self.ifac_netkey
|
||||
if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
|
||||
ifac_origin = b""
|
||||
if spawned_interface.ifac_netname != None:
|
||||
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
|
||||
if spawned_interface.ifac_netkey != None:
|
||||
ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
|
||||
if spawned_interface.ifac_netname != None: ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
|
||||
if spawned_interface.ifac_netkey != None: ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
|
||||
|
||||
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
|
||||
spawned_interface.ifac_key = RNS.Cryptography.hkdf(
|
||||
length=64,
|
||||
derive_from=ifac_origin_hash,
|
||||
salt=RNS.Reticulum.IFAC_SALT,
|
||||
context=None
|
||||
)
|
||||
spawned_interface.ifac_key = RNS.Cryptography.hkdf(length=64, derive_from=ifac_origin_hash, salt=RNS.Reticulum.IFAC_SALT, context=None)
|
||||
spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
|
||||
spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
|
||||
|
||||
@@ -990,11 +860,11 @@ class I2PInterface(Interface):
|
||||
spawned_interface.announce_rate_grace = self.announce_rate_grace
|
||||
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.gravity = self.gravity
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
RNS.log("Spawned new I2PInterface Peer: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
while spawned_interface in self.spawned_interfaces:
|
||||
self.spawned_interfaces.remove(spawned_interface)
|
||||
while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface)
|
||||
self.spawned_interfaces.append(spawned_interface)
|
||||
spawned_interface.read_loop()
|
||||
|
||||
@@ -1017,13 +887,11 @@ class I2PInterface(Interface):
|
||||
RNS.log("Detaching "+str(self), RNS.LOG_DEBUG)
|
||||
self.i2p.stop()
|
||||
|
||||
def __str__(self):
|
||||
return "I2PInterface["+self.name+"]"
|
||||
def __str__(self): return "I2PInterface["+self.name+"]"
|
||||
|
||||
class I2PInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, callback, *args, **keys):
|
||||
self.callback = callback
|
||||
socketserver.BaseRequestHandler.__init__(self, *args, **keys)
|
||||
|
||||
def handle(self):
|
||||
self.callback(handler=self)
|
||||
def handle(self): self.callback(handler=self)
|
||||
|
||||
+30
-34
@@ -48,10 +48,12 @@ class Interface:
|
||||
MODE_ROAMING = 0x04
|
||||
MODE_BOUNDARY = 0x05
|
||||
MODE_GATEWAY = 0x06
|
||||
MODE_INTERNAL = 0x07
|
||||
|
||||
# Which interface modes a Transport Node should
|
||||
# actively discover paths for.
|
||||
DISCOVER_PATHS_FOR = [MODE_ACCESS_POINT, MODE_GATEWAY, MODE_ROAMING]
|
||||
DISCOVER_PATHS_FOR = [MODE_ACCESS_POINT, MODE_GATEWAY, MODE_ROAMING, MODE_INTERNAL]
|
||||
BOUNDARY_SEARCH_MODES = [MODE_BOUNDARY, MODE_GATEWAY]
|
||||
|
||||
# How many samples to use for announce
|
||||
# frequency calculations
|
||||
@@ -69,9 +71,8 @@ class Interface:
|
||||
# to hold at any given time.
|
||||
MAX_HELD_ANNOUNCES = 256
|
||||
|
||||
# How long a spawned interface will be
|
||||
# considered to be newly created. Two
|
||||
# hours by default.
|
||||
# Control parameters
|
||||
DEFAULT_GRAVITY = 0
|
||||
IC_NEW_TIME = 2*60*60
|
||||
IC_BURST_FREQ_NEW = 3
|
||||
IC_BURST_FREQ = 10
|
||||
@@ -96,16 +97,21 @@ class Interface:
|
||||
def __init__(self):
|
||||
self.rxb = 0
|
||||
self.txb = 0
|
||||
self.gravity = 0
|
||||
self.created = time.time()
|
||||
self.detached = False
|
||||
self.online = False
|
||||
self.bitrate = 62500
|
||||
self.HW_MTU = None
|
||||
self.__hash = None
|
||||
|
||||
self.supports_discovery = False
|
||||
self.discoverable = False
|
||||
self.last_discovery_announce = 0
|
||||
self.bootstrap_only = False
|
||||
self.recursive_prs = False
|
||||
self.announces_from_internal = True
|
||||
self.announces_to_internal = None
|
||||
self.parent_interface = None
|
||||
self.spawned_interfaces = None
|
||||
self.tunnel_id = None
|
||||
@@ -136,7 +142,8 @@ class Interface:
|
||||
self.op_freq_deque = deque(maxlen=Interface.OA_FREQ_SAMPLES)
|
||||
|
||||
def get_hash(self):
|
||||
return RNS.Identity.full_hash(str(self).encode("utf-8"))
|
||||
if not self.__hash: self.__hash = RNS.Identity.full_hash(str(self).encode("utf-8"))
|
||||
return self.__hash
|
||||
|
||||
# This is a generic function for determining when an interface
|
||||
# should activate ingress limiting. Since this can vary for
|
||||
@@ -149,7 +156,7 @@ class Interface:
|
||||
|
||||
if self.ic_burst_active:
|
||||
if ia_freq < freq_threshold and time.time() > self.ic_burst_activated+self.ic_burst_hold:
|
||||
if len(self.ia_freq_deque) >= self.IC_BURST_MIN_SAMPLES: self.ic_burst_active = False
|
||||
if len(self.ia_freq_deque) >= self.IC_DEQUE_MIN_SAMPLE: self.ic_burst_active = False
|
||||
|
||||
return True
|
||||
|
||||
@@ -197,36 +204,27 @@ class Interface:
|
||||
|
||||
def optimise_mtu(self):
|
||||
if self.AUTOCONFIGURE_MTU:
|
||||
if self.bitrate >= 1_000_000_000:
|
||||
self.HW_MTU = 524288
|
||||
elif self.bitrate > 750_000_000:
|
||||
self.HW_MTU = 262144
|
||||
elif self.bitrate > 400_000_000:
|
||||
self.HW_MTU = 131072
|
||||
elif self.bitrate > 200_000_000:
|
||||
self.HW_MTU = 65536
|
||||
elif self.bitrate > 100_000_000:
|
||||
self.HW_MTU = 32768
|
||||
elif self.bitrate > 10_000_000:
|
||||
self.HW_MTU = 16384
|
||||
elif self.bitrate > 5_000_000:
|
||||
self.HW_MTU = 8192
|
||||
elif self.bitrate > 2_000_000:
|
||||
self.HW_MTU = 4096
|
||||
elif self.bitrate > 1_000_000:
|
||||
self.HW_MTU = 2048
|
||||
elif self.bitrate > 62_500:
|
||||
self.HW_MTU = 1024
|
||||
else:
|
||||
self.HW_MTU = None
|
||||
if self.bitrate >= 1_000_000_000: self.HW_MTU = 524288
|
||||
elif self.bitrate > 750_000_000: self.HW_MTU = 262144
|
||||
elif self.bitrate > 400_000_000: self.HW_MTU = 131072
|
||||
elif self.bitrate > 200_000_000: self.HW_MTU = 65536
|
||||
elif self.bitrate > 100_000_000: self.HW_MTU = 32768
|
||||
elif self.bitrate > 10_000_000: self.HW_MTU = 16384
|
||||
elif self.bitrate > 5_000_000: self.HW_MTU = 8192
|
||||
elif self.bitrate > 2_000_000: self.HW_MTU = 4096
|
||||
elif self.bitrate > 1_000_000: self.HW_MTU = 2048
|
||||
elif self.bitrate > 62_500: self.HW_MTU = 1024
|
||||
else: self.HW_MTU = None
|
||||
|
||||
RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_DEBUG)
|
||||
RNS.log(f"{self} hardware MTU set to {self.HW_MTU}", RNS.LOG_PATHING)
|
||||
|
||||
def age(self):
|
||||
return time.time()-self.created
|
||||
|
||||
def hold_announce(self, announce_packet):
|
||||
if announce_packet.destination_hash in self.held_announces:
|
||||
if announce_packet.hops >= RNS.Transport.PATHFINDER_M-1:
|
||||
return
|
||||
elif announce_packet.destination_hash in self.held_announces:
|
||||
self.held_announces[announce_packet.destination_hash] = announce_packet
|
||||
elif not len(self.held_announces) >= self.ic_max_held_announces:
|
||||
self.held_announces[announce_packet.destination_hash] = announce_packet
|
||||
@@ -370,11 +368,9 @@ class Interface:
|
||||
|
||||
@staticmethod
|
||||
def get_config_obj(config_in):
|
||||
if type(config_in) == ConfigObj:
|
||||
return config_in
|
||||
if type(config_in) == ConfigObj: return config_in
|
||||
else:
|
||||
try:
|
||||
return ConfigObj(config_in)
|
||||
try: return ConfigObj(config_in)
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not parse supplied configuration data. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
raise SystemError("Invalid configuration data supplied")
|
||||
@@ -333,6 +333,13 @@ class TCPClientInterface(Interface):
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
self.teardown()
|
||||
|
||||
def check_frame_len(self, frame_len):
|
||||
if frame_len <= RNS.Reticulum.HEADER_MINSIZE: return False
|
||||
elif frame_len > self.HW_MTU + (self.ifac_size or 0): return False
|
||||
else: return True
|
||||
|
||||
def invalid_frame(self, frame_len):
|
||||
RNS.log(f"Invalid HDLC frame of {RNS.prettysize(frame_len)} received on {self}, dropping frame", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
def read_loop(self):
|
||||
try:
|
||||
@@ -389,12 +396,18 @@ class TCPClientInterface(Interface):
|
||||
frame = frame_buffer[frame_start+1:frame_end]
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.FLAG ^ HDLC.ESC_MASK]), bytes([HDLC.FLAG]))
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
|
||||
if len(frame) > RNS.Reticulum.HEADER_MINSIZE:
|
||||
self.process_incoming(frame)
|
||||
frame_len = len(frame)
|
||||
if frame_len != 0:
|
||||
if self.check_frame_len(frame_len): self.process_incoming(frame)
|
||||
else: self.invalid_frame(len(frame))
|
||||
|
||||
frame_buffer = frame_buffer[frame_end:]
|
||||
|
||||
else:
|
||||
if len(frame_buffer) > self.HW_MTU*2: frame_buffer = b""
|
||||
flags_remaining = False
|
||||
else:
|
||||
frame_buffer = b""
|
||||
flags_remaining = False
|
||||
|
||||
else:
|
||||
@@ -623,6 +636,7 @@ class TCPServerInterface(Interface):
|
||||
spawned_interface.announce_rate_grace = self.announce_rate_grace
|
||||
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.gravity = self.gravity
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.log("Spawned new TCPClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
|
||||
@@ -131,8 +131,8 @@ class WDCL():
|
||||
self.wdcl_connected = False
|
||||
self.reconnecting = False
|
||||
self.frame_queue = deque()
|
||||
if not self.as_interface:
|
||||
self.id = RNS.Identity.full_hash(port.hwid.encode("utf-8"))
|
||||
|
||||
if not self.as_interface: self.id = RNS.Identity.full_hash(port.hwid.encode("utf-8"))
|
||||
|
||||
if self.as_interface:
|
||||
try:
|
||||
@@ -168,19 +168,10 @@ class WDCL():
|
||||
self.owner.wlog(f"Opening serial port {self.port.device}...")
|
||||
target_port = self.port.device
|
||||
|
||||
self.serial = self.pyserial.Serial(
|
||||
port = target_port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = 0.250,
|
||||
inter_byte_timeout = None,
|
||||
write_timeout = None,
|
||||
dsrdtr = False)
|
||||
|
||||
self.serial = self.pyserial.Serial(port = target_port, baudrate = self.speed,
|
||||
bytesize = self.databits, parity = self.parity, stopbits = self.stopbits,
|
||||
xonxoff = False, rtscts = False, timeout = 0.250, inter_byte_timeout = None,
|
||||
write_timeout = None, dsrdtr = False)
|
||||
else:
|
||||
if self.port != None:
|
||||
# Get device parameters
|
||||
@@ -198,19 +189,9 @@ class WDCL():
|
||||
from usbserial4a.cdcacmserial4a import CdcAcmSerial
|
||||
proxy = CdcAcmSerial
|
||||
|
||||
self.serial = proxy(
|
||||
self.port,
|
||||
baudrate = self.speed,
|
||||
bytesize = self.databits,
|
||||
parity = self.parity,
|
||||
stopbits = self.stopbits,
|
||||
xonxoff = False,
|
||||
rtscts = False,
|
||||
timeout = None,
|
||||
inter_byte_timeout = None,
|
||||
# write_timeout = wtimeout,
|
||||
dsrdtr = False,
|
||||
)
|
||||
self.serial = proxy(self.port, baudrate = self.speed, bytesize = self.databits,
|
||||
parity = self.parity, stopbits = self.stopbits, xonxoff = False,
|
||||
rtscts = False, timeout = None, inter_byte_timeout = None, dsrdtr = False)
|
||||
|
||||
if vid == 0x0403:
|
||||
# Hardware parameters for FTDI devices @ 115200 baud
|
||||
@@ -299,10 +280,9 @@ class WDCL():
|
||||
frame = frame.replace(bytes([HDLC.ESC, HDLC.ESC ^ HDLC.ESC_MASK]), bytes([HDLC.ESC]))
|
||||
if len(frame) > WDCL.HEADER_MINSIZE: self.process_incoming(frame)
|
||||
self.frame_buffer = self.frame_buffer[frame_end:]
|
||||
else:
|
||||
flags_remaining = False
|
||||
else:
|
||||
flags_remaining = False
|
||||
|
||||
else: flags_remaining = False
|
||||
else: flags_remaining = False
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
@@ -359,6 +339,7 @@ class Evt():
|
||||
ET_MSG = 0x0000
|
||||
ET_SYSTEM_BOOT = 0x0001
|
||||
ET_CORE_INIT = 0x0002
|
||||
ET_BOARD_INIT = 0x0003
|
||||
ET_DRV_UART_INIT = 0x1000
|
||||
ET_DRV_USB_CDC_INIT = 0x1010
|
||||
ET_DRV_USB_CDC_HOST_AVAIL = 0x1011
|
||||
@@ -428,6 +409,7 @@ class Evt():
|
||||
event_descriptions = {
|
||||
ET_SYSTEM_BOOT: "System boot",
|
||||
ET_CORE_INIT: "Core initialization",
|
||||
ET_BOARD_INIT: "Board hardware initialization",
|
||||
ET_DRV_UART_INIT: "UART driver initialization",
|
||||
ET_DRV_USB_CDC_INIT: "USB CDC driver initialization",
|
||||
ET_DRV_USB_CDC_HOST_AVAIL: "USB CDC host became available",
|
||||
@@ -979,6 +961,7 @@ class WeaveInterface(Interface):
|
||||
spawned_interface.announce_rate_grace = self.announce_rate_grace
|
||||
spawned_interface.announce_rate_penalty = self.announce_rate_penalty
|
||||
spawned_interface.mode = self.mode
|
||||
spawned_interface.gravity = self.gravity
|
||||
spawned_interface.HW_MTU = self.HW_MTU
|
||||
spawned_interface._online = True
|
||||
RNS.Transport.add_interface(spawned_interface)
|
||||
|
||||
+139
-177
@@ -190,25 +190,22 @@ class Link:
|
||||
link.set_link_id(packet)
|
||||
|
||||
if len(data) == Link.ECPUBSIZE+Link.LINK_MTU_SIZE:
|
||||
RNS.log("Link request includes MTU signalling", RNS.LOG_DEBUG) # TODO: Remove debug
|
||||
try:
|
||||
link.mtu = Link.mtu_from_lr_packet(packet) or Reticulum.MTU
|
||||
RNS.log("Link request includes MTU signalling", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
try: link.mtu = Link.mtu_from_lr_packet(packet) or Reticulum.MTU
|
||||
except Exception as e:
|
||||
RNS.trace_exception(e)
|
||||
link.mtu = RNS.Reticulum.MTU
|
||||
|
||||
link.mode = Link.mode_from_lr_packet(packet)
|
||||
|
||||
# TODO: Remove debug
|
||||
RNS.log(f"Incoming link request with mode {Link.MODE_DESCRIPTIONS[link.mode]}", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Incoming link request with mode {Link.MODE_DESCRIPTIONS[link.mode]}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
link.update_mdu()
|
||||
link.destination = packet.destination
|
||||
link.establishment_timeout = Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, packet.hops) + Link.KEEPALIVE
|
||||
link.establishment_cost += len(packet.raw)
|
||||
RNS.log(f"Validating link request {RNS.prettyhexrep(link.link_id)}", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Link MTU configured to {RNS.prettysize(link.mtu)}", RNS.LOG_EXTREME)
|
||||
RNS.log(f"Establishment timeout is {RNS.prettytime(link.establishment_timeout)} for incoming link request "+RNS.prettyhexrep(link.link_id), RNS.LOG_EXTREME)
|
||||
RNS.log(f"Validating link request {RNS.prettyhexrep(link.link_id)}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
RNS.log(f"Link MTU configured to {RNS.prettysize(link.mtu)}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
RNS.log(f"Establishment timeout is {RNS.prettytime(link.establishment_timeout)} for incoming link request {RNS.prettyhexrep(link.link_id)}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
link.handshake()
|
||||
link.attached_interface = packet.receiving_interface
|
||||
link.prove()
|
||||
@@ -218,15 +215,15 @@ class Link:
|
||||
link.__update_phy_stats(packet, force_update=True)
|
||||
link.start_watchdog()
|
||||
|
||||
RNS.log("Incoming link request "+str(link)+" accepted on "+str(link.attached_interface), RNS.LOG_DEBUG)
|
||||
RNS.log(f"Incoming link request {link} accepted on {link.attached_interface}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return link
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Validating link request failed: {e}", RNS.LOG_VERBOSE)
|
||||
RNS.log(f"Validating link request failed: {e}", RNS.LOG_VERBOSE) if RNS.sl(RNS.LOG_VERBOSE) else None
|
||||
return None
|
||||
|
||||
else:
|
||||
RNS.log(f"Invalid link request payload size of {len(data)} bytes, dropping request", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Invalid link request payload size of {len(data)} bytes, dropping request", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return None
|
||||
|
||||
|
||||
@@ -268,6 +265,7 @@ class Link:
|
||||
self.owner = owner
|
||||
self.destination = destination
|
||||
self.expected_hops = None
|
||||
self.rebalanced = None
|
||||
self.attached_interface = None
|
||||
self.__remote_identity = None
|
||||
self.__track_phy_stats = False
|
||||
@@ -277,6 +275,7 @@ class Link:
|
||||
self.initiator = False
|
||||
self.prv = X25519PrivateKey.generate()
|
||||
self.sig_prv = self.owner.identity.sig_prv
|
||||
|
||||
else:
|
||||
self.initiator = True
|
||||
self.expected_hops = RNS.Transport.hops_to(self.destination.hash)
|
||||
@@ -296,14 +295,11 @@ class Link:
|
||||
if peer_pub_bytes == None:
|
||||
self.peer_pub = None
|
||||
self.peer_pub_bytes = None
|
||||
else:
|
||||
self.load_peer(peer_pub_bytes, peer_sig_pub_bytes)
|
||||
|
||||
if established_callback != None:
|
||||
self.set_link_established_callback(established_callback)
|
||||
else: self.load_peer(peer_pub_bytes, peer_sig_pub_bytes)
|
||||
|
||||
if closed_callback != None:
|
||||
self.set_link_closed_callback(closed_callback)
|
||||
if established_callback != None: self.set_link_established_callback(established_callback)
|
||||
if closed_callback != None: self.set_link_closed_callback(closed_callback)
|
||||
|
||||
if self.initiator:
|
||||
signalling_bytes = b""
|
||||
@@ -334,8 +330,7 @@ class Link:
|
||||
self.peer_sig_pub_bytes = peer_sig_pub_bytes
|
||||
self.peer_sig_pub = Ed25519PublicKey.from_public_bytes(self.peer_sig_pub_bytes)
|
||||
|
||||
if not hasattr(self.peer_pub, "curve"):
|
||||
self.peer_pub.curve = Link.CURVE
|
||||
if not hasattr(self.peer_pub, "curve"): self.peer_pub.curve = Link.CURVE
|
||||
|
||||
@staticmethod
|
||||
def link_id_from_lr_packet(packet):
|
||||
@@ -399,13 +394,13 @@ class Link:
|
||||
signalling_bytes = b""
|
||||
confirmed_mtu = None
|
||||
mode = Link.mode_from_lp_packet(packet)
|
||||
RNS.log(f"Validating link request proof with mode {Link.MODE_DESCRIPTIONS[mode]}", RNS.LOG_DEBUG) # TODO: Remove debug
|
||||
RNS.log(f"Validating link request proof with mode {Link.MODE_DESCRIPTIONS[mode]}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
if mode != self.mode: raise TypeError(f"Invalid link mode {mode} in link request proof")
|
||||
if len(packet.data) == RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2+Link.LINK_MTU_SIZE:
|
||||
confirmed_mtu = Link.mtu_from_lp_packet(packet)
|
||||
signalling_bytes = Link.signalling_bytes(confirmed_mtu, mode)
|
||||
packet.data = packet.data[:RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2]
|
||||
RNS.log(f"Destination confirmed link MTU of {RNS.prettysize(confirmed_mtu)}", RNS.LOG_DEBUG) # TODO: Remove debug
|
||||
RNS.log(f"Destination confirmed link MTU of {RNS.prettysize(confirmed_mtu)}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
if self.initiator and len(packet.data) == RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2:
|
||||
peer_pub_bytes = packet.data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2]
|
||||
@@ -430,7 +425,7 @@ class Link:
|
||||
self.activated_at = time.time()
|
||||
self.last_proof = self.activated_at
|
||||
RNS.Transport.activate_link(self)
|
||||
RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+RNS.prettyshorttime(self.rtt), RNS.LOG_DEBUG)
|
||||
RNS.log(f"Link {self} established with {self.destination}, RTT is {RNS.prettyshorttime(self.rtt)}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
if self.rtt != None and self.establishment_cost != None and self.rtt > 0 and self.establishment_cost > 0:
|
||||
self.establishment_rate = self.establishment_cost/self.rtt
|
||||
@@ -447,8 +442,8 @@ class Link:
|
||||
thread = threading.Thread(target=self.callbacks.link_established, args=(self,))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
else:
|
||||
RNS.log("Invalid link proof signature received by "+str(self)+". Ignoring.", RNS.LOG_DEBUG)
|
||||
|
||||
else: RNS.log(f"Invalid link proof signature received by {self}. Ignoring.", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
|
||||
except Exception as e:
|
||||
self.status = Link.CLOSED
|
||||
@@ -475,7 +470,8 @@ class Link:
|
||||
self.had_outbound()
|
||||
|
||||
|
||||
def request(self, path, data = None, response_callback = None, failed_callback = None, progress_callback = None, timeout = None):
|
||||
def request(self, path, data=None, response_callback=None, failed_callback=None, progress_callback=None,
|
||||
timeout=None, max_response_size=None):
|
||||
"""
|
||||
Sends a request to the remote peer.
|
||||
|
||||
@@ -483,6 +479,7 @@ class Link:
|
||||
:param response_callback: An optional function or method with the signature *response_callback(request_receipt)* to be called when a response is received. See the :ref:`Request Example<example-request>` for more info.
|
||||
:param failed_callback: An optional function or method with the signature *failed_callback(request_receipt)* to be called when a request fails. See the :ref:`Request Example<example-request>` for more info.
|
||||
:param progress_callback: An optional function or method with the signature *progress_callback(request_receipt)* to be called when progress is made receiving the response. Progress can be accessed as a float between 0.0 and 1.0 by the *request_receipt.progress* property.
|
||||
:param max_response_size: An optional maximum accepted response size, in bytes as an integer.
|
||||
:param timeout: An optional timeout in seconds for the request. If *None* is supplied it will be calculated based on link RTT.
|
||||
:returns: A :ref:`RNS.RequestReceipt<api-requestreceipt>` instance if the request was sent, or *False* if it was not.
|
||||
"""
|
||||
@@ -497,35 +494,20 @@ class Link:
|
||||
request_packet = RNS.Packet(self, packed_request, RNS.Packet.DATA, context = RNS.Packet.REQUEST)
|
||||
packet_receipt = request_packet.send()
|
||||
|
||||
if packet_receipt == False:
|
||||
return False
|
||||
if packet_receipt == False: return False
|
||||
else:
|
||||
packet_receipt.set_timeout(timeout)
|
||||
return RequestReceipt(
|
||||
self,
|
||||
packet_receipt = packet_receipt,
|
||||
response_callback = response_callback,
|
||||
failed_callback = failed_callback,
|
||||
progress_callback = progress_callback,
|
||||
timeout = timeout,
|
||||
request_size = len(packed_request),
|
||||
)
|
||||
|
||||
return RequestReceipt(self, packet_receipt=packet_receipt, response_callback=response_callback,
|
||||
failed_callback=failed_callback, progress_callback=progress_callback, timeout=timeout,
|
||||
request_size=len(packed_request), max_response_size=max_response_size)
|
||||
else:
|
||||
request_id = RNS.Identity.truncated_hash(packed_request)
|
||||
RNS.log("Sending request "+RNS.prettyhexrep(request_id)+" as resource.", RNS.LOG_DEBUG)
|
||||
request_resource = RNS.Resource(packed_request, self, request_id = request_id, is_response = False, timeout = timeout)
|
||||
|
||||
return RequestReceipt(
|
||||
self,
|
||||
resource = request_resource,
|
||||
response_callback = response_callback,
|
||||
failed_callback = failed_callback,
|
||||
progress_callback = progress_callback,
|
||||
timeout = timeout,
|
||||
request_size = len(packed_request),
|
||||
)
|
||||
|
||||
return RequestReceipt(self, resource=request_resource, response_callback=response_callback,
|
||||
failed_callback=failed_callback, progress_callback=progress_callback, timeout=timeout,
|
||||
request_size=len(packed_request), max_response_size=max_response_size)
|
||||
|
||||
def update_mdu(self):
|
||||
self.mdu = self.mtu - RNS.Reticulum.HEADER_MAXSIZE - RNS.Reticulum.IFAC_MIN_SIZE
|
||||
@@ -540,6 +522,7 @@ class Link:
|
||||
self.rtt = max(measured_rtt, rtt)
|
||||
self.status = Link.ACTIVE
|
||||
self.activated_at = time.time()
|
||||
self.expected_hops = packet.hops
|
||||
|
||||
if self.rtt != None and self.establishment_cost != None and self.rtt > 0 and self.establishment_cost > 0:
|
||||
self.establishment_rate = self.establishment_cost/self.rtt
|
||||
@@ -547,10 +530,8 @@ class Link:
|
||||
self.__update_keepalive()
|
||||
|
||||
try:
|
||||
if self.owner.callbacks.link_established != None:
|
||||
self.owner.callbacks.link_established(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error occurred in external link establishment callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
if self.owner.callbacks.link_established != None: self.owner.callbacks.link_established(self)
|
||||
except Exception as e: RNS.log("Error occurred in external link establishment callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error occurred while processing RTT packet, tearing down link. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -565,73 +546,57 @@ class Link:
|
||||
|
||||
:param track: Whether or not to keep track of physical layer statistics. Value must be ``True`` or ``False``.
|
||||
"""
|
||||
if track:
|
||||
self.__track_phy_stats = True
|
||||
else:
|
||||
self.__track_phy_stats = False
|
||||
if track: self.__track_phy_stats = True
|
||||
else: self.__track_phy_stats = False
|
||||
|
||||
def get_rssi(self):
|
||||
"""
|
||||
:returns: The physical layer *Received Signal Strength Indication* if available, otherwise ``None``. Physical layer statistics must be enabled on the link for this method to return a value.
|
||||
"""
|
||||
if self.__track_phy_stats:
|
||||
return self.rssi
|
||||
else:
|
||||
return None
|
||||
if self.__track_phy_stats: return self.rssi
|
||||
else: return None
|
||||
|
||||
def get_snr(self):
|
||||
"""
|
||||
:returns: The physical layer *Signal-to-Noise Ratio* if available, otherwise ``None``. Physical layer statistics must be enabled on the link for this method to return a value.
|
||||
"""
|
||||
if self.__track_phy_stats:
|
||||
return self.snr
|
||||
else:
|
||||
return None
|
||||
if self.__track_phy_stats: return self.snr
|
||||
else: return None
|
||||
|
||||
def get_q(self):
|
||||
"""
|
||||
:returns: The physical layer *Link Quality* if available, otherwise ``None``. Physical layer statistics must be enabled on the link for this method to return a value.
|
||||
"""
|
||||
if self.__track_phy_stats:
|
||||
return self.q
|
||||
else:
|
||||
return None
|
||||
if self.__track_phy_stats: return self.q
|
||||
else: return None
|
||||
|
||||
def get_establishment_rate(self):
|
||||
"""
|
||||
:returns: The data transfer rate at which the link establishment procedure ocurred, in bits per second.
|
||||
"""
|
||||
if self.establishment_rate != None:
|
||||
return self.establishment_rate*8
|
||||
else:
|
||||
return None
|
||||
if self.establishment_rate != None: return self.establishment_rate*8
|
||||
else: return None
|
||||
|
||||
def get_mtu(self):
|
||||
"""
|
||||
:returns: The MTU of an established link.
|
||||
"""
|
||||
if self.status == Link.ACTIVE:
|
||||
return self.mtu
|
||||
else:
|
||||
return None
|
||||
if self.status == Link.ACTIVE: return self.mtu
|
||||
else: return None
|
||||
|
||||
def get_mdu(self):
|
||||
"""
|
||||
:returns: The packet MDU of an established link.
|
||||
"""
|
||||
if self.status == Link.ACTIVE:
|
||||
return self.mdu
|
||||
else:
|
||||
return None
|
||||
if self.status == Link.ACTIVE: return self.mdu
|
||||
else: return None
|
||||
|
||||
def get_expected_rate(self):
|
||||
"""
|
||||
:returns: The packet expected in-flight data rate of an established link.
|
||||
"""
|
||||
if self.status == Link.ACTIVE:
|
||||
return self.expected_rate
|
||||
else:
|
||||
return None
|
||||
if self.status == Link.ACTIVE: return self.expected_rate
|
||||
else: return None
|
||||
|
||||
def get_mode(self):
|
||||
"""
|
||||
@@ -649,10 +614,8 @@ class Link:
|
||||
"""
|
||||
:returns: The time in seconds since this link was established.
|
||||
"""
|
||||
if self.activated_at:
|
||||
return time.time() - self.activated_at
|
||||
else:
|
||||
return None
|
||||
if self.activated_at: return time.time() - self.activated_at
|
||||
else: return None
|
||||
|
||||
def no_inbound_for(self):
|
||||
"""
|
||||
@@ -701,6 +664,7 @@ class Link:
|
||||
Closes the link and purges encryption keys. New keys will
|
||||
be used if a new link to the same destination is established.
|
||||
"""
|
||||
if self.status == Link.CLOSED: return
|
||||
if self.status != Link.PENDING and self.status != Link.CLOSED: self.__teardown_packet()
|
||||
self.status = Link.CLOSED
|
||||
if self.initiator: self.teardown_reason = Link.INITIATOR_CLOSED
|
||||
@@ -712,14 +676,11 @@ class Link:
|
||||
plaintext = self.decrypt(packet.data)
|
||||
if plaintext == self.link_id:
|
||||
self.status = Link.CLOSED
|
||||
if self.initiator:
|
||||
self.teardown_reason = Link.DESTINATION_CLOSED
|
||||
else:
|
||||
self.teardown_reason = Link.INITIATOR_CLOSED
|
||||
if self.initiator: self.teardown_reason = Link.DESTINATION_CLOSED
|
||||
else: self.teardown_reason = Link.INITIATOR_CLOSED
|
||||
self.__update_phy_stats(packet)
|
||||
self.link_closed()
|
||||
except Exception as e:
|
||||
pass
|
||||
except Exception as e: pass
|
||||
|
||||
def link_closed(self):
|
||||
for resource in self.incoming_resources: resource.cancel()
|
||||
@@ -752,9 +713,7 @@ class Link:
|
||||
while not self.status == Link.CLOSED:
|
||||
while (self.watchdog_lock):
|
||||
rtt_wait = 0.025
|
||||
if hasattr(self, "rtt") and self.rtt:
|
||||
rtt_wait = self.rtt
|
||||
|
||||
if hasattr(self, "rtt") and self.rtt: rtt_wait = self.rtt
|
||||
sleep(max(rtt_wait, 0.025))
|
||||
|
||||
if not self.status == Link.CLOSED:
|
||||
@@ -779,28 +738,25 @@ class Link:
|
||||
self.link_closed()
|
||||
sleep_time = 0.001
|
||||
|
||||
if self.initiator:
|
||||
RNS.log("Timeout waiting for link request proof", RNS.LOG_DEBUG)
|
||||
else:
|
||||
RNS.log("Timeout waiting for RTT packet from link initiator", RNS.LOG_DEBUG)
|
||||
if self.initiator: RNS.log("Timeout waiting for link request proof", RNS.LOG_DEBUG)
|
||||
else: RNS.log("Timeout waiting for RTT packet from link initiator", RNS.LOG_DEBUG)
|
||||
|
||||
elif self.status == Link.ACTIVE:
|
||||
activated_at = self.activated_at if self.activated_at != None else 0
|
||||
last_inbound = max(max(self.last_inbound, self.last_proof), activated_at)
|
||||
now = time.time()
|
||||
|
||||
if now >= last_inbound + self.keepalive:
|
||||
if now >= last_inbound + self.keepalive or now >= self.last_outbound + self.keepalive:
|
||||
if self.initiator and now >= self.last_keepalive + self.keepalive:
|
||||
self.send_keepalive()
|
||||
|
||||
if time.time() >= last_inbound + self.stale_time:
|
||||
if now >= last_inbound + self.stale_time:
|
||||
sleep_time = self.rtt * self.keepalive_timeout_factor + Link.STALE_GRACE
|
||||
self.status = Link.STALE
|
||||
else:
|
||||
sleep_time = self.keepalive
|
||||
|
||||
else:
|
||||
sleep_time = (last_inbound + self.keepalive) - time.time()
|
||||
else: sleep_time = self.keepalive
|
||||
|
||||
else: sleep_time = (last_inbound + self.keepalive) - now
|
||||
|
||||
elif self.status == Link.STALE:
|
||||
sleep_time = 0.001
|
||||
@@ -810,10 +766,9 @@ class Link:
|
||||
self.link_closed()
|
||||
|
||||
|
||||
if sleep_time == 0:
|
||||
RNS.log("Warning! Link watchdog sleep time of 0!", RNS.LOG_ERROR)
|
||||
if sleep_time == 0: RNS.log(f"Link watchdog sleep time of 0 on {self}", RNS.LOG_ERROR)
|
||||
if sleep_time == None or sleep_time < 0:
|
||||
RNS.log("Timing error! Tearing down link "+str(self)+" now.", RNS.LOG_ERROR)
|
||||
RNS.log(f"Timing error, tearing down link {self} now", RNS.LOG_ERROR)
|
||||
self.teardown()
|
||||
sleep_time = 0.1
|
||||
|
||||
@@ -825,7 +780,6 @@ class Link:
|
||||
self.snr = None
|
||||
self.q = None
|
||||
|
||||
|
||||
def __update_phy_stats(self, packet, query_shared = True, force_update = False):
|
||||
if self.__track_phy_stats or force_update:
|
||||
if query_shared:
|
||||
@@ -834,12 +788,9 @@ class Link:
|
||||
if packet.snr == None: packet.snr = reticulum.get_packet_snr(packet.packet_hash)
|
||||
if packet.q == None: packet.q = reticulum.get_packet_q(packet.packet_hash)
|
||||
|
||||
if packet.rssi != None:
|
||||
self.rssi = packet.rssi
|
||||
if packet.snr != None:
|
||||
self.snr = packet.snr
|
||||
if packet.q != None:
|
||||
self.q = packet.q
|
||||
if packet.rssi != None: self.rssi = packet.rssi
|
||||
if packet.snr != None: self.snr = packet.snr
|
||||
if packet.q != None: self.q = packet.q
|
||||
|
||||
def __update_keepalive(self):
|
||||
self.keepalive = max(min(self.rtt*(Link.KEEPALIVE_MAX/Link.KEEPALIVE_MAX_RTT), Link.KEEPALIVE_MAX), Link.KEEPALIVE_MIN)
|
||||
@@ -903,19 +854,26 @@ class Link:
|
||||
identity_string = str(self.get_remote_identity()) if self.get_remote_identity() != None else "<Unknown>"
|
||||
RNS.log("Request "+RNS.prettyhexrep(request_id)+" from "+identity_string+" not allowed for: "+str(path), RNS.LOG_DEBUG)
|
||||
|
||||
def handle_response(self, request_id, response_data, response_size, response_transfer_size, metadata=None, update_sizes=False):
|
||||
def handle_response(self, request_id, response_data, response_size, response_transfer_size, metadata=None, update_sizes=False, check_size=False):
|
||||
if self.status == Link.ACTIVE:
|
||||
remove = None
|
||||
for pending_request in self.pending_requests:
|
||||
if pending_request.request_id == request_id:
|
||||
if not check_size or pending_request.max_response_size == None: size_ok = True
|
||||
else: size_ok = response_size <= pending_request.max_response_size
|
||||
|
||||
remove = pending_request
|
||||
|
||||
try:
|
||||
if update_sizes:
|
||||
pending_request.response_size = response_size
|
||||
if pending_request.response_transfer_size == None: pending_request.response_transfer_size = 0
|
||||
pending_request.response_transfer_size += response_transfer_size
|
||||
|
||||
pending_request.response_received(response_data, metadata)
|
||||
if size_ok: pending_request.response_received(response_data, metadata)
|
||||
else:
|
||||
RNS.log(f"Rejected response with excessive size {RNS.prettysize(response_size)} on {self}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
pending_request.response_rejected()
|
||||
|
||||
except Exception as e: RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
break
|
||||
@@ -933,8 +891,8 @@ class Link:
|
||||
|
||||
def job(): self.handle_request(request_id, request_data)
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
else:
|
||||
RNS.log("Incoming request resource failed with status: "+RNS.hexrep([resource.status]), RNS.LOG_DEBUG)
|
||||
|
||||
else: RNS.log("Incoming request resource failed with status: "+RNS.hexrep([resource.status]), RNS.LOG_DEBUG)
|
||||
|
||||
def response_resource_concluded(self, resource):
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
@@ -965,8 +923,7 @@ class Link:
|
||||
|
||||
:return: ``Channel`` object
|
||||
"""
|
||||
if self._channel is None:
|
||||
self._channel = Channel(LinkChannelOutlet(self))
|
||||
if self._channel is None: self._channel = Channel(LinkChannelOutlet(self))
|
||||
return self._channel
|
||||
|
||||
def receive(self, packet):
|
||||
@@ -976,12 +933,10 @@ class Link:
|
||||
RNS.log(f"Link-associated packet received on unexpected interface {packet.receiving_interface} instead of {self.attached_interface}! Someone might be trying to manipulate your communication!", RNS.LOG_ERROR)
|
||||
else:
|
||||
self.last_inbound = time.time()
|
||||
if packet.context != RNS.Packet.KEEPALIVE:
|
||||
self.last_data = self.last_inbound
|
||||
if packet.context != RNS.Packet.KEEPALIVE: self.last_data = self.last_inbound
|
||||
self.rx += 1
|
||||
self.rxbytes += len(packet.data)
|
||||
if self.status == Link.STALE:
|
||||
self.status = Link.ACTIVE
|
||||
if self.status == Link.STALE: self.status = Link.ACTIVE
|
||||
|
||||
if packet.packet_type == RNS.Packet.DATA:
|
||||
should_query = False
|
||||
@@ -1002,10 +957,8 @@ class Link:
|
||||
elif self.destination.proof_strategy == RNS.Destination.PROVE_APP:
|
||||
if self.destination.callbacks.proof_requested:
|
||||
try:
|
||||
if self.destination.callbacks.proof_requested(packet):
|
||||
packet.prove()
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing proof request callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
if self.destination.callbacks.proof_requested(packet): packet.prove()
|
||||
except Exception as e: RNS.log("Error while executing proof request callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
elif packet.context == RNS.Packet.LINKIDENTIFY:
|
||||
plaintext = self.decrypt(packet.data)
|
||||
@@ -1023,6 +976,7 @@ class Link:
|
||||
self.teardown()
|
||||
|
||||
else:
|
||||
if self.__remote_identity == None:
|
||||
self.__remote_identity = identity
|
||||
if self.callbacks.remote_identified != None:
|
||||
try: self.callbacks.remote_identified(self, self.__remote_identity)
|
||||
@@ -1035,12 +989,15 @@ class Link:
|
||||
request_id = packet.getTruncatedHash()
|
||||
packed_request = self.decrypt(packet.data)
|
||||
if packed_request != None:
|
||||
if self.destination.max_request_size == None: size_ok = True
|
||||
else: size_ok = len(packed_request) <= self.destination.max_request_size
|
||||
if not size_ok: RNS.log(f"Ignored request with excessive size {RNS.prettysize(len(packed_request))} on {self.destination}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
else:
|
||||
unpacked_request = umsgpack.unpackb(packed_request)
|
||||
def job(): self.handle_request(request_id, unpacked_request)
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
self.__update_phy_stats(packet, query_shared=True)
|
||||
except Exception as e:
|
||||
RNS.log("Error occurred while handling request. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log("Error occurred while handling request. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
elif packet.context == RNS.Packet.RESPONSE:
|
||||
try:
|
||||
@@ -1050,11 +1007,10 @@ class Link:
|
||||
request_id = unpacked_response[0]
|
||||
response_data = unpacked_response[1]
|
||||
transfer_size = len(umsgpack.packb(response_data))-2
|
||||
def job(): self.handle_response(request_id, response_data, transfer_size, transfer_size, update_sizes=True)
|
||||
def job(): self.handle_response(request_id, response_data, transfer_size, transfer_size, update_sizes=True, check_size=True)
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
self.__update_phy_stats(packet, query_shared=True)
|
||||
except Exception as e:
|
||||
RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
elif packet.context == RNS.Packet.LRRTT:
|
||||
if not self.initiator:
|
||||
@@ -1069,13 +1025,26 @@ class Link:
|
||||
packet.plaintext = self.decrypt(packet.data)
|
||||
if packet.plaintext != None:
|
||||
self.__update_phy_stats(packet, query_shared=True)
|
||||
|
||||
try:
|
||||
if RNS.ResourceAdvertisement.is_request(packet):
|
||||
RNS.Resource.accept(packet, callback=self.request_resource_concluded)
|
||||
if self.destination.request_handlers:
|
||||
if self.destination.max_request_size == None: size_ok = True
|
||||
else: size_ok = RNS.ResourceAdvertisement.read_size(packet) <= self.destination.max_request_size
|
||||
if size_ok: RNS.Resource.accept(packet, callback=self.request_resource_concluded)
|
||||
else:
|
||||
RNS.Resource.reject(packet)
|
||||
RNS.log(f"Rejected request with excessive size {RNS.prettysize(RNS.ResourceAdvertisement.read_size(packet))} on {self}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
elif RNS.ResourceAdvertisement.is_response(packet):
|
||||
request_id = RNS.ResourceAdvertisement.read_request_id(packet)
|
||||
for pending_request in self.pending_requests:
|
||||
if pending_request.request_id == request_id:
|
||||
if pending_request.max_response_size == None: size_ok = True
|
||||
else: size_ok = RNS.ResourceAdvertisement.read_size(packet) <= pending_request.max_response_size
|
||||
if not size_ok:
|
||||
RNS.Resource.reject(packet)
|
||||
pending_request.response_rejected()
|
||||
RNS.log(f"Rejected response with excessive size {RNS.prettysize(RNS.ResourceAdvertisement.read_size(packet))} on {self}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
else:
|
||||
response_resource = RNS.Resource.accept(packet, callback=self.response_resource_concluded, progress_callback=pending_request.response_resource_progress, request_id = request_id)
|
||||
if response_resource != None:
|
||||
if pending_request.response_size == None:
|
||||
@@ -1086,7 +1055,6 @@ class Link:
|
||||
if pending_request.started_at == None:
|
||||
pending_request.started_at = time.time()
|
||||
pending_request.response_resource_progress(response_resource)
|
||||
|
||||
elif self.resource_strategy == Link.ACCEPT_NONE: pass
|
||||
elif self.resource_strategy == Link.ACCEPT_APP:
|
||||
if self.callbacks.resource != None:
|
||||
@@ -1099,6 +1067,9 @@ class Link:
|
||||
RNS.log("Error while executing resource accept callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
elif self.resource_strategy == Link.ACCEPT_ALL:
|
||||
RNS.Resource.accept(packet, self.callbacks.resource_concluded)
|
||||
except Exception as e:
|
||||
RNS.log(f"Invalid resource advertisement on {self}: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.teardown()
|
||||
|
||||
elif packet.context == RNS.Packet.RESOURCE_REQ:
|
||||
plaintext = self.decrypt(packet.data)
|
||||
@@ -1151,6 +1122,7 @@ class Link:
|
||||
|
||||
elif packet.context == RNS.Packet.KEEPALIVE:
|
||||
if not self.initiator and packet.data == bytes([0xFF]):
|
||||
if time.time() >= self.last_outbound + self.keepalive:
|
||||
keepalive_packet = RNS.Packet(self, bytes([0xFE]), context=RNS.Packet.KEEPALIVE)
|
||||
keepalive_packet.send()
|
||||
self.had_outbound(is_keepalive = True)
|
||||
@@ -1186,7 +1158,6 @@ class Link:
|
||||
|
||||
self.watchdog_lock = False
|
||||
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
try:
|
||||
if not self.token:
|
||||
@@ -1201,7 +1172,6 @@ class Link:
|
||||
RNS.log("Encryption on link "+str(self)+" failed. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
try:
|
||||
if not self.token: self.token = Token(self.derived_key)
|
||||
@@ -1211,7 +1181,6 @@ class Link:
|
||||
RNS.log("Decryption failed on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
return None
|
||||
|
||||
|
||||
def sign(self, message):
|
||||
return self.sig_prv.sign(message)
|
||||
|
||||
@@ -1219,8 +1188,7 @@ class Link:
|
||||
try:
|
||||
self.peer_sig_pub.verify(signature, message)
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
except Exception as e: return False
|
||||
|
||||
def set_link_established_callback(self, callback):
|
||||
self.callbacks.link_established = callback
|
||||
@@ -1348,7 +1316,9 @@ class RequestReceipt():
|
||||
RECEIVING = 0x03
|
||||
READY = 0x04
|
||||
|
||||
def __init__(self, link, packet_receipt = None, resource = None, response_callback = None, failed_callback = None, progress_callback = None, timeout = None, request_size = None):
|
||||
def __init__(self, link, packet_receipt=None, resource=None, response_callback=None, failed_callback=None,
|
||||
progress_callback=None, timeout=None, request_size=None, max_response_size=None):
|
||||
|
||||
self.packet_receipt = packet_receipt
|
||||
self.resource = resource
|
||||
self.started_at = None
|
||||
@@ -1375,6 +1345,7 @@ class RequestReceipt():
|
||||
self.progress = 0
|
||||
self.concluded_at = None
|
||||
self.response_concluded_at = None
|
||||
self.max_response_size = max_response_size
|
||||
|
||||
if timeout != None:
|
||||
self.timeout = timeout
|
||||
@@ -1388,7 +1359,6 @@ class RequestReceipt():
|
||||
|
||||
self.link.pending_requests.append(self)
|
||||
|
||||
|
||||
def request_resource_concluded(self, resource):
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
RNS.log("Request "+RNS.prettyhexrep(self.request_id)+" successfully sent as resource.", RNS.LOG_DEBUG)
|
||||
@@ -1406,12 +1376,10 @@ class RequestReceipt():
|
||||
self.link.pending_requests.remove(self)
|
||||
|
||||
if self.callbacks.failed != None:
|
||||
try:
|
||||
self.callbacks.failed(self)
|
||||
try: self.callbacks.failed(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing request failed callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def __response_timeout_job(self):
|
||||
while self.status == RequestReceipt.DELIVERED:
|
||||
now = time.time()
|
||||
@@ -1421,7 +1389,6 @@ class RequestReceipt():
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def request_timed_out(self, packet_receipt):
|
||||
if self in self.link.pending_requests and self.status == RequestReceipt.DELIVERED:
|
||||
self.status = RequestReceipt.FAILED
|
||||
@@ -1430,9 +1397,17 @@ class RequestReceipt():
|
||||
|
||||
if self.callbacks.failed != None:
|
||||
try: self.callbacks.failed(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing request timed out callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log("Error while executing request timed out callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def response_rejected(self):
|
||||
if self in self.link.pending_requests and self.status == RequestReceipt.DELIVERED:
|
||||
self.status = RequestReceipt.FAILED
|
||||
self.concluded_at = time.time()
|
||||
self.link.pending_requests.remove(self)
|
||||
|
||||
if self.callbacks.failed != None:
|
||||
try: self.callbacks.failed(self)
|
||||
except Exception as e: RNS.log("Error while executing request timed out callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def response_resource_progress(self, resource):
|
||||
if resource != None:
|
||||
@@ -1449,13 +1424,10 @@ class RequestReceipt():
|
||||
self.progress = resource.get_progress()
|
||||
|
||||
if self.callbacks.progress != None:
|
||||
try:
|
||||
self.callbacks.progress(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing response progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
else:
|
||||
resource.cancel()
|
||||
try: self.callbacks.progress(self)
|
||||
except Exception as e: RNS.log("Error while executing response progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
else: resource.cancel()
|
||||
|
||||
def response_received(self, response, metadata=None):
|
||||
if not self.status == RequestReceipt.FAILED:
|
||||
@@ -1474,13 +1446,11 @@ class RequestReceipt():
|
||||
|
||||
if self.callbacks.progress != None:
|
||||
try: self.callbacks.progress(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing response progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log("Error while executing response progress callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
if self.callbacks.response != None:
|
||||
try: self.callbacks.response(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing response received callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log("Error while executing response received callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def get_request_id(self):
|
||||
"""
|
||||
@@ -1504,31 +1474,23 @@ class RequestReceipt():
|
||||
"""
|
||||
:returns: The response as *bytes* if it is ready, otherwise *None*.
|
||||
"""
|
||||
if self.status == RequestReceipt.READY:
|
||||
return self.response
|
||||
else:
|
||||
return None
|
||||
if self.status == RequestReceipt.READY: return self.response
|
||||
else: return None
|
||||
|
||||
def get_response_time(self):
|
||||
"""
|
||||
:returns: The response time of the request in seconds.
|
||||
"""
|
||||
if self.status == RequestReceipt.READY:
|
||||
return self.response_concluded_at - self.started_at
|
||||
else:
|
||||
return None
|
||||
if self.status == RequestReceipt.READY: return self.response_concluded_at - self.started_at
|
||||
else: return None
|
||||
|
||||
def concluded(self):
|
||||
"""
|
||||
:returns: True if the associated request has concluded (successfully or with a failure), otherwise False.
|
||||
"""
|
||||
if self.status == RequestReceipt.READY:
|
||||
return True
|
||||
elif self.status == RequestReceipt.FAILED:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
if self.status == RequestReceipt.READY: return True
|
||||
elif self.status == RequestReceipt.FAILED: return True
|
||||
else: return False
|
||||
|
||||
|
||||
class RequestReceiptCallbacks:
|
||||
|
||||
+46
-73
@@ -244,6 +244,9 @@ class Packet:
|
||||
self.flags = self.raw[0]
|
||||
self.hops = self.raw[1]
|
||||
|
||||
if self.hops >= RNS.Transport.PATHFINDER_M:
|
||||
raise ValueError(f"Invalid hop count {self.hops}")
|
||||
|
||||
self.header_type = (self.flags & 0b01000000) >> 6
|
||||
self.context_flag = (self.flags & 0b00100000) >> 5
|
||||
self.transport_type = (self.flags & 0b00010000) >> 4
|
||||
@@ -268,7 +271,7 @@ class Packet:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Received malformed packet, dropping it. The contained exception was: "+str(e), RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
RNS.log(f"Received malformed packet, dropping it. The contained exception was: {e}", RNS.LOG_EXTREME) if RNS.sl(RNS.LOG_EXTREME) else None
|
||||
return False
|
||||
|
||||
def send(self):
|
||||
@@ -278,6 +281,7 @@ class Packet:
|
||||
:returns: A :ref:`RNS.PacketReceipt<api-packetreceipt>` instance if *create_receipt* was set to *True* when the packet was instantiated, if not returns *None*. If the packet could not be sent *False* is returned.
|
||||
"""
|
||||
if not self.sent:
|
||||
if not self.packed: self.pack()
|
||||
if self.destination.type == RNS.Destination.LINK:
|
||||
if self.destination.status == RNS.Link.CLOSED:
|
||||
RNS.log("Attempt to transmit over a closed link, dropping packet", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
@@ -288,9 +292,7 @@ class Packet:
|
||||
else:
|
||||
self.destination.last_outbound = time.time()
|
||||
self.destination.tx += 1
|
||||
self.destination.txbytes += len(self.data)
|
||||
|
||||
if not self.packed: self.pack()
|
||||
self.destination.txbytes += len(self.ciphertext)
|
||||
|
||||
if RNS.Transport.outbound(self): return self.receipt
|
||||
else:
|
||||
@@ -313,51 +315,40 @@ class Packet:
|
||||
# encrypted destinations
|
||||
self.pack()
|
||||
|
||||
if RNS.Transport.outbound(self):
|
||||
return self.receipt
|
||||
if RNS.Transport.outbound(self): return self.receipt
|
||||
else:
|
||||
RNS.log("Re-send failed. No interfaces could process the outbound packet", RNS.LOG_WARNING)
|
||||
self.sent = False
|
||||
self.receipt = None
|
||||
return False
|
||||
else:
|
||||
raise IOError("Packet was not sent yet")
|
||||
|
||||
else: raise IOError("Packet was not sent yet")
|
||||
|
||||
def prove(self, destination=None):
|
||||
if self.fromPacked and hasattr(self, "destination") and self.destination:
|
||||
if self.destination.identity and self.destination.identity.prv:
|
||||
self.destination.identity.prove(self, destination)
|
||||
elif self.fromPacked and hasattr(self, "link") and self.link:
|
||||
self.link.prove_packet(self)
|
||||
else:
|
||||
RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR)
|
||||
elif self.fromPacked and hasattr(self, "link") and self.link: self.link.prove_packet(self)
|
||||
else: RNS.log("Could not prove packet associated with neither a destination nor a link", RNS.LOG_ERROR)
|
||||
|
||||
# Generates a special destination that allows Reticulum
|
||||
# to direct the proof back to the proved packet's sender
|
||||
def generate_proof_destination(self):
|
||||
return ProofDestination(self)
|
||||
def generate_proof_destination(self): return ProofDestination(self)
|
||||
|
||||
def validate_proof_packet(self, proof_packet):
|
||||
return self.receipt.validate_proof_packet(proof_packet)
|
||||
def validate_proof_packet(self, proof_packet): return self.receipt.validate_proof_packet(proof_packet)
|
||||
|
||||
def validate_proof(self, proof):
|
||||
return self.receipt.validate_proof(proof)
|
||||
def validate_proof(self, proof): return self.receipt.validate_proof(proof)
|
||||
|
||||
def update_hash(self):
|
||||
self.packet_hash = self.get_hash()
|
||||
def update_hash(self): self.packet_hash = self.get_hash()
|
||||
|
||||
def get_hash(self):
|
||||
return RNS.Identity.full_hash(self.get_hashable_part())
|
||||
def get_hash(self): return RNS.Identity.full_hash(self.get_hashable_part())
|
||||
|
||||
def getTruncatedHash(self):
|
||||
return RNS.Identity.truncated_hash(self.get_hashable_part())
|
||||
def getTruncatedHash(self): return RNS.Identity.truncated_hash(self.get_hashable_part())
|
||||
|
||||
def get_hashable_part(self):
|
||||
hashable_part = bytes([self.raw[0] & 0b00001111])
|
||||
if self.header_type == Packet.HEADER_2:
|
||||
hashable_part += self.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:]
|
||||
else:
|
||||
hashable_part += self.raw[2:]
|
||||
if self.header_type == Packet.HEADER_2: hashable_part += self.raw[(RNS.Identity.TRUNCATED_HASHLENGTH//8)+2:]
|
||||
else: hashable_part += self.raw[2:]
|
||||
|
||||
return hashable_part
|
||||
|
||||
@@ -365,36 +356,29 @@ class Packet:
|
||||
"""
|
||||
:returns: The physical layer *Received Signal Strength Indication* if available, otherwise ``None``.
|
||||
"""
|
||||
if self.rssi != None:
|
||||
return self.rssi
|
||||
else:
|
||||
return reticulum.get_packet_rssi(self.packet_hash)
|
||||
if self.rssi != None: return self.rssi
|
||||
else: return reticulum.get_packet_rssi(self.packet_hash)
|
||||
|
||||
def get_snr(self):
|
||||
"""
|
||||
:returns: The physical layer *Signal-to-Noise Ratio* if available, otherwise ``None``.
|
||||
"""
|
||||
if self.snr != None:
|
||||
return self.snr
|
||||
else:
|
||||
return reticulum.get_packet_snr(self.packet_hash)
|
||||
if self.snr != None: return self.snr
|
||||
else: return reticulum.get_packet_snr(self.packet_hash)
|
||||
|
||||
def get_q(self):
|
||||
"""
|
||||
:returns: The physical layer *Link Quality* if available, otherwise ``None``.
|
||||
"""
|
||||
if self.q != None:
|
||||
return self.q
|
||||
else:
|
||||
return reticulum.get_packet_q(self.packet_hash)
|
||||
if self.q != None: return self.q
|
||||
else: return reticulum.get_packet_q(self.packet_hash)
|
||||
|
||||
class ProofDestination:
|
||||
def __init__(self, packet):
|
||||
self.hash = packet.get_hash()[:RNS.Reticulum.TRUNCATED_HASHLENGTH//8];
|
||||
self.type = RNS.Destination.SINGLE
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
return plaintext
|
||||
def encrypt(self, plaintext): return plaintext
|
||||
|
||||
|
||||
class PacketReceipt:
|
||||
@@ -463,18 +447,17 @@ class PacketReceipt:
|
||||
link.last_proof = self.concluded_at
|
||||
|
||||
if self.callbacks.delivery != None:
|
||||
try:
|
||||
self.callbacks.delivery(self)
|
||||
try: self.callbacks.delivery(self)
|
||||
except Exception as e:
|
||||
RNS.log("An error occurred while evaluating external delivery callback for "+str(link), RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
else: return False
|
||||
else: return False
|
||||
|
||||
elif len(proof) == PacketReceipt.IMPL_LENGTH:
|
||||
pass
|
||||
# TODO: Why is this disabled?
|
||||
@@ -491,8 +474,7 @@ class PacketReceipt:
|
||||
# else:
|
||||
# RNS.log("invalid")
|
||||
# return False
|
||||
else:
|
||||
return False
|
||||
else: return False
|
||||
|
||||
# Validate a raw proof
|
||||
def validate_proof(self, proof, proof_packet=None):
|
||||
@@ -509,24 +491,20 @@ class PacketReceipt:
|
||||
self.proof_packet = proof_packet
|
||||
|
||||
if self.callbacks.delivery != None:
|
||||
try:
|
||||
self.callbacks.delivery(self)
|
||||
try: self.callbacks.delivery(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
else: return False
|
||||
else: return False
|
||||
|
||||
elif len(proof) == PacketReceipt.IMPL_LENGTH:
|
||||
# This is an implicit proof
|
||||
|
||||
if not hasattr(self.destination, "identity"):
|
||||
return False
|
||||
|
||||
if self.destination.identity == None:
|
||||
return False
|
||||
if not hasattr(self.destination, "identity"): return False
|
||||
if self.destination.identity == None: return False
|
||||
|
||||
signature = proof[:RNS.Identity.SIGLENGTH//8]
|
||||
proof_valid = self.destination.identity.validate(signature, self.hash)
|
||||
@@ -537,16 +515,13 @@ class PacketReceipt:
|
||||
self.proof_packet = proof_packet
|
||||
|
||||
if self.callbacks.delivery != None:
|
||||
try:
|
||||
self.callbacks.delivery(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
try: self.callbacks.delivery(self)
|
||||
except Exception as e: RNS.log("Error while executing proof validated callback. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
else: return False
|
||||
else: return False
|
||||
|
||||
def get_rtt(self):
|
||||
"""
|
||||
@@ -559,10 +534,8 @@ class PacketReceipt:
|
||||
|
||||
def check_timeout(self):
|
||||
if self.status == PacketReceipt.SENT and self.is_timed_out():
|
||||
if self.timeout == -1:
|
||||
self.status = PacketReceipt.CULLED
|
||||
else:
|
||||
self.status = PacketReceipt.FAILED
|
||||
if self.timeout == -1: self.status = PacketReceipt.CULLED
|
||||
else: self.status = PacketReceipt.FAILED
|
||||
|
||||
self.concluded_at = time.time()
|
||||
|
||||
|
||||
+1
-2
@@ -31,5 +31,4 @@
|
||||
class Resolver:
|
||||
|
||||
@staticmethod
|
||||
def resolve_identity(full_name):
|
||||
pass
|
||||
def resolve_identity(full_name): pass
|
||||
+67
-80
@@ -149,7 +149,7 @@ class Resource:
|
||||
COMPLETE = 0x06
|
||||
FAILED = 0x07
|
||||
CORRUPT = 0x08
|
||||
REJECTED = 0x00
|
||||
REJECTED = 0x09
|
||||
|
||||
@staticmethod
|
||||
def reject(advertisement_packet):
|
||||
@@ -239,7 +239,7 @@ class Resource:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not decode resource advertisement, dropping resource", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
RNS.log(f"Could not decode resource advertisement, dropping resource: {e}", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
return None
|
||||
|
||||
# Create a resource for transmission to a remote destination
|
||||
@@ -322,11 +322,9 @@ class Resource:
|
||||
self.segment_index = 1
|
||||
self.split = False
|
||||
|
||||
elif data == None:
|
||||
pass
|
||||
elif data == None: pass
|
||||
|
||||
else:
|
||||
raise TypeError("Invalid data instance type passed to resource initialisation")
|
||||
else: raise TypeError("Invalid data instance type passed to resource initialisation")
|
||||
|
||||
if resource_data:
|
||||
if self.has_metadata: data = self.metadata + resource_data
|
||||
@@ -334,10 +332,8 @@ class Resource:
|
||||
|
||||
self.status = Resource.NONE
|
||||
self.link = link
|
||||
if self.link.mtu:
|
||||
self.sdu = self.link.mtu - RNS.Reticulum.HEADER_MAXSIZE - RNS.Reticulum.IFAC_MIN_SIZE
|
||||
else:
|
||||
self.sdu = link.mdu or Resource.SDU
|
||||
if self.link.mtu: self.sdu = self.link.mtu - RNS.Reticulum.HEADER_MAXSIZE - RNS.Reticulum.IFAC_MIN_SIZE
|
||||
else: self.sdu = link.mdu or Resource.SDU
|
||||
self.max_retries = Resource.MAX_RETRIES
|
||||
self.max_adv_retries = Resource.MAX_ADV_RETRIES
|
||||
self.retries_left = self.max_retries
|
||||
@@ -482,23 +478,27 @@ class Resource:
|
||||
|
||||
def hashmap_update_packet(self, plaintext):
|
||||
if not self.status == Resource.FAILED:
|
||||
if self.waiting_for_hmu:
|
||||
self.last_activity = time.time()
|
||||
self.retries_left = self.max_retries
|
||||
|
||||
update = umsgpack.unpackb(plaintext[RNS.Identity.HASHLENGTH//8:])
|
||||
self.hashmap_update(update[0], update[1])
|
||||
|
||||
|
||||
def hashmap_update(self, segment, hashmap):
|
||||
if not self.status == Resource.FAILED:
|
||||
self.status = Resource.TRANSFERRING
|
||||
seg_len = ResourceAdvertisement.HASHMAP_MAX_LEN
|
||||
hashes = len(hashmap)//Resource.MAPHASH_LEN
|
||||
for i in range(0,hashes):
|
||||
if self.hashmap[i+segment*seg_len] == None:
|
||||
self.hashmap_height += 1
|
||||
if self.hashmap[i+segment*seg_len] == None: self.hashmap_height += 1
|
||||
self.hashmap[i+segment*seg_len] = hashmap[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN]
|
||||
|
||||
if hashes < 1:
|
||||
RNS.log("Invalid HMU received, cancelling transfer", RNS.LOG_ERROR)
|
||||
self.cancel()
|
||||
|
||||
else:
|
||||
self.waiting_for_hmu = False
|
||||
self.request_next()
|
||||
|
||||
@@ -517,6 +517,14 @@ class Resource:
|
||||
prepare_thread = threading.Thread(target=self.__prepare_next_segment, daemon=True)
|
||||
prepare_thread.start()
|
||||
|
||||
def ensure_link(self):
|
||||
if not self.link or self.link.status != RNS.Link.ACTIVE:
|
||||
RNS.log(f"Invalid link state for {self}, aborting transfer", RNS.LOG_VERBOSE)
|
||||
try: self.cancel()
|
||||
except Exception as e: RNS.log(f"Error while cancelling resource on link-state abort: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
else: return True
|
||||
|
||||
def __advertise_job(self):
|
||||
self.advertisement_packet = RNS.Packet(self.link, ResourceAdvertisement(self).pack(), context=RNS.Packet.RESOURCE_ADV)
|
||||
while not self.link.ready_for_new_resource():
|
||||
@@ -524,6 +532,7 @@ class Resource:
|
||||
sleep(0.25)
|
||||
|
||||
try:
|
||||
if not self.ensure_link(): return
|
||||
self.advertisement_packet.send()
|
||||
self.last_activity = time.time()
|
||||
self.started_transferring = self.last_activity
|
||||
@@ -541,18 +550,13 @@ class Resource:
|
||||
self.watchdog_job()
|
||||
|
||||
def update_eifr(self):
|
||||
if self.rtt == None:
|
||||
rtt = self.link.rtt
|
||||
else:
|
||||
rtt = self.rtt
|
||||
if self.rtt == None: rtt = self.link.rtt
|
||||
else: rtt = self.rtt
|
||||
|
||||
if self.req_data_rtt_rate != 0:
|
||||
expected_inflight_rate = self.req_data_rtt_rate*8
|
||||
if self.req_data_rtt_rate != 0: expected_inflight_rate = self.req_data_rtt_rate*8
|
||||
else:
|
||||
if self.previous_eifr != None:
|
||||
expected_inflight_rate = self.previous_eifr
|
||||
else:
|
||||
expected_inflight_rate = self.link.establishment_cost*8 / rtt
|
||||
if self.previous_eifr != None: expected_inflight_rate = self.previous_eifr
|
||||
else: expected_inflight_rate = self.link.establishment_cost*8 / rtt
|
||||
|
||||
self.eifr = expected_inflight_rate
|
||||
if self.link: self.link.expected_rate = self.eifr
|
||||
@@ -566,8 +570,7 @@ class Resource:
|
||||
this_job_id = self.__watchdog_job_id
|
||||
|
||||
while self.status < Resource.ASSEMBLING and this_job_id == self.__watchdog_job_id:
|
||||
while self.watchdog_lock:
|
||||
sleep(0.025)
|
||||
while self.watchdog_lock: sleep(0.025)
|
||||
|
||||
sleep_time = None
|
||||
if self.status == Resource.ADVERTISED:
|
||||
@@ -581,6 +584,7 @@ class Resource:
|
||||
try:
|
||||
RNS.log("No part requests received, retrying resource advertisement...", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
self.retries_left -= 1
|
||||
if not self.ensure_link(): return
|
||||
self.advertisement_packet = RNS.Packet(self.link, ResourceAdvertisement(self).pack(), context=RNS.Packet.RESOURCE_ADV)
|
||||
self.advertisement_packet.send()
|
||||
self.last_activity = time.time()
|
||||
@@ -657,13 +661,13 @@ class Resource:
|
||||
self.last_part_sent = time.time()
|
||||
sleep_time = 0.001
|
||||
|
||||
elif self.status == Resource.REJECTED:
|
||||
elif self.status >= Resource.ASSEMBLING:
|
||||
sleep_time = 0.001
|
||||
|
||||
if sleep_time == 0:
|
||||
RNS.log("Warning! Link watchdog sleep time of 0!", RNS.LOG_DEBUG) if RNS.sl(RNS.LOG_DEBUG) else None
|
||||
if sleep_time == None or sleep_time < 0:
|
||||
RNS.log("Timing error, cancelling resource transfer.", RNS.LOG_ERROR)
|
||||
RNS.log(f"Timing error ({sleep_time}/{self.status}), cancelling resource transfer.", RNS.LOG_ERROR)
|
||||
self.cancel()
|
||||
|
||||
if sleep_time != None:
|
||||
@@ -754,6 +758,7 @@ class Resource:
|
||||
try:
|
||||
proof = RNS.Identity.full_hash(self.data+self.hash)
|
||||
proof_data = self.hash+proof
|
||||
if not self.ensure_link(): return
|
||||
proof_packet = RNS.Packet(self.link, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.RESOURCE_PRF)
|
||||
proof_packet.send()
|
||||
RNS.Transport.cache(proof_packet, force_cache=True)
|
||||
@@ -776,8 +781,8 @@ class Resource:
|
||||
advertise = False,
|
||||
auto_compress = self.auto_compress_option,
|
||||
sent_metadata_size = self.metadata_size)
|
||||
if self.__progress_callback:
|
||||
self.next_segment.progress_callback(self.__progress_callback)
|
||||
|
||||
if self.__progress_callback: self.next_segment.progress_callback(self.__progress_callback)
|
||||
|
||||
def validate_proof(self, proof_data):
|
||||
if not self.status == Resource.FAILED:
|
||||
@@ -966,6 +971,7 @@ class Resource:
|
||||
request_packet = RNS.Packet(self.link, request_data, context = RNS.Packet.RESOURCE_REQ)
|
||||
|
||||
try:
|
||||
if not self.ensure_link(): return
|
||||
request_packet.send()
|
||||
self.last_activity = time.time()
|
||||
self.req_sent = self.last_activity
|
||||
@@ -1010,11 +1016,11 @@ class Resource:
|
||||
|
||||
for part in requested_parts:
|
||||
try:
|
||||
if not self.ensure_link(): return
|
||||
if not part.sent:
|
||||
part.send()
|
||||
self.sent_parts += 1
|
||||
else:
|
||||
part.resend()
|
||||
else: part.resend()
|
||||
|
||||
self.last_activity = time.time()
|
||||
self.last_part_sent = self.last_activity
|
||||
@@ -1032,8 +1038,7 @@ class Resource:
|
||||
search_end = self.receiver_min_consecutive_height+ResourceAdvertisement.COLLISION_GUARD_SIZE
|
||||
for part in self.parts[search_start:search_end]:
|
||||
part_index += 1
|
||||
if part.map_hash == last_map_hash:
|
||||
break
|
||||
if part.map_hash == last_map_hash: break
|
||||
|
||||
self.receiver_min_consecutive_height = max(part_index-1-Resource.WINDOW_MAX, 0)
|
||||
|
||||
@@ -1041,9 +1046,7 @@ class Resource:
|
||||
RNS.log("Resource sequencing error, cancelling transfer!", RNS.LOG_ERROR)
|
||||
self.cancel()
|
||||
return
|
||||
else:
|
||||
segment = part_index // ResourceAdvertisement.HASHMAP_MAX_LEN
|
||||
|
||||
else: segment = part_index // ResourceAdvertisement.HASHMAP_MAX_LEN
|
||||
|
||||
hashmap_start = segment*ResourceAdvertisement.HASHMAP_MAX_LEN
|
||||
hashmap_end = min((segment+1)*ResourceAdvertisement.HASHMAP_MAX_LEN, len(self.parts))
|
||||
@@ -1052,10 +1055,16 @@ class Resource:
|
||||
for i in range(hashmap_start,hashmap_end):
|
||||
hashmap += self.hashmap[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN]
|
||||
|
||||
if not hashmap:
|
||||
RNS.log("Resource HMU error, cancelling transfer!", RNS.LOG_ERROR)
|
||||
self.cancel()
|
||||
return
|
||||
|
||||
hmu = self.hash+umsgpack.packb([segment, hashmap])
|
||||
hmu_packet = RNS.Packet(self.link, hmu, context = RNS.Packet.RESOURCE_HMU)
|
||||
|
||||
try:
|
||||
if not self.ensure_link(): return
|
||||
hmu_packet.send()
|
||||
self.last_activity = time.time()
|
||||
except Exception as e:
|
||||
@@ -1090,10 +1099,14 @@ class Resource:
|
||||
try:
|
||||
cancel_packet = RNS.Packet(self.link, self.hash, context=RNS.Packet.RESOURCE_ICL)
|
||||
cancel_packet.send()
|
||||
except Exception as e:
|
||||
RNS.log("Could not send resource cancel packet, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
except Exception as e: RNS.log("Could not send resource cancel packet, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
self.link.cancel_outgoing_resource(self)
|
||||
else:
|
||||
if self.link.status == RNS.Link.ACTIVE:
|
||||
try:
|
||||
cancel_packet = RNS.Packet(self.link, self.hash, context=RNS.Packet.RESOURCE_RCL)
|
||||
cancel_packet.send()
|
||||
except Exception as e: RNS.log("Could not send resource cancel packet, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
self.link.cancel_incoming_resource(self)
|
||||
|
||||
if self.callback != None:
|
||||
@@ -1241,40 +1254,30 @@ class ResourceAdvertisement:
|
||||
@staticmethod
|
||||
def is_request(advertisement_packet):
|
||||
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
|
||||
if adv.q != None and adv.u:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
if adv.q != None and adv.u: return True
|
||||
else: return False
|
||||
|
||||
@staticmethod
|
||||
def is_response(advertisement_packet):
|
||||
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
|
||||
|
||||
if adv.q != None and adv.p:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
if adv.q != None and adv.p: return True
|
||||
else: return False
|
||||
|
||||
@staticmethod
|
||||
def read_request_id(advertisement_packet):
|
||||
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
|
||||
return adv.q
|
||||
|
||||
|
||||
@staticmethod
|
||||
def read_transfer_size(advertisement_packet):
|
||||
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
|
||||
return adv.t
|
||||
|
||||
|
||||
@staticmethod
|
||||
def read_size(advertisement_packet):
|
||||
adv = ResourceAdvertisement.unpack(advertisement_packet.plaintext)
|
||||
return adv.d
|
||||
|
||||
|
||||
def __init__(self, resource=None, request_id=None, is_response=False):
|
||||
self.link = None
|
||||
if resource != None:
|
||||
@@ -1306,29 +1309,14 @@ class ResourceAdvertisement:
|
||||
# Flags
|
||||
self.f = 0x00 | self.x << 5 | self.p << 4 | self.u << 3 | self.s << 2 | self.c << 1 | self.e
|
||||
|
||||
def get_transfer_size(self):
|
||||
return self.t
|
||||
|
||||
def get_data_size(self):
|
||||
return self.d
|
||||
|
||||
def get_parts(self):
|
||||
return self.n
|
||||
|
||||
def get_segments(self):
|
||||
return self.l
|
||||
|
||||
def get_hash(self):
|
||||
return self.h
|
||||
|
||||
def is_compressed(self):
|
||||
return self.c
|
||||
|
||||
def has_metadata(self):
|
||||
return self.x
|
||||
|
||||
def get_link(self):
|
||||
return self.link
|
||||
def get_transfer_size(self): return self.t
|
||||
def get_data_size(self): return self.d
|
||||
def get_parts(self): return self.n
|
||||
def get_segments(self): return self.l
|
||||
def get_hash(self): return self.h
|
||||
def is_compressed(self): return self.c
|
||||
def has_metadata(self): return self.x
|
||||
def get_link(self): return self.link
|
||||
|
||||
def pack(self, segment=0):
|
||||
hashmap_start = segment*ResourceAdvertisement.HASHMAP_MAX_LEN
|
||||
@@ -1338,8 +1326,7 @@ class ResourceAdvertisement:
|
||||
for i in range(hashmap_start,hashmap_end):
|
||||
hashmap += self.m[i*Resource.MAPHASH_LEN:(i+1)*Resource.MAPHASH_LEN]
|
||||
|
||||
dictionary = {
|
||||
"t": self.t, # Transfer size
|
||||
dictionary = { "t": self.t, # Transfer size
|
||||
"d": self.d, # Data size
|
||||
"n": self.n, # Number of parts
|
||||
"h": self.h, # Resource hash
|
||||
@@ -1349,12 +1336,10 @@ class ResourceAdvertisement:
|
||||
"l": self.l, # Total segments
|
||||
"q": self.q, # Request ID
|
||||
"f": self.f, # Resource flags
|
||||
"m": hashmap
|
||||
}
|
||||
"m": hashmap }
|
||||
|
||||
return umsgpack.packb(dictionary)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def unpack(data):
|
||||
dictionary = umsgpack.unpackb(data)
|
||||
@@ -1378,4 +1363,6 @@ class ResourceAdvertisement:
|
||||
adv.p = True if ((adv.f >> 4) & 0x01) == 0x01 else False
|
||||
adv.x = True if ((adv.f >> 5) & 0x01) == 0x01 else False
|
||||
|
||||
if adv.t > Resource.MAX_EFFICIENT_SIZE*3: raise ValueError("Invalid transfer size")
|
||||
|
||||
return adv
|
||||
+242
-199
@@ -48,6 +48,7 @@ else:
|
||||
|
||||
from RNS.vendor.configobj import ConfigObj
|
||||
from threading import Lock
|
||||
import RNS.vendor.umsgpack as mp
|
||||
import configparser
|
||||
import multiprocessing.connection
|
||||
import importlib.util
|
||||
@@ -251,6 +252,8 @@ class Reticulum:
|
||||
|
||||
Reticulum.__network_identity = None
|
||||
Reticulum.__transport_enabled = False
|
||||
Reticulum.__static_transport_identity = False
|
||||
Reticulum.__local_hops_delta = False
|
||||
Reticulum.__link_mtu_discovery = Reticulum.LINK_MTU_DISCOVERY
|
||||
Reticulum.__remote_management_enabled = False
|
||||
Reticulum.__use_implicit_proof = True
|
||||
@@ -258,11 +261,15 @@ class Reticulum:
|
||||
Reticulum.__discovery_enabled = False
|
||||
Reticulum.__discover_interfaces = False
|
||||
Reticulum.__autoconnect_discovered_interfaces = False
|
||||
Reticulum.__autoconnect_interface_mode = None
|
||||
Reticulum.__autoconnect_interface_gravity = None
|
||||
Reticulum.__autoconnect_announces_to_internal = None
|
||||
Reticulum.__required_discovery_value = None
|
||||
Reticulum.__publish_blackhole = False
|
||||
Reticulum.__blackhole_update_interval = RNS.Discovery.BlackholeUpdater.UPDATE_INTERVAL
|
||||
Reticulum.__blackhole_sources = []
|
||||
Reticulum.__interface_sources = []
|
||||
Reticulum.__default_gravity = None
|
||||
Reticulum.__default_ar_target = None
|
||||
Reticulum.__default_ar_penalty = None
|
||||
Reticulum.__default_ar_grace = None
|
||||
@@ -346,7 +353,7 @@ class Reticulum:
|
||||
self.rpc_type = "AF_INET"
|
||||
|
||||
if self.rpc_key == None:
|
||||
self.rpc_key = RNS.Identity.full_hash(RNS.Transport.identity.get_private_key())
|
||||
self.rpc_key = RNS.Identity.full_hash(RNS.Transport.internal_identity().get_private_key())
|
||||
|
||||
if self.is_shared_instance:
|
||||
self.rpc_listener = multiprocessing.connection.Listener(self.rpc_addr, family=self.rpc_type, authkey=self.rpc_key)
|
||||
@@ -385,11 +392,7 @@ class Reticulum:
|
||||
def __start_local_interface(self):
|
||||
if self.share_instance:
|
||||
try:
|
||||
interface = LocalInterface.LocalServerInterface(
|
||||
RNS.Transport,
|
||||
self.local_interface_port,
|
||||
socket_path=self.local_socket_path
|
||||
)
|
||||
interface = LocalInterface.LocalServerInterface(RNS.Transport, self.local_interface_port, socket_path=self.local_socket_path)
|
||||
interface.OUT = True
|
||||
if hasattr(Reticulum, "_force_shared_instance_bitrate"):
|
||||
interface.bitrate = Reticulum._force_shared_instance_bitrate
|
||||
@@ -431,6 +434,7 @@ class Reticulum:
|
||||
Reticulum.__remote_management_enabled = False
|
||||
Reticulum.__allow_probes = False
|
||||
RNS.log("Connected to locally available Reticulum instance via: "+str(interface), RNS.LOG_DEBUG)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Local shared instance appears to be running, but it could not be connected", RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -455,7 +459,7 @@ class Reticulum:
|
||||
RNS.loglevel = int(value)
|
||||
if self.requested_verbosity != None: RNS.loglevel += self.requested_verbosity
|
||||
if RNS.loglevel < 0: RNS.loglevel = 0
|
||||
if RNS.loglevel > 7: RNS.loglevel = 7
|
||||
if RNS.loglevel > 8: RNS.loglevel = 8
|
||||
elif option == "logtimestamps":
|
||||
value = self.config["logging"].as_bool(option)
|
||||
RNS.logtimestamps = bool(value)
|
||||
@@ -499,6 +503,14 @@ class Reticulum:
|
||||
v = self.config["reticulum"].as_bool(option)
|
||||
if v == True: Reticulum.__transport_enabled = True
|
||||
|
||||
if option == "static_transport_identity":
|
||||
v = self.config["reticulum"].as_bool(option)
|
||||
if v == True: Reticulum.__static_transport_identity = True
|
||||
|
||||
if option == "local_hops_delta":
|
||||
v = self.config["reticulum"].as_bool(option)
|
||||
if v == True: Reticulum.__local_hops_delta = True
|
||||
|
||||
if option == "network_identity":
|
||||
if Reticulum.__network_identity == None:
|
||||
path = self.config["reticulum"][option]
|
||||
@@ -558,6 +570,10 @@ class Reticulum:
|
||||
if v == True: Reticulum.__use_implicit_proof = True
|
||||
if v == False: Reticulum.__use_implicit_proof = False
|
||||
|
||||
if option == "default_gravity":
|
||||
v = self.config["reticulum"].as_int(option)
|
||||
Reticulum.__default_gravity = v
|
||||
|
||||
if option == "discover_interfaces":
|
||||
v = self.config["reticulum"].as_bool(option)
|
||||
if v == True: Reticulum.__discover_interfaces = True
|
||||
@@ -600,6 +616,29 @@ class Reticulum:
|
||||
v = self.config["reticulum"].as_int(option)
|
||||
if v > 0: Reticulum.__autoconnect_discovered_interfaces = v
|
||||
|
||||
if option == "autoconnect_interface_mode":
|
||||
v = None; dmode = str(self.config["reticulum"]["autoconnect_interface_mode"]).lower()
|
||||
if dmode == "full": v = Interface.Interface.MODE_FULL
|
||||
elif dmode == "access_point": v = Interface.Interface.MODE_ACCESS_POINT
|
||||
elif dmode == "accesspoint": v = Interface.Interface.MODE_ACCESS_POINT
|
||||
elif dmode == "ap": v = Interface.Interface.MODE_ACCESS_POINT
|
||||
elif dmode == "pointtopoint": v = Interface.Interface.MODE_POINT_TO_POINT
|
||||
elif dmode == "ptp": v = Interface.Interface.MODE_POINT_TO_POINT
|
||||
elif dmode == "roaming": v = Interface.Interface.MODE_ROAMING
|
||||
elif dmode == "boundary": v = Interface.Interface.MODE_BOUNDARY
|
||||
elif dmode == "gateway": v = Interface.Interface.MODE_GATEWAY
|
||||
elif dmode == "gw": v = Interface.Interface.MODE_GATEWAY
|
||||
elif dmode == "internal": v = Interface.Interface.MODE_INTERNAL
|
||||
if v != None: Reticulum.__autoconnect_interface_mode = v
|
||||
|
||||
if option == "autoconnect_interface_gravity":
|
||||
v = self.config["reticulum"].as_int(option)
|
||||
Reticulum.__autoconnect_interface_gravity = v
|
||||
|
||||
if option == "autoconnect_announces_to_internal":
|
||||
v = self.config["reticulum"].as_bool(option)
|
||||
if v > 0: Reticulum.__autoconnect_announces_to_internal = v
|
||||
|
||||
if option == "default_ar_target":
|
||||
v = self.config["reticulum"].as_int(option)
|
||||
if v == 0: Reticulum.__default_ar_target = None
|
||||
@@ -668,6 +707,9 @@ class Reticulum:
|
||||
self.shared_instance_type = "tcp"
|
||||
self.use_af_unix = False
|
||||
|
||||
if self.shared_instance_type == "tcp":
|
||||
self.local_socket_path = None
|
||||
|
||||
if self.local_socket_path == None and self.use_af_unix:
|
||||
self.local_socket_path = "default"
|
||||
|
||||
@@ -706,6 +748,8 @@ class Reticulum:
|
||||
interface_mode = Interface.Interface.MODE_BOUNDARY
|
||||
elif c["mode"] == "gateway" or c["mode"] == "gw":
|
||||
interface_mode = Interface.Interface.MODE_GATEWAY
|
||||
elif c["mode"] == "internal":
|
||||
interface_mode = Interface.Interface.MODE_INTERNAL
|
||||
|
||||
elif "mode" in c:
|
||||
c["mode"] = str(c["mode"]).lower()
|
||||
@@ -721,27 +765,27 @@ class Reticulum:
|
||||
interface_mode = Interface.Interface.MODE_BOUNDARY
|
||||
elif c["mode"] == "gateway" or c["mode"] == "gw":
|
||||
interface_mode = Interface.Interface.MODE_GATEWAY
|
||||
elif c["mode"] == "internal":
|
||||
interface_mode = Interface.Interface.MODE_INTERNAL
|
||||
|
||||
gravity = self._default_gravity()
|
||||
if "gravity" in c: gravity = c.as_int("gravity")
|
||||
|
||||
ifac_size = None
|
||||
if "ifac_size" in c:
|
||||
if c.as_int("ifac_size") >= Reticulum.IFAC_MIN_SIZE*8:
|
||||
ifac_size = c.as_int("ifac_size")//8
|
||||
if c.as_int("ifac_size") >= Reticulum.IFAC_MIN_SIZE*8: ifac_size = c.as_int("ifac_size")//8
|
||||
|
||||
ifac_netname = None
|
||||
if "networkname" in c:
|
||||
if c["networkname"] != "":
|
||||
ifac_netname = c["networkname"]
|
||||
if c["networkname"] != "": ifac_netname = c["networkname"]
|
||||
if "network_name" in c:
|
||||
if c["network_name"] != "":
|
||||
ifac_netname = c["network_name"]
|
||||
if c["network_name"] != "": ifac_netname = c["network_name"]
|
||||
|
||||
ifac_netkey = None
|
||||
if "passphrase" in c:
|
||||
if c["passphrase"] != "":
|
||||
ifac_netkey = c["passphrase"]
|
||||
if c["passphrase"] != "": ifac_netkey = c["passphrase"]
|
||||
if "pass_phrase" in c:
|
||||
if c["pass_phrase"] != "":
|
||||
ifac_netkey = c["pass_phrase"]
|
||||
if c["pass_phrase"] != "": ifac_netkey = c["pass_phrase"]
|
||||
|
||||
ingress_control = True
|
||||
if "ingress_control" in c: ingress_control = c.as_bool("ingress_control")
|
||||
@@ -770,29 +814,22 @@ class Reticulum:
|
||||
|
||||
configured_bitrate = None
|
||||
if "bitrate" in c:
|
||||
if c.as_int("bitrate") >= Reticulum.MINIMUM_BITRATE:
|
||||
configured_bitrate = c.as_int("bitrate")
|
||||
if c.as_int("bitrate") >= Reticulum.MINIMUM_BITRATE: configured_bitrate = c.as_int("bitrate")
|
||||
|
||||
announce_rate_target = None
|
||||
if "announce_rate_target" in c:
|
||||
if c.as_int("announce_rate_target") > 0:
|
||||
announce_rate_target = c.as_int("announce_rate_target")
|
||||
if c.as_int("announce_rate_target") > 0: announce_rate_target = c.as_int("announce_rate_target")
|
||||
|
||||
announce_rate_grace = None
|
||||
if "announce_rate_grace" in c:
|
||||
if c.as_int("announce_rate_grace") >= 0:
|
||||
announce_rate_grace = c.as_int("announce_rate_grace")
|
||||
if c.as_int("announce_rate_grace") >= 0: announce_rate_grace = c.as_int("announce_rate_grace")
|
||||
|
||||
announce_rate_penalty = None
|
||||
if "announce_rate_penalty" in c:
|
||||
if c.as_int("announce_rate_penalty") >= 0:
|
||||
announce_rate_penalty = c.as_int("announce_rate_penalty")
|
||||
if c.as_int("announce_rate_penalty") >= 0: announce_rate_penalty = c.as_int("announce_rate_penalty")
|
||||
|
||||
if announce_rate_target != None and announce_rate_grace == None:
|
||||
announce_rate_grace = 0
|
||||
|
||||
if announce_rate_target != None and announce_rate_penalty == None:
|
||||
announce_rate_penalty = 0
|
||||
if announce_rate_target != None and announce_rate_grace == None: announce_rate_grace = 0
|
||||
if announce_rate_target != None and announce_rate_penalty == None: announce_rate_penalty = 0
|
||||
|
||||
announce_cap = Reticulum.ANNOUNCE_CAP/100.0
|
||||
if "announce_cap" in c:
|
||||
@@ -802,6 +839,15 @@ class Reticulum:
|
||||
bootstrap_only = False
|
||||
if "bootstrap_only" in c: bootstrap_only = c.as_bool("bootstrap_only")
|
||||
|
||||
recursive_prs = False
|
||||
if "recursive_prs" in c: recursive_prs = c.as_bool("recursive_prs")
|
||||
|
||||
announces_from_internal = True
|
||||
if "announces_from_internal" in c: announces_from_internal = c.as_bool("announces_from_internal")
|
||||
|
||||
announces_to_internal = None
|
||||
if "announces_to_internal" in c: announces_to_internal = c.as_bool("announces_to_internal")
|
||||
|
||||
ignore_config_warnings = False
|
||||
if "ignore_config_warnings" in c: ignore_config_warnings = c.as_bool("ignore_config_warnings")
|
||||
|
||||
@@ -820,6 +866,7 @@ class Reticulum:
|
||||
latitude = None
|
||||
longitude = None
|
||||
height = None
|
||||
discovery_location = None
|
||||
discovery_frequency = None
|
||||
discovery_bandwidth = None
|
||||
discovery_modulation = None
|
||||
@@ -837,6 +884,7 @@ class Reticulum:
|
||||
if "discovery_encrypt" in c: discovery_encrypt = c.as_bool("discovery_encrypt")
|
||||
if "reachable_on" in c: reachable_on = c["reachable_on"]
|
||||
if "publish_ifac" in c: publish_ifac = c.as_bool("publish_ifac")
|
||||
if "location_cmd" in c: discovery_location = c["location_cmd"]
|
||||
if "latitude" in c: latitude = c.as_float("latitude")
|
||||
if "longitude" in c: longitude = c.as_float("longitude")
|
||||
if "height" in c: height = c.as_float("height")
|
||||
@@ -844,14 +892,14 @@ class Reticulum:
|
||||
if "discovery_bandwidth" in c: discovery_bandwidth = c.as_int("discovery_bandwidth")
|
||||
if "discovery_modulation" in c: discovery_modulation = c.as_int("discovery_modulation")
|
||||
|
||||
if not interface_mode in [Interface.Interface.MODE_GATEWAY, Interface.Interface.MODE_ACCESS_POINT]:
|
||||
if not interface_mode in [Interface.Interface.MODE_GATEWAY, Interface.Interface.MODE_ACCESS_POINT, Interface.Interface.MODE_INTERNAL]:
|
||||
if not ignore_config_warnings:
|
||||
if c["type"] in ["RNodeInterface", "RNodeMultiInterface"]:
|
||||
interface_mode = Interface.Interface.MODE_ACCESS_POINT
|
||||
RNS.log(f"Discovery enabled on interface {name} without gateway or AP mode. Auto-configured to AP mode.", RNS.LOG_NOTICE)
|
||||
RNS.log(f"Discovery enabled on interface {name} without gateway, internal or AP mode. Auto-configured to AP mode.", RNS.LOG_NOTICE)
|
||||
else:
|
||||
interface_mode = Interface.Interface.MODE_GATEWAY
|
||||
RNS.log(f"Discovery enabled on interface {name} without gateway or AP mode. Auto-configured to gateway mode.", RNS.LOG_NOTICE)
|
||||
RNS.log(f"Discovery enabled on interface {name} without gateway, internal or AP mode. Auto-configured to gateway mode.", RNS.LOG_NOTICE)
|
||||
|
||||
try:
|
||||
def interface_post_init(interface):
|
||||
@@ -860,6 +908,7 @@ class Reticulum:
|
||||
else: interface.OUT = True
|
||||
|
||||
interface.mode = interface_mode
|
||||
interface.gravity = gravity
|
||||
interface.announce_cap = announce_cap
|
||||
interface.bootstrap_only = bootstrap_only
|
||||
if configured_bitrate: interface.bitrate = configured_bitrate
|
||||
@@ -875,6 +924,7 @@ class Reticulum:
|
||||
interface.discovery_name = discovery_name
|
||||
interface.discovery_encrypt = discovery_encrypt
|
||||
interface.discovery_stamp_value = discovery_stamp_value
|
||||
interface.discovery_location = discovery_location
|
||||
interface.discovery_latitude = latitude
|
||||
interface.discovery_longitude = longitude
|
||||
interface.discovery_height = height
|
||||
@@ -882,10 +932,14 @@ class Reticulum:
|
||||
interface.discovery_bandwidth = discovery_bandwidth
|
||||
interface.discovery_modulation = discovery_modulation
|
||||
|
||||
interface.recursive_prs = recursive_prs
|
||||
interface.announces_from_internal = announces_from_internal
|
||||
interface.announces_to_internal = announces_to_internal
|
||||
interface.announce_rate_target = announce_rate_target
|
||||
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
|
||||
@@ -911,12 +965,8 @@ class Reticulum:
|
||||
ifac_origin += RNS.Identity.full_hash(interface.ifac_netkey.encode("utf-8"))
|
||||
|
||||
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
|
||||
interface.ifac_key = RNS.Cryptography.hkdf(
|
||||
length=64,
|
||||
derive_from=ifac_origin_hash,
|
||||
salt=self.ifac_salt,
|
||||
context=None
|
||||
)
|
||||
interface.ifac_key = RNS.Cryptography.hkdf(length=64, derive_from=ifac_origin_hash,
|
||||
salt=self.ifac_salt, context=None)
|
||||
|
||||
interface.ifac_identity = RNS.Identity.from_bytes(interface.ifac_key)
|
||||
interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key))
|
||||
@@ -1039,13 +1089,17 @@ class Reticulum:
|
||||
RNS.trace_exception(e)
|
||||
RNS.panic()
|
||||
|
||||
def _add_interface(self, interface, mode = None, configured_bitrate=None, ifac_size=None, ifac_netname=None, ifac_netkey=None,
|
||||
announce_cap=None, announce_rate_target=None, announce_rate_grace=None, announce_rate_penalty=None, bootstrap_only=False):
|
||||
def _add_interface(self, interface, mode=None, gravity=None, configured_bitrate=None, ifac_size=None, ifac_netname=None, ifac_netkey=None,
|
||||
announce_cap=None, announce_rate_target=None, announce_rate_grace=None, announce_rate_penalty=None,
|
||||
bootstrap_only=False, recursive_prs=False, announces_from_internal=True, announces_to_internal=None):
|
||||
|
||||
if not self.is_connected_to_shared_instance:
|
||||
if interface != None and issubclass(type(interface), RNS.Interfaces.Interface.Interface):
|
||||
|
||||
if mode == None: mode = Interface.Interface.MODE_FULL
|
||||
if gravity == None: gravity = self._default_gravity()
|
||||
interface.mode = mode
|
||||
interface.gravity = gravity
|
||||
interface.OUT = True
|
||||
|
||||
if configured_bitrate: interface.bitrate = configured_bitrate
|
||||
@@ -1055,6 +1109,9 @@ class Reticulum:
|
||||
if ifac_size != None: interface.ifac_size = ifac_size
|
||||
else: interface.ifac_size = interface.DEFAULT_IFAC_SIZE
|
||||
|
||||
interface.recursive_prs = recursive_prs
|
||||
interface.announces_from_internal = announces_from_internal
|
||||
interface.announces_to_internal = announces_to_internal
|
||||
interface.announce_cap = announce_cap if announce_cap != None else Reticulum.ANNOUNCE_CAP/100.0
|
||||
interface.announce_rate_target = announce_rate_target
|
||||
interface.announce_rate_grace = announce_rate_grace
|
||||
@@ -1073,12 +1130,8 @@ class Reticulum:
|
||||
ifac_origin += RNS.Identity.full_hash(interface.ifac_netkey.encode("utf-8"))
|
||||
|
||||
ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
|
||||
interface.ifac_key = RNS.Cryptography.hkdf(
|
||||
length=64,
|
||||
derive_from=ifac_origin_hash,
|
||||
salt=self.ifac_salt,
|
||||
context=None
|
||||
)
|
||||
interface.ifac_key = RNS.Cryptography.hkdf(length=64, derive_from=ifac_origin_hash,
|
||||
salt=self.ifac_salt, context=None)
|
||||
|
||||
interface.ifac_identity = RNS.Identity.from_bytes(interface.ifac_key)
|
||||
interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key))
|
||||
@@ -1086,6 +1139,9 @@ class Reticulum:
|
||||
RNS.Transport.add_interface(interface)
|
||||
interface.final_init()
|
||||
|
||||
def _default_gravity(self):
|
||||
return self.__default_gravity or RNS.Interfaces.Interface.Interface.DEFAULT_GRAVITY
|
||||
|
||||
def _default_ar_target(self):
|
||||
return self.__default_ar_target or RNS.Interfaces.Interface.Interface.DEFAULT_AR_TARGET
|
||||
|
||||
@@ -1175,64 +1231,66 @@ class Reticulum:
|
||||
self.config = ConfigObj(__default_rns_config__)
|
||||
self.config.filename = Reticulum.configpath
|
||||
|
||||
if not os.path.isdir(Reticulum.configdir):
|
||||
os.makedirs(Reticulum.configdir)
|
||||
if not os.path.isdir(Reticulum.configdir): os.makedirs(Reticulum.configdir)
|
||||
self.config.write()
|
||||
|
||||
def rpc_return(self, connection, response):
|
||||
connection.send_bytes(mp.packb(response))
|
||||
|
||||
def rpc_loop(self):
|
||||
while RNS.Transport._should_run:
|
||||
try:
|
||||
rpc_connection = self.rpc_listener.accept()
|
||||
call = rpc_connection.recv()
|
||||
conn = self.rpc_listener.accept()
|
||||
call = mp.unpackb(conn.recv_bytes())
|
||||
|
||||
if "get" in call:
|
||||
path = call["get"]
|
||||
|
||||
if path == "path_table":
|
||||
mh = call["max_hops"]
|
||||
rpc_connection.send(self.get_path_table(max_hops=mh))
|
||||
self.rpc_return(conn, self.get_path_table(max_hops=mh))
|
||||
|
||||
if path == "interface_stats": rpc_connection.send(self.get_interface_stats())
|
||||
if path == "rate_table": rpc_connection.send(self.get_rate_table())
|
||||
if path == "next_hop_if_name": rpc_connection.send(self.get_next_hop_if_name(call["destination_hash"]))
|
||||
if path == "next_hop": rpc_connection.send(self.get_next_hop(call["destination_hash"]))
|
||||
if path == "first_hop_timeout": rpc_connection.send(self.get_first_hop_timeout(call["destination_hash"]))
|
||||
if path == "link_count": rpc_connection.send(self.get_link_count())
|
||||
if path == "packet_rssi": rpc_connection.send(self.get_packet_rssi(call["packet_hash"]))
|
||||
if path == "packet_snr": rpc_connection.send(self.get_packet_snr(call["packet_hash"]))
|
||||
if path == "packet_q": rpc_connection.send(self.get_packet_q(call["packet_hash"]))
|
||||
if path == "blackholed_identities": rpc_connection.send(self.get_blackholed_identities())
|
||||
if path == "is_blackholed": rpc_connection.send(self.is_blackholed(call["identity_hash"]))
|
||||
if path == "interface_stats": self.rpc_return(conn, self.get_interface_stats())
|
||||
if path == "rate_table": self.rpc_return(conn, self.get_rate_table())
|
||||
if path == "next_hop_if_name": self.rpc_return(conn, self.get_next_hop_if_name(call["destination_hash"]))
|
||||
if path == "next_hop": self.rpc_return(conn, self.get_next_hop(call["destination_hash"]))
|
||||
if path == "first_hop_timeout": self.rpc_return(conn, self.get_first_hop_timeout(call["destination_hash"]))
|
||||
if path == "link_count": self.rpc_return(conn, self.get_link_count())
|
||||
if path == "packet_rssi": self.rpc_return(conn, self.get_packet_rssi(call["packet_hash"]))
|
||||
if path == "packet_snr": self.rpc_return(conn, self.get_packet_snr(call["packet_hash"]))
|
||||
if path == "packet_q": self.rpc_return(conn, self.get_packet_q(call["packet_hash"]))
|
||||
if path == "blackholed_identities": self.rpc_return(conn, self.get_blackholed_identities())
|
||||
if path == "is_blackholed": self.rpc_return(conn, self.is_blackholed(call["identity_hash"]))
|
||||
|
||||
if "drop" in call:
|
||||
path = call["drop"]
|
||||
if path == "path": rpc_connection.send(self.drop_path(call["destination_hash"]))
|
||||
if path == "all_via": rpc_connection.send(self.drop_all_via(call["destination_hash"]))
|
||||
if path == "announce_queues": rpc_connection.send(self.drop_announce_queues())
|
||||
if path == "path": self.rpc_return(conn, self.drop_path(call["destination_hash"]))
|
||||
if path == "all_via": self.rpc_return(conn, self.drop_all_via(call["destination_hash"]))
|
||||
if path == "announce_queues": self.rpc_return(conn, self.drop_announce_queues())
|
||||
|
||||
if "blackhole_identity" in call:
|
||||
identity_hash = call["blackhole_identity"]
|
||||
until = call["until"]
|
||||
reason = call["reason"]
|
||||
rpc_connection.send(self.blackhole_identity(identity_hash, until=until, reason=reason))
|
||||
self.rpc_return(conn, self.blackhole_identity(identity_hash, until=until, reason=reason))
|
||||
|
||||
if "unblackhole_identity" in call:
|
||||
identity_hash = call["unblackhole_identity"]
|
||||
rpc_connection.send(self.unblackhole_identity(identity_hash))
|
||||
self.rpc_return(conn, self.unblackhole_identity(identity_hash))
|
||||
|
||||
if "destination_data" in call:
|
||||
operation = call["destination_data"]
|
||||
destination_hash = call["destination_hash"]
|
||||
if operation == "used": rpc_connection.send(self._used_destination_data(destination_hash))
|
||||
elif operation == "retain": rpc_connection.send(self._retain_destination_data(destination_hash))
|
||||
elif operation == "unretain": rpc_connection.send(self._unretain_destination_data(destination_hash))
|
||||
if operation == "used": self.rpc_return(conn, self._used_destination_data(destination_hash))
|
||||
elif operation == "retain": self.rpc_return(conn, self._retain_destination_data(destination_hash))
|
||||
elif operation == "unretain": self.rpc_return(conn, self._unretain_destination_data(destination_hash))
|
||||
|
||||
if "identity_data" in call:
|
||||
operation = call["identity_data"]
|
||||
identity_hash = call["identity_hash"]
|
||||
if operation == "retain": rpc_connection.send(self._retain_identity(identity_hash))
|
||||
if operation == "retain": self.rpc_return(conn, self._retain_identity(identity_hash))
|
||||
|
||||
rpc_connection.close()
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("An error ocurred while handling RPC call from local client: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -1243,8 +1301,8 @@ class Reticulum:
|
||||
if self.is_connected_to_shared_instance:
|
||||
try:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "used", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"destination_data": "used", "destination_hash": destination_hash}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
@@ -1257,8 +1315,8 @@ class Reticulum:
|
||||
if self.is_connected_to_shared_instance:
|
||||
try:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "retain", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"destination_data": "retain", "destination_hash": destination_hash}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
@@ -1271,8 +1329,8 @@ class Reticulum:
|
||||
if self.is_connected_to_shared_instance:
|
||||
try:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"destination_data": "unretain", "destination_hash": destination_hash})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"destination_data": "unretain", "destination_hash": destination_hash}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
@@ -1288,8 +1346,8 @@ class Reticulum:
|
||||
if self.is_connected_to_shared_instance:
|
||||
try:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"identity_data": "retain", "identity_hash": identity_hash})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"identity_data": "retain", "identity_hash": identity_hash}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
@@ -1301,34 +1359,29 @@ class Reticulum:
|
||||
def get_interface_stats(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "interface_stats"})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "interface_stats"}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
interfaces = []
|
||||
for interface in RNS.Transport.interfaces:
|
||||
ifstats = {}
|
||||
|
||||
if hasattr(interface, "clients"):
|
||||
ifstats["clients"] = interface.clients
|
||||
else:
|
||||
ifstats["clients"] = None
|
||||
if hasattr(interface, "clients"): ifstats["clients"] = interface.clients
|
||||
else: ifstats["clients"] = None
|
||||
|
||||
if hasattr(interface, "parent_interface") and interface.parent_interface != None:
|
||||
ifstats["parent_interface_name"] = str(interface.parent_interface)
|
||||
ifstats["parent_interface_hash"] = interface.parent_interface.get_hash()
|
||||
|
||||
if hasattr(interface, "i2p") and hasattr(interface, "connectable"):
|
||||
if interface.connectable:
|
||||
ifstats["i2p_connectable"] = True
|
||||
else:
|
||||
ifstats["i2p_connectable"] = False
|
||||
if interface.connectable: ifstats["i2p_connectable"] = True
|
||||
else: ifstats["i2p_connectable"] = False
|
||||
|
||||
if hasattr(interface, "b32"):
|
||||
if interface.b32 != None:
|
||||
ifstats["i2p_b32"] = interface.b32+".b32.i2p"
|
||||
else:
|
||||
ifstats["i2p_b32"] = None
|
||||
if interface.b32 != None: ifstats["i2p_b32"] = interface.b32+".b32.i2p"
|
||||
else: ifstats["i2p_b32"] = None
|
||||
|
||||
if hasattr(interface, "i2p_tunnel_state"):
|
||||
if interface.i2p_tunnel_state != None:
|
||||
@@ -1343,37 +1396,20 @@ class Reticulum:
|
||||
else:
|
||||
ifstats["tunnelstate"] = None
|
||||
|
||||
if hasattr(interface, "r_airtime_short"):
|
||||
ifstats["airtime_short"] = interface.r_airtime_short
|
||||
|
||||
if hasattr(interface, "r_airtime_long"):
|
||||
ifstats["airtime_long"] = interface.r_airtime_long
|
||||
|
||||
if hasattr(interface, "r_channel_load_short"):
|
||||
ifstats["channel_load_short"] = interface.r_channel_load_short
|
||||
|
||||
if hasattr(interface, "r_channel_load_long"):
|
||||
ifstats["channel_load_long"] = interface.r_channel_load_long
|
||||
|
||||
if hasattr(interface, "r_noise_floor"):
|
||||
ifstats["noise_floor"] = interface.r_noise_floor
|
||||
|
||||
if hasattr(interface, "r_interference"):
|
||||
ifstats["interference"] = interface.r_interference
|
||||
if hasattr(interface, "r_airtime_short"): ifstats["airtime_short"] = interface.r_airtime_short
|
||||
if hasattr(interface, "r_airtime_long"): ifstats["airtime_long"] = interface.r_airtime_long
|
||||
if hasattr(interface, "r_channel_load_short"): ifstats["channel_load_short"] = interface.r_channel_load_short
|
||||
if hasattr(interface, "r_channel_load_long"): ifstats["channel_load_long"] = interface.r_channel_load_long
|
||||
if hasattr(interface, "r_noise_floor"): ifstats["noise_floor"] = interface.r_noise_floor
|
||||
if hasattr(interface, "r_interference"): ifstats["interference"] = interface.r_interference
|
||||
if hasattr(interface, "cpu_temp"): ifstats["cpu_temp"] = interface.cpu_temp
|
||||
if hasattr(interface, "cpu_load"): ifstats["cpu_load"] = interface.cpu_load
|
||||
if hasattr(interface, "mem_load"): ifstats["mem_load"] = interface.mem_load
|
||||
|
||||
if hasattr(interface, "r_interference_l") and type(interface.r_interference_l) == list:
|
||||
ifstats["interference_last_ts"] = interface.r_interference_l[0]
|
||||
ifstats["interference_last_dbm"] = interface.r_interference_l[1]
|
||||
|
||||
if hasattr(interface, "cpu_temp"):
|
||||
ifstats["cpu_temp"] = interface.cpu_temp
|
||||
|
||||
if hasattr(interface, "cpu_load"):
|
||||
ifstats["cpu_load"] = interface.cpu_load
|
||||
|
||||
if hasattr(interface, "mem_load"):
|
||||
ifstats["mem_load"] = interface.mem_load
|
||||
|
||||
if hasattr(interface, "switch_id"):
|
||||
if interface.switch_id != None: ifstats["switch_id"] = RNS.hexrep(interface.switch_id)
|
||||
else: ifstats["switch_id"] = None
|
||||
@@ -1387,39 +1423,26 @@ class Reticulum:
|
||||
else: ifstats["endpoint_id"] = None
|
||||
|
||||
if hasattr(interface, "r_battery_state"):
|
||||
if interface.r_battery_state != 0x00:
|
||||
ifstats["battery_state"] = interface.get_battery_state_string()
|
||||
|
||||
if hasattr(interface, "r_battery_percent"):
|
||||
ifstats["battery_percent"] = interface.r_battery_percent
|
||||
if interface.r_battery_state != 0x00: ifstats["battery_state"] = interface.get_battery_state_string()
|
||||
if hasattr(interface, "r_battery_percent"): ifstats["battery_percent"] = interface.r_battery_percent
|
||||
|
||||
if hasattr(interface, "bitrate"):
|
||||
if interface.bitrate != None:
|
||||
ifstats["bitrate"] = interface.bitrate
|
||||
else:
|
||||
ifstats["bitrate"] = None
|
||||
if interface.bitrate != None: ifstats["bitrate"] = interface.bitrate
|
||||
else: ifstats["bitrate"] = None
|
||||
|
||||
if hasattr(interface, "current_rx_speed"):
|
||||
if interface.current_rx_speed != None:
|
||||
ifstats["rxs"] = interface.current_rx_speed
|
||||
else:
|
||||
ifstats["rxs"] = 0
|
||||
else:
|
||||
ifstats["rxs"] = 0
|
||||
if interface.current_rx_speed != None: ifstats["rxs"] = interface.current_rx_speed
|
||||
else: ifstats["rxs"] = 0
|
||||
else: ifstats["rxs"] = 0
|
||||
|
||||
if hasattr(interface, "current_tx_speed"):
|
||||
if interface.current_tx_speed != None:
|
||||
ifstats["txs"] = interface.current_tx_speed
|
||||
else:
|
||||
ifstats["txs"] = 0
|
||||
else:
|
||||
ifstats["txs"] = 0
|
||||
if interface.current_tx_speed != None: ifstats["txs"] = interface.current_tx_speed
|
||||
else: ifstats["txs"] = 0
|
||||
else: ifstats["txs"] = 0
|
||||
|
||||
if hasattr(interface, "peers"):
|
||||
if interface.peers != None:
|
||||
ifstats["peers"] = len(interface.peers)
|
||||
else:
|
||||
ifstats["peers"] = None
|
||||
if interface.peers != None: ifstats["peers"] = len(interface.peers)
|
||||
else: ifstats["peers"] = None
|
||||
|
||||
if hasattr(interface, "ifac_signature"):
|
||||
ifstats["ifac_signature"] = interface.ifac_signature
|
||||
@@ -1430,16 +1453,18 @@ class Reticulum:
|
||||
ifstats["ifac_size"] = None
|
||||
ifstats["ifac_netname"] = None
|
||||
|
||||
if hasattr(interface, "autoconnect_source"):
|
||||
ifstats["autoconnect_source"] = interface.autoconnect_source
|
||||
else:
|
||||
ifstats["autoconnect_source"] = None
|
||||
if hasattr(interface, "autoconnect_source"): ifstats["autoconnect_source"] = interface.autoconnect_source
|
||||
else: ifstats["autoconnect_source"] = None
|
||||
|
||||
if hasattr(interface, "announce_queue"):
|
||||
if interface.announce_queue != None:
|
||||
ifstats["announce_queue"] = len(interface.announce_queue)
|
||||
else:
|
||||
ifstats["announce_queue"] = None
|
||||
if interface.announce_queue != None: ifstats["announce_queue"] = len(interface.announce_queue)
|
||||
else: ifstats["announce_queue"] = None
|
||||
|
||||
if hasattr(interface, "blocked_ip_count"):
|
||||
ifstats["blocked_ips"] = interface.blocked_ip_count
|
||||
|
||||
if hasattr(interface, "blocked_ip_list"):
|
||||
ifstats["blocked_ip_list"] = interface.blocked_ip_list
|
||||
|
||||
ifstats["name"] = str(interface)
|
||||
ifstats["short_name"] = str(interface.name)
|
||||
@@ -1461,6 +1486,8 @@ class Reticulum:
|
||||
ifstats["pr_burst_activated"] = interface.ic_pr_burst_activated
|
||||
ifstats["status"] = interface.online
|
||||
ifstats["mode"] = interface.mode
|
||||
ifstats["gravity"] = interface.gravity
|
||||
ifstats["announces_to_internal"] = interface.announces_to_internal
|
||||
|
||||
interfaces.append(ifstats)
|
||||
|
||||
@@ -1470,14 +1497,13 @@ class Reticulum:
|
||||
stats["txb"] = RNS.Transport.traffic_txb
|
||||
stats["rxs"] = RNS.Transport.speed_rx
|
||||
stats["txs"] = RNS.Transport.speed_tx
|
||||
|
||||
if Reticulum.transport_enabled():
|
||||
stats["transport_id"] = RNS.Transport.identity.hash
|
||||
stats["network_id"] = RNS.Transport.network_identity.hash if RNS.Transport.network_identity else None
|
||||
stats["transport_uptime"] = time.time()-RNS.Transport.start_time
|
||||
if Reticulum.probe_destination_enabled():
|
||||
stats["probe_responder"] = RNS.Transport.probe_destination.hash
|
||||
else:
|
||||
stats["probe_responder"] = None
|
||||
if Reticulum.probe_destination_enabled(): stats["probe_responder"] = RNS.Transport.probe_destination.hash
|
||||
else: stats["probe_responder"] = None
|
||||
|
||||
if importlib.util.find_spec('psutil') != None:
|
||||
import psutil
|
||||
@@ -1491,8 +1517,8 @@ class Reticulum:
|
||||
def get_path_table(self, max_hops=None):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "path_table", "max_hops": max_hops})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "path_table", "max_hops": max_hops}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1500,14 +1526,10 @@ class Reticulum:
|
||||
for dst_hash in RNS.Transport.path_table:
|
||||
path_hops = RNS.Transport.path_table[dst_hash][2]
|
||||
if max_hops == None or path_hops <= max_hops:
|
||||
entry = {
|
||||
"hash": dst_hash,
|
||||
"timestamp": RNS.Transport.path_table[dst_hash][0],
|
||||
"via": RNS.Transport.path_table[dst_hash][1],
|
||||
"hops": path_hops,
|
||||
entry = { "hash": dst_hash, "timestamp": RNS.Transport.path_table[dst_hash][0],
|
||||
"via": RNS.Transport.path_table[dst_hash][1], "hops": path_hops,
|
||||
"expires": RNS.Transport.path_table[dst_hash][3],
|
||||
"interface": str(RNS.Transport.path_table[dst_hash][5]),
|
||||
}
|
||||
"interface": str(RNS.Transport.path_table[dst_hash][5]) }
|
||||
path_table.append(entry)
|
||||
|
||||
return path_table
|
||||
@@ -1515,8 +1537,8 @@ class Reticulum:
|
||||
def get_rate_table(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "rate_table"})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "rate_table"}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1536,8 +1558,8 @@ class Reticulum:
|
||||
def drop_path(self, destination):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"drop": "path", "destination_hash": destination})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"drop": "path", "destination_hash": destination}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1546,8 +1568,8 @@ class Reticulum:
|
||||
def drop_all_via(self, transport_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"drop": "all_via", "destination_hash": transport_hash})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"drop": "all_via", "destination_hash": transport_hash}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1562,8 +1584,8 @@ class Reticulum:
|
||||
def drop_announce_queues(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"drop": "announce_queues"})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"drop": "announce_queues"}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1572,8 +1594,8 @@ class Reticulum:
|
||||
def get_next_hop_if_name(self, destination):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "next_hop_if_name", "destination_hash": destination})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "next_hop_if_name", "destination_hash": destination}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1583,8 +1605,8 @@ class Reticulum:
|
||||
if self.is_connected_to_shared_instance:
|
||||
try:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "first_hop_timeout", "destination_hash": destination})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "first_hop_timeout", "destination_hash": destination}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
|
||||
if self.is_connected_to_shared_instance and hasattr(self, "_force_shared_instance_bitrate") and self._force_shared_instance_bitrate:
|
||||
simulated_latency = ((1/self._force_shared_instance_bitrate)*8)*RNS.Reticulum.MTU
|
||||
@@ -1602,8 +1624,8 @@ class Reticulum:
|
||||
def get_next_hop(self, destination):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "next_hop", "destination_hash": destination})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "next_hop", "destination_hash": destination}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
|
||||
return response
|
||||
|
||||
@@ -1613,8 +1635,8 @@ class Reticulum:
|
||||
def get_link_count(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "link_count"})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "link_count"}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1623,8 +1645,8 @@ class Reticulum:
|
||||
def get_packet_rssi(self, packet_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "packet_rssi", "packet_hash": packet_hash})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "packet_rssi", "packet_hash": packet_hash}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1637,8 +1659,8 @@ class Reticulum:
|
||||
def get_packet_snr(self, packet_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "packet_snr", "packet_hash": packet_hash})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "packet_snr", "packet_hash": packet_hash}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1651,8 +1673,8 @@ class Reticulum:
|
||||
def get_packet_q(self, packet_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "packet_q", "packet_hash": packet_hash})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "packet_q", "packet_hash": packet_hash}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1674,8 +1696,8 @@ class Reticulum:
|
||||
def get_blackholed_identities(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "blackholed_identities"})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "blackholed_identities"}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else: return RNS.Transport.blackholed_identities
|
||||
@@ -1688,8 +1710,8 @@ class Reticulum:
|
||||
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "is_blackholed", "identity_hash": identity_hash})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"get": "is_blackholed", "identity_hash": identity_hash}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else: return identity_hash in RNS.Transport.blackholed_identities
|
||||
@@ -1699,8 +1721,8 @@ class Reticulum:
|
||||
else:
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"blackhole_identity": identity_hash, "until": until, "reason": reason})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"blackhole_identity": identity_hash, "until": until, "reason": reason}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else: return RNS.Transport.blackhole_identity(identity_hash, until=until, reason=reason)
|
||||
@@ -1710,8 +1732,8 @@ class Reticulum:
|
||||
else:
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"unblackhole_identity": identity_hash})
|
||||
response = rpc_connection.recv()
|
||||
rpc_connection.send_bytes(mp.packb({"unblackhole_identity": identity_hash}))
|
||||
response = mp.unpackb(rpc_connection.recv_bytes())
|
||||
return response
|
||||
|
||||
else: return RNS.Transport.unblackhole_identity(identity_hash)
|
||||
@@ -1827,10 +1849,30 @@ class Reticulum:
|
||||
def should_autoconnect_discovered_interfaces():
|
||||
return Reticulum.__autoconnect_discovered_interfaces > 0
|
||||
|
||||
@staticmethod
|
||||
def autoconnect_interface_mode():
|
||||
return Reticulum.__autoconnect_interface_mode
|
||||
|
||||
@staticmethod
|
||||
def autoconnect_interface_gravity():
|
||||
return Reticulum.__autoconnect_interface_gravity
|
||||
|
||||
@staticmethod
|
||||
def autoconnect_announces_to_internal():
|
||||
return Reticulum.__autoconnect_announces_to_internal
|
||||
|
||||
@staticmethod
|
||||
def max_autoconnected_interfaces():
|
||||
return Reticulum.__autoconnect_discovered_interfaces
|
||||
|
||||
@staticmethod
|
||||
def static_transport_identity():
|
||||
return Reticulum.__static_transport_identity
|
||||
|
||||
@staticmethod
|
||||
def local_hops_delta():
|
||||
return Reticulum.__local_hops_delta
|
||||
|
||||
# Default configuration file:
|
||||
__default_rns_config__ = '''# This is the default Reticulum config file.
|
||||
# You should probably edit it to include any additional,
|
||||
@@ -1918,7 +1960,7 @@ instance_name = default
|
||||
|
||||
|
||||
[logging]
|
||||
# Valid log levels are 0 through 7:
|
||||
# Valid log levels are 0 through 8:
|
||||
# 0: Log only critical information
|
||||
# 1: Log errors and lower log levels
|
||||
# 2: Log warnings and lower log levels
|
||||
@@ -1926,7 +1968,8 @@ instance_name = default
|
||||
# 4: Log info and lower (this is the default)
|
||||
# 5: Verbose logging
|
||||
# 6: Debug logging
|
||||
# 7: Extreme logging
|
||||
# 7: Path logging
|
||||
# 8: Extreme logging
|
||||
|
||||
loglevel = 4
|
||||
|
||||
|
||||
+357
-236
File diff suppressed because it is too large
Load Diff
+44
-8
@@ -100,7 +100,7 @@ __example_rns_config__ = '''# This is an example Reticulum config file.
|
||||
# always-on. This directive is optional and can be removed
|
||||
# for brevity.
|
||||
|
||||
enable_transport = No
|
||||
enable_transport = no
|
||||
|
||||
|
||||
# By default, the first program to launch the Reticulum
|
||||
@@ -112,7 +112,7 @@ enable_transport = No
|
||||
# user, and should generally be turned on. This directive
|
||||
# is optional and can be removed for brevity.
|
||||
|
||||
share_instance = Yes
|
||||
share_instance = yes
|
||||
|
||||
|
||||
# If you want to run multiple *different* shared instances
|
||||
@@ -173,12 +173,18 @@ instance_name = default
|
||||
# network_identity = ~/.reticulum/storage/identity/network
|
||||
|
||||
|
||||
# It is possible to configure the global default interface
|
||||
# gravity. This value will be used if an interface does
|
||||
# not have a specific gravity value in its configuration.
|
||||
|
||||
# default_gravity = 0
|
||||
|
||||
# You can configure whether Reticulum should discover
|
||||
# available interfaces from other Transport Instances over
|
||||
# the network. If this option is enabled, Reticulum will
|
||||
# collect interface information discovered from the network.
|
||||
|
||||
# discover_interfaces = No
|
||||
# discover_interfaces = no
|
||||
|
||||
|
||||
# If you only want to discover interfaces from specific
|
||||
@@ -215,7 +221,26 @@ instance_name = default
|
||||
# an optional directive, and can be left out for brevity.
|
||||
# This behaviour is disabled by default.
|
||||
|
||||
# panic_on_interface_error = No
|
||||
# panic_on_interface_error = no
|
||||
|
||||
# You can specify which mode discovered interfaces should
|
||||
# be created with when auto-connecting.
|
||||
|
||||
# autoconnect_interface_mode = gw
|
||||
|
||||
|
||||
# It is possible to allow announces from auto-connected
|
||||
# interfaces to propagate announces to internal-mode
|
||||
# interfaces, even if the auto-connected interface's mode
|
||||
# would normally not allow for this.
|
||||
|
||||
# autoconnect_announces_to_internal = yes
|
||||
|
||||
|
||||
# You can configure the gravity for discovered interfaces
|
||||
# that are auto-connected.
|
||||
|
||||
# autoconnect_interface_gravity = 0
|
||||
|
||||
|
||||
# When Transport is enabled, it is possible to allow the
|
||||
@@ -226,14 +251,15 @@ instance_name = default
|
||||
# Transport Instance, and printed to the log at startup.
|
||||
# Optional, and disabled by default.
|
||||
|
||||
# respond_to_probes = No
|
||||
# respond_to_probes = no
|
||||
|
||||
|
||||
# You can publish your local list of blackholed identities
|
||||
# for other transport instances to use for automatic,
|
||||
# network-wide blackhole management.
|
||||
|
||||
# publish_blackhole = No
|
||||
# publish_blackhole = no
|
||||
|
||||
|
||||
# List of remote transport identities from which to auto-
|
||||
# matically source lists of blackholed identities.
|
||||
@@ -248,14 +274,23 @@ instance_name = default
|
||||
|
||||
# blackhole_sources = 521c87a83afb8f29e4455e77930b973b
|
||||
|
||||
|
||||
# You can set the interval in minutes at which remote
|
||||
# blackhole sources are updated. Defaults to one hour.
|
||||
|
||||
# blackhole_update_interval = 60
|
||||
|
||||
|
||||
# When not running as a transport node, it is possible to
|
||||
# force the use of the same, static transport identity
|
||||
# for every instance start. Defaults to using a new identity
|
||||
# at every instance start, if transport is disabled.
|
||||
|
||||
# static_transport_identity = no
|
||||
|
||||
|
||||
[logging]
|
||||
# Valid log levels are 0 through 7:
|
||||
# Valid log levels are 0 through 8:
|
||||
# 0: Log only critical information
|
||||
# 1: Log errors and lower log levels
|
||||
# 2: Log warnings and lower log levels
|
||||
@@ -263,7 +298,8 @@ instance_name = default
|
||||
# 4: Log info and lower (this is the default)
|
||||
# 5: Verbose logging
|
||||
# 6: Debug logging
|
||||
# 7: Extreme logging
|
||||
# 7: Path logging
|
||||
# 8: Extreme logging
|
||||
|
||||
loglevel = 4
|
||||
|
||||
|
||||
@@ -35,7 +35,13 @@
|
||||
from ._version import __version__
|
||||
|
||||
import os
|
||||
import glob
|
||||
module_abs_filename = os.path.abspath(__file__)
|
||||
module_dir = os.path.dirname(module_abs_filename)
|
||||
|
||||
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
|
||||
modules = py_modules+pyc_modules
|
||||
__all__ = list(set([os.path.basename(f).replace(".pyc", "").replace(".py", "") for f in modules if not (f.endswith("__init__.py") or f.endswith("__init__.pyc"))]))
|
||||
|
||||
def _get_version(): return __version__
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.3.0"
|
||||
@@ -44,7 +44,8 @@ def setup_argument_parser():
|
||||
parser = argparse.ArgumentParser(description="Reticulum Remote Shell Utility", epilog="When specifying a command to execute, separate rnsh\noptions from the command and its arguments with --\n\nFor example:\n rnsh -l -- /bin/bash --login\n rnsh <destination> -- ls -la /tmp", formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
|
||||
# Common options
|
||||
parser.add_argument("--config", "-c", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("--config", "-c", action="store", default=None, help="path to config directory", type=str)
|
||||
parser.add_argument("--rnsconfig", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("--identity", "-i", action="store", default=None, help="path to identity file to use", type=str)
|
||||
parser.add_argument("-v", "--verbose", action="count", default=0, help="increase verbosity")
|
||||
parser.add_argument("-q", "--quiet", action="count", default=0, help="decrease verbosity")
|
||||
|
||||
@@ -53,8 +53,6 @@ class permit(AbstractContextManager):
|
||||
"""
|
||||
|
||||
def __init__(self, *exceptions): self._exceptions = exceptions
|
||||
|
||||
def __enter__(self): pass
|
||||
|
||||
def __exit__(self, exctype, excinst, exctb):
|
||||
return exctype is not None and not issubclass(exctype, self._exceptions)
|
||||
|
||||
@@ -55,5 +55,4 @@ class SleepRate:
|
||||
return sleep_for if sleep_for > 0 else 0
|
||||
|
||||
async def sleep_async(self): await asyncio.sleep(self.next_sleep_time())
|
||||
|
||||
def sleep_block(self): time.sleep(self.next_sleep_time())
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import enum
|
||||
import functools
|
||||
import os
|
||||
@@ -53,10 +52,8 @@ import RNS.Utilities.rnsh.exception as exception
|
||||
import RNS.Utilities.rnsh.process as process
|
||||
import RNS.Utilities.rnsh.retry as retry
|
||||
import RNS.Utilities.rnsh.session as session
|
||||
import re
|
||||
import contextlib
|
||||
|
||||
import pwd
|
||||
import bz2
|
||||
import RNS.Utilities.rnsh.protocol as protocol
|
||||
import RNS.Utilities.rnsh.helpers as helpers
|
||||
@@ -71,7 +68,6 @@ _retry_timer: retry.RetryThread | None = None
|
||||
_destination: RNS.Destination | None = None
|
||||
_loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
|
||||
async def _check_finished(timeout: float = 0):
|
||||
return _finished is not None and await process.event_wait(_finished, timeout=timeout)
|
||||
|
||||
@@ -92,13 +88,11 @@ async def _spin_tty(until=None, msg=None, timeout=None):
|
||||
print(("\b\b"+syms[i]+" "), end="")
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
print("\r"+" "*len(msg)+" \r", end="")
|
||||
|
||||
if timeout != None and time.time() > timeout: return False
|
||||
else: return True
|
||||
|
||||
|
||||
async def _spin_pipe(until: callable = None, msg=None, timeout: float | None = None) -> bool:
|
||||
if timeout is not None: timeout += time.time()
|
||||
|
||||
@@ -133,18 +127,15 @@ def _client_message_handler(message: RNS.MessageBase): _pq.put(message)
|
||||
def compute_target_rns_loglevel(verbosity: int, quietness: int, base_level: int = RNS.LOG_INFO) -> int:
|
||||
try:
|
||||
target = int(base_level) + int(verbosity) - int(quietness)
|
||||
if target < RNS.LOG_CRITICAL: target = RNS.LOG_CRITICAL
|
||||
if target > RNS.LOG_DEBUG: target = RNS.LOG_DEBUG
|
||||
if target < RNS.LOG_NONE: target = RNS.LOG_NONE
|
||||
if target > RNS.LOG_EXTREME: target = RNS.LOG_EXTREME
|
||||
return target
|
||||
|
||||
except Exception: return base_level
|
||||
|
||||
|
||||
class RemoteExecutionError(Exception):
|
||||
def __init__(self, msg): self.msg = msg
|
||||
|
||||
|
||||
async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0, quietness=0, noid=False, destination=None,
|
||||
async def _initiate_link(configdir, rnsconfigdir, identitypath=None, logfile=None, verbosity=0, quietness=0, noid=False, destination=None,
|
||||
timeout=RNS.Transport.PATH_REQUEST_TIMEOUT):
|
||||
global _identity, _reticulum, _link, _destination, _remote_exec_grace
|
||||
|
||||
@@ -153,34 +144,28 @@ async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0
|
||||
raise RemoteExecutionError(
|
||||
"Allowed destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(
|
||||
hex=dest_len, byte=dest_len // 2))
|
||||
try:
|
||||
destination_hash = bytes.fromhex(destination)
|
||||
except Exception as e:
|
||||
raise RemoteExecutionError("Invalid destination entered. Check your input.")
|
||||
|
||||
try: destination_hash = bytes.fromhex(destination)
|
||||
except Exception as e: raise RemoteExecutionError("Invalid destination entered. Check your input.")
|
||||
|
||||
if _reticulum is None:
|
||||
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_ERROR)
|
||||
RNS.logfile = os.path.join(configdir, "logfile")
|
||||
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_INFO)
|
||||
RNS.logfile = logfile
|
||||
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel, logdest=RNS.LOG_FILE)
|
||||
|
||||
if _identity is None:
|
||||
_identity = rnsh.prepare_identity(identitypath)
|
||||
_identity = rnsh.prepare_identity(identity_path=identitypath, service_name=None, configdir=configdir)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
RNS.log(f"Requesting path...", RNS.LOG_INFO)
|
||||
if not await _spin(until=lambda: RNS.Transport.has_path(destination_hash), msg="Requesting path...",
|
||||
RNS.log(f"Requesting path to {RNS.prettyhexrep(destination_hash)}", RNS.LOG_INFO)
|
||||
if not await _spin(until=lambda: RNS.Transport.has_path(destination_hash), msg=f"Requesting path to {RNS.prettyhexrep(destination_hash)}",
|
||||
timeout=timeout, quiet=quietness > 0):
|
||||
raise RemoteExecutionError("Path not found")
|
||||
|
||||
if _destination is None:
|
||||
listener_identity = RNS.Identity.recall(destination_hash)
|
||||
_destination = RNS.Destination(
|
||||
listener_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
rnsh.APP_NAME
|
||||
)
|
||||
_destination = RNS.Destination(listener_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, rnsh.APP_NAME)
|
||||
|
||||
if _link is None or _link.status == RNS.Link.PENDING:
|
||||
RNS.log("No link", RNS.LOG_DEBUG)
|
||||
@@ -189,33 +174,30 @@ async def _initiate_link(configdir, rnsconfigdir, identitypath=None, verbosity=0
|
||||
|
||||
_link.set_link_closed_callback(_client_link_closed)
|
||||
|
||||
RNS.log(f"Establishing link...", RNS.LOG_VERBOSE)
|
||||
if not await _spin(until=lambda: _link.status == RNS.Link.ACTIVE, msg="Establishing link...",
|
||||
RNS.log(f"Establishing link with {RNS.prettyhexrep(destination_hash)}", RNS.LOG_INFO)
|
||||
if not await _spin(until=lambda: _link.status == RNS.Link.ACTIVE, msg=f"Establishing link with {RNS.prettyhexrep(destination_hash)}",
|
||||
timeout=timeout, quiet=quietness > 0):
|
||||
raise RemoteExecutionError("Could not establish link with " + RNS.prettyhexrep(destination_hash))
|
||||
|
||||
RNS.log("Have link", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Link established with {RNS.prettyhexrep(destination_hash)}", RNS.LOG_INFO)
|
||||
if not noid and not _link.did_identify:
|
||||
# Delay a tiny bit to allow listener to fully enter WAIT_IDENT state
|
||||
await asyncio.sleep(min(1, _link.rtt * 1.1 + 0.05))
|
||||
_link.identify(_identity)
|
||||
_link.did_identify = True
|
||||
|
||||
|
||||
async def _handle_error(errmsg: RNS.MessageBase):
|
||||
if isinstance(errmsg, protocol.ErrorMessage):
|
||||
with contextlib.suppress(Exception):
|
||||
if _link and _link.status == RNS.Link.ACTIVE:
|
||||
_link.teardown()
|
||||
if _link and _link.status == RNS.Link.ACTIVE: _link.teardown()
|
||||
await asyncio.sleep(0.1)
|
||||
raise RemoteExecutionError(f"Remote error: {errmsg.msg}")
|
||||
|
||||
|
||||
async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosity: int, quietness: int, noid: bool, destination: str,
|
||||
async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, logfile:str, verbosity: int, quietness: int, noid: bool, destination: str,
|
||||
timeout: float, command: [str] | None = None):
|
||||
|
||||
global _finished, _link
|
||||
if timeout is None:
|
||||
timeout = RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
if timeout is None: timeout = RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
with process.TTYRestorer(sys.stdin.fileno()) as ttyRestorer:
|
||||
loop = asyncio.get_running_loop()
|
||||
state = InitiatorState.IS_INITIAL
|
||||
@@ -225,33 +207,26 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
|
||||
await _initiate_link(configdir=configdir,
|
||||
rnsconfigdir=rnsconfigdir,
|
||||
identitypath=identitypath,
|
||||
logfile=logfile,
|
||||
verbosity=verbosity,
|
||||
quietness=quietness,
|
||||
noid=noid,
|
||||
destination=destination,
|
||||
timeout=timeout)
|
||||
|
||||
if not _link or _link.status not in [RNS.Link.ACTIVE, RNS.Link.PENDING]:
|
||||
return 255
|
||||
if not _link or _link.status not in [RNS.Link.ACTIVE, RNS.Link.PENDING]: return 255
|
||||
|
||||
state = InitiatorState.IS_LINKED
|
||||
outlet = session.RNSOutlet(_link)
|
||||
channel = _link.get_channel()
|
||||
protocol.register_message_types(channel)
|
||||
channel.add_message_handler(_client_message_handler)
|
||||
|
||||
# Next step after linking and identifying: send version
|
||||
# if not await _spin(lambda: messenger.is_outlet_ready(outlet), timeout=5, quiet=quietness > 0):
|
||||
# print("Error bringing up link")
|
||||
# return 253
|
||||
|
||||
channel.send(protocol.VersionInfoMessage())
|
||||
try:
|
||||
vm = _pq.get(timeout=max(outlet.rtt * 20, 5))
|
||||
await _handle_error(vm)
|
||||
if not isinstance(vm, protocol.VersionInfoMessage):
|
||||
raise Exception("Invalid message received")
|
||||
RNS.log(f"Server version info: sw {vm.sw_version} prot {vm.protocol_version}", RNS.LOG_DEBUG)
|
||||
if not isinstance(vm, protocol.VersionInfoMessage): raise Exception("Invalid message received")
|
||||
RNS.log(f"Connected server version info: sw {vm.sw_version}, proto {vm.protocol_version}", RNS.LOG_INFO)
|
||||
state = InitiatorState.IS_RUNNING
|
||||
except queue.Empty:
|
||||
print("Protocol error")
|
||||
@@ -283,10 +258,8 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
|
||||
return None
|
||||
elif b == "L":
|
||||
line_mode = not line_mode
|
||||
if line_mode:
|
||||
os.write(1, "\n\rLine-interactive mode enabled\n\r".encode("utf-8"))
|
||||
else:
|
||||
os.write(1, "\n\rLine-interactive mode disabled\n\r".encode("utf-8"))
|
||||
if line_mode: os.write(1, "\n\rLine-interactive mode enabled\n\r".encode("utf-8"))
|
||||
else: os.write(1, "\n\rLine-interactive mode disabled\n\r".encode("utf-8"))
|
||||
return None
|
||||
|
||||
return b
|
||||
@@ -329,8 +302,7 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
|
||||
pre_esc = False
|
||||
data.append(b)
|
||||
|
||||
if not line_mode:
|
||||
data_buffer.extend(data)
|
||||
if not line_mode: data_buffer.extend(data)
|
||||
else:
|
||||
line_buffer.extend(data)
|
||||
if line_flush:
|
||||
@@ -344,8 +316,7 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
|
||||
blind_write_count += len(data)
|
||||
|
||||
except EOFError:
|
||||
if os.isatty(0):
|
||||
data_buffer.extend(process.CTRL_D)
|
||||
if os.isatty(0): data_buffer.extend(process.CTRL_D)
|
||||
stdin_eof = True
|
||||
process.tty_unset_reader_callbacks(sys.stdin.fileno())
|
||||
|
||||
@@ -398,7 +369,7 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
|
||||
if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDOUT:
|
||||
if message.data and len(message.data) > 0:
|
||||
ttyRestorer.raw()
|
||||
RNS.log(f"stdout: {message.data}", RNS.LOG_DEBUG)
|
||||
# RNS.log(f"stdout: {message.data}", RNS.LOG_EXTREME)
|
||||
os.write(1, message.data)
|
||||
sys.stdout.flush()
|
||||
if message.eof:
|
||||
@@ -406,7 +377,7 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
|
||||
if message.stream_id == protocol.StreamDataMessage.STREAM_ID_STDERR:
|
||||
if message.data and len(message.data) > 0:
|
||||
ttyRestorer.raw()
|
||||
RNS.log(f"stdout: {message.data}", RNS.LOG_DEBUG)
|
||||
# RNS.log(f"stdout: {message.data}", RNS.LOG_EXTREME)
|
||||
os.write(2, message.data)
|
||||
sys.stderr.flush()
|
||||
if message.eof:
|
||||
@@ -420,8 +391,7 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
|
||||
_link.teardown()
|
||||
return 200
|
||||
|
||||
except queue.Empty:
|
||||
processed = False
|
||||
except queue.Empty: processed = False
|
||||
|
||||
if channel.is_ready_to_send():
|
||||
def compress_adaptive(buf: bytes):
|
||||
@@ -443,8 +413,7 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
|
||||
if compressed_length < max_data_len and compressed_length < chunk_segment_length:
|
||||
comp_success = True
|
||||
break
|
||||
else:
|
||||
comp_try += 1
|
||||
else: comp_try += 1
|
||||
|
||||
if comp_success:
|
||||
diff = max_data_len - len(compressed_chunk)
|
||||
@@ -473,6 +442,7 @@ async def initiate(configdir: str, rnsconfigdir:str, identitypath: str, verbosit
|
||||
r, c, h, v = process.tty_get_winsize(0)
|
||||
channel.send(protocol.WindowSizeMessage(r, c, h, v))
|
||||
processed = True
|
||||
|
||||
except RemoteExecutionError as e:
|
||||
print(e.msg)
|
||||
return 255
|
||||
|
||||
@@ -50,7 +50,6 @@ import RNS.Utilities.rnsh.exception as exception
|
||||
import RNS.Utilities.rnsh.process as process
|
||||
import RNS.Utilities.rnsh.retry as retry
|
||||
import RNS.Utilities.rnsh.session as session
|
||||
import re
|
||||
import contextlib
|
||||
|
||||
import pwd
|
||||
@@ -112,31 +111,38 @@ def _reload_allowed_file():
|
||||
def compute_target_rns_loglevel(verbosity: int, quietness: int, base_level: int = RNS.LOG_INFO) -> int:
|
||||
try:
|
||||
target = int(base_level) + int(verbosity) - int(quietness)
|
||||
if target < RNS.LOG_CRITICAL: target = RNS.LOG_CRITICAL
|
||||
if target > RNS.LOG_DEBUG: target = RNS.LOG_DEBUG
|
||||
if target < RNS.LOG_NONE: target = RNS.LOG_NONE
|
||||
if target > RNS.LOG_EXTREME: target = RNS.LOG_EXTREME
|
||||
return target
|
||||
|
||||
except Exception: return base_level
|
||||
|
||||
async def listen(configdir, rnsconfigdir, command, identitypath=None, service_name=None, verbosity=0, quietness=0, allowed=None,
|
||||
allowed_file=None, disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False,
|
||||
async def listen(configdir, rnsconfigdir, command, identitypath=None, logfile=None, service_name=None, verbosity=0, quietness=0,
|
||||
allowed=None, allowed_file=None, disable_auth=None, announce_period=900, no_remote_command=True, remote_cmd_as_args=False,
|
||||
loop: asyncio.AbstractEventLoop = None):
|
||||
global _identity, _allow_all, _allowed_identity_hashes, _allowed_file, _allowed_file_identity_hashes
|
||||
global _reticulum, _cmd, _destination, _no_remote_command, _remote_cmd_as_args, _finished
|
||||
|
||||
if not loop: loop = asyncio.get_running_loop()
|
||||
if service_name is None or len(service_name) == 0:
|
||||
service_name = "default"
|
||||
if service_name is None or len(service_name) == 0: service_name = "default"
|
||||
|
||||
RNS.log(f"Using service name {service_name}", RNS.LOG_INFO)
|
||||
if logfile:
|
||||
RNS.log(f"Logging to {logfile}", RNS.LOG_NOTICE)
|
||||
logdest = RNS.LOG_FILE
|
||||
RNS.logfile = logfile
|
||||
else:
|
||||
RNS.log(f"Logging to console", RNS.LOG_NOTICE)
|
||||
logdest = RNS.LOG_STDOUT
|
||||
|
||||
# More -v should increase verbosity (higher RNS.loglevel); -q should decrease it
|
||||
targetloglevel = compute_target_rns_loglevel(verbosity, quietness, RNS.LOG_INFO)
|
||||
_reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=targetloglevel)
|
||||
_identity = rnsh.prepare_identity(identitypath, service_name)
|
||||
_identity = rnsh.prepare_identity(identity_path=identitypath, service_name=service_name, configdir=configdir)
|
||||
_destination = RNS.Destination(_identity, RNS.Destination.IN, RNS.Destination.SINGLE, rnsh.APP_NAME)
|
||||
|
||||
RNS.log(f"rnsh listening for commands on {RNS.prettyhexrep(_destination.hash)}", RNS.LOG_NOTICE)
|
||||
RNS.logdest = logdest
|
||||
|
||||
_cmd = command
|
||||
if _cmd is None or len(_cmd) == 0:
|
||||
@@ -154,9 +160,8 @@ async def listen(configdir, rnsconfigdir, command, identitypath=None, service_na
|
||||
_no_remote_command = no_remote_command
|
||||
session.ListenerSession.allow_remote_command = not no_remote_command
|
||||
_remote_cmd_as_args = remote_cmd_as_args
|
||||
if (_cmd is None or len(_cmd) == 0 or _cmd[0] is None or len(_cmd[0]) == 0) \
|
||||
and (_no_remote_command or _remote_cmd_as_args):
|
||||
raise Exception(f"Unable to look up shell for {os.getlogin}, cannot proceed with -A or -C and no <program>.")
|
||||
if (_cmd is None or len(_cmd) == 0 or _cmd[0] is None or len(_cmd[0]) == 0) and (_no_remote_command or _remote_cmd_as_args):
|
||||
raise Exception(f"Unable to look up shell for {os.getlogin()}, cannot proceed with -A or -C and no <program>.")
|
||||
|
||||
session.ListenerSession.default_command = _cmd
|
||||
session.ListenerSession.remote_cmd_as_args = _remote_cmd_as_args
|
||||
|
||||
@@ -488,8 +488,6 @@ class CallbackSubprocess:
|
||||
self._stdin_is_pipe = stdin_is_pipe
|
||||
self._stdout_is_pipe = stdout_is_pipe
|
||||
self._stderr_is_pipe = stderr_is_pipe
|
||||
self._at_line_start: bool = True
|
||||
self._tty_line_buffer: bytearray = bytearray()
|
||||
|
||||
def _ensure_pipes_closed(self):
|
||||
stdin = self._child_stdin
|
||||
@@ -624,14 +622,6 @@ class CallbackSubprocess:
|
||||
"""
|
||||
RNS.log("start()", RNS.LOG_EXTREME)
|
||||
|
||||
# # Using the parent environment seems to do some weird stuff, at least on macOS
|
||||
# parentenv = os.environ.copy()
|
||||
# env = {"HOME": parentenv["HOME"],
|
||||
# "PATH": parentenv["PATH"],
|
||||
# "TERM": self._term if self._term is not None else parentenv.get("TERM", "xterm"),
|
||||
# "LANG": parentenv.get("LANG"),
|
||||
# "SHELL": self._command[0]}
|
||||
|
||||
env = os.environ.copy()
|
||||
for key in self._env:
|
||||
env[key] = self._env[key]
|
||||
@@ -639,12 +629,6 @@ class CallbackSubprocess:
|
||||
program = self._command[0]
|
||||
assert isinstance(program, str)
|
||||
|
||||
# match = re.search("^/bin/(.*sh)$", program)
|
||||
# if match:
|
||||
# self._command[0] = "-" + match.group(1)
|
||||
# env["SHELL"] = program
|
||||
# self._log.debug(f"set login shell {self._command}")
|
||||
|
||||
self._pid, \
|
||||
self._child_stdin, \
|
||||
self._child_stdout, \
|
||||
|
||||
@@ -88,8 +88,7 @@ class RetryThread(AbstractContextManager):
|
||||
self._thread = threading.Thread(name=name, target=self._thread_run, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def is_alive(self):
|
||||
return self._thread.is_alive()
|
||||
def is_alive(self): return self._thread.is_alive()
|
||||
|
||||
def close(self, loop: asyncio.AbstractEventLoop = None) -> asyncio.Future:
|
||||
RNS.log("Stopping timer thread", RNS.LOG_DEBUG)
|
||||
@@ -102,17 +101,12 @@ class RetryThread(AbstractContextManager):
|
||||
return self._finished
|
||||
|
||||
def wait(self, timeout: float = None):
|
||||
if timeout:
|
||||
timeout = timeout + time.time()
|
||||
|
||||
if timeout: timeout = timeout + time.time()
|
||||
while timeout is None or time.time() < timeout:
|
||||
with self._lock:
|
||||
task_count = len(self._statuses)
|
||||
if task_count == 0:
|
||||
return
|
||||
with self._lock: task_count = len(self._statuses)
|
||||
if task_count == 0: return
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def _thread_run(self):
|
||||
while self._run and self._finished is None:
|
||||
time.sleep(self._loop_period)
|
||||
|
||||
+39
-24
@@ -37,7 +37,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
|
||||
import re
|
||||
import os
|
||||
@@ -57,43 +56,54 @@ loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
def _sanitize_service_name(service_name:str) -> str: return re.sub(r'\W+', '', service_name)
|
||||
|
||||
def prepare_identity(identity_path, service_name: str = None) -> tuple[RNS.Identity]:
|
||||
def prepare_identity(identity_path=None, service_name=None, configdir=None):
|
||||
service_name = _sanitize_service_name(service_name or "")
|
||||
if identity_path is None:
|
||||
identity_path = RNS.Reticulum.identitypath + "/" + APP_NAME + \
|
||||
(f".{service_name}" if service_name and len(service_name) > 0 else "")
|
||||
if not identity_path:
|
||||
identity_path = f"{configdir}/identity" + (f".{service_name}" if service_name and len(service_name) > 0 else "")
|
||||
|
||||
identity = None
|
||||
if os.path.isfile(identity_path):
|
||||
RNS.log(f"Loading identity from {identity_path}", RNS.LOG_VERBOSE)
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
|
||||
if identity is None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
RNS.log(f"Creating identity {identity_path}", RNS.LOG_NOTICE)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
|
||||
return identity
|
||||
|
||||
|
||||
def print_identity(configdir, identitypath, service_name, include_destination: bool):
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=RNS.LOG_INFO)
|
||||
if service_name and len(service_name) > 0:
|
||||
print(f"Using service name \"{service_name}\"")
|
||||
identity = prepare_identity(identitypath, service_name)
|
||||
def print_identity(configdir, rnsconfigdir, identitypath, service_name, include_destination: bool):
|
||||
reticulum = RNS.Reticulum(configdir=rnsconfigdir, loglevel=RNS.LOG_INFO)
|
||||
if service_name and len(service_name) > 0: print(f"Using service name \"{service_name}\"")
|
||||
identity = prepare_identity(identity_path=identitypath, service_name=service_name, configdir=configdir)
|
||||
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME)
|
||||
print("Identity : " + str(identity))
|
||||
if include_destination:
|
||||
print("Listening on : " + RNS.prettyhexrep(destination.hash))
|
||||
if include_destination: print("Listening on : " + RNS.prettyhexrep(destination.hash))
|
||||
|
||||
exit(0)
|
||||
|
||||
verbose_set = False
|
||||
|
||||
def ensure_config_directory():
|
||||
if os.path.isdir(os.path.expanduser("~/.config/rnsh")): return os.path.expanduser("~/.config/rnsh")
|
||||
def ensure_config_directory(configdir=None):
|
||||
if configdir:
|
||||
if os.path.isdir(os.path.expanduser(configdir)): return os.path.expanduser(configdir)
|
||||
else:
|
||||
try:
|
||||
RNS.log(f"Creating configuration directory: {os.path.expanduser(configdir)}")
|
||||
os.makedirs(os.path.expanduser(configdir))
|
||||
return os.path.expanduser(configdir)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not get or create rnsh configuration directory, aborting", RNS.LOG_CRITICAL)
|
||||
os._exit(1)
|
||||
|
||||
elif os.path.isdir(os.path.expanduser("~/.config/rnsh")): return os.path.expanduser("~/.config/rnsh")
|
||||
elif os.path.isdir(os.path.expanduser("~/.rnsh")): return os.path.expanduser("~/.rnsh")
|
||||
else:
|
||||
try:
|
||||
RNS.log(f"Creating configuration directory: {os.path.expanduser('~/.rnsh')}")
|
||||
os.makedirs(os.path.expanduser("~/.rnsh"))
|
||||
return os.path.expanduser("~/.rnsh")
|
||||
|
||||
@@ -106,25 +116,29 @@ async def _rnsh_cli_main():
|
||||
global verbose_set
|
||||
args, parser = parse_arguments()
|
||||
verbose_set = args.verbose > 0
|
||||
configdir = ensure_config_directory(args.config)
|
||||
|
||||
configdir = ensure_config_directory()
|
||||
if not configdir:
|
||||
RNS.log(f"Could not resolve rnsh configuration directory", RNS.LOG_CRITICAL)
|
||||
os._exit(1)
|
||||
|
||||
if args.print_identity:
|
||||
print_identity(args.config, args.identity, args.service, args.listen)
|
||||
print_identity(configdir, args.rnsconfig, args.identity, args.service, args.listen)
|
||||
return 0
|
||||
|
||||
logfile = f"{configdir}/logfile"
|
||||
|
||||
if args.listen:
|
||||
allowed_file = None
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if os.path.isfile(os.path.expanduser("~/.config/rnsh/allowed_identities")):
|
||||
allowed_file = os.path.expanduser("~/.config/rnsh/allowed_identities")
|
||||
elif os.path.isfile(os.path.expanduser("~/.rnsh/allowed_identities")):
|
||||
allowed_file = os.path.expanduser("~/.rnsh/allowed_identities")
|
||||
if os.path.isfile(os.path.expanduser(f"{configdir}/allowed_identities")):
|
||||
allowed_file = os.path.expanduser(f"{configdir}/allowed_identities")
|
||||
|
||||
await listener.listen(configdir=configdir,
|
||||
rnsconfigdir=args.config,
|
||||
rnsconfigdir=args.rnsconfig,
|
||||
command=args.command,
|
||||
identitypath=args.identity,
|
||||
logfile=logfile,
|
||||
service_name=args.service,
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
@@ -138,8 +152,9 @@ async def _rnsh_cli_main():
|
||||
|
||||
if args.destination is not None:
|
||||
return_code = await initiator.initiate(configdir=configdir,
|
||||
rnsconfigdir=args.config,
|
||||
rnsconfigdir=args.rnsconfig,
|
||||
identitypath=args.identity,
|
||||
logfile=f"{logfile}.initiator",
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
noid=args.no_id,
|
||||
@@ -147,6 +162,7 @@ async def _rnsh_cli_main():
|
||||
timeout=args.timeout,
|
||||
command=args.command
|
||||
)
|
||||
RNS.log(f"Sesssion with <{args.destination}> ended", RNS.LOG_INFO)
|
||||
return return_code if args.mirror else 0
|
||||
else:
|
||||
print("")
|
||||
@@ -170,5 +186,4 @@ def main():
|
||||
if verbose_set and exc: raise exc
|
||||
sys.exit(return_code if return_code is not None else 255)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
|
||||
@@ -43,7 +43,6 @@ import RNS.Utilities.rnsh.protocol as protocol
|
||||
import enum
|
||||
from typing import TypeVar, Generic, Callable, List
|
||||
from abc import abstractmethod, ABC
|
||||
from multiprocessing import Manager
|
||||
import os
|
||||
import bz2
|
||||
import RNS
|
||||
@@ -103,6 +102,8 @@ class ListenerSession:
|
||||
self.outlet.set_link_closed_callback(self._link_closed)
|
||||
self.loop = loop
|
||||
self.state: LSState = None
|
||||
self.terminated = False
|
||||
self.authenticated = False
|
||||
self.remote_identity = None
|
||||
self.term: str | None = None
|
||||
self.stdin_is_pipe: bool = False
|
||||
@@ -122,8 +123,10 @@ class ListenerSession:
|
||||
self.return_code_sent = False
|
||||
self.process: process.CallbackSubprocess | None = None
|
||||
|
||||
if self.allow_all: self._set_state(LSState.LSSTATE_WAIT_VERS)
|
||||
else: self._set_state(LSState.LSSTATE_WAIT_IDENT)
|
||||
if not self.allow_all: self._set_state(LSState.LSSTATE_WAIT_IDENT)
|
||||
else:
|
||||
self._set_state(LSState.LSSTATE_WAIT_VERS)
|
||||
self.authenticated = True
|
||||
|
||||
self.sessions.append(self)
|
||||
protocol.register_message_types(self.channel)
|
||||
@@ -147,33 +150,27 @@ class ListenerSession:
|
||||
|
||||
self.loop.call_soon_threadsafe(call_inner)
|
||||
|
||||
def send(self, message: RNS.MessageBase):
|
||||
self.channel.send(message)
|
||||
|
||||
def _protocol_error(self, name: str):
|
||||
self.terminate(f"Protocol error ({name})")
|
||||
|
||||
def _protocol_timeout_error(self, name: str):
|
||||
self.terminate(f"Protocol timeout error: {name}")
|
||||
def send(self, message: RNS.MessageBase): self.channel.send(message)
|
||||
def _protocol_error(self, name: str): self.terminate(f"Protocol error ({name})")
|
||||
def _protocol_timeout_error(self, name: str): self.terminate(f"Protocol timeout error: {name}")
|
||||
|
||||
def terminate(self, error: str = None):
|
||||
self.terminated = True
|
||||
self.state = LSState.LSSTATE_ERROR
|
||||
with contextlib.suppress(Exception):
|
||||
RNS.log("Terminating session" + (f": {error}" if error else ""), RNS.LOG_DEBUG)
|
||||
if error and self.state != LSState.LSSTATE_TEARDOWN:
|
||||
with contextlib.suppress(Exception):
|
||||
self.send(protocol.ErrorMessage(error, True))
|
||||
|
||||
self.state = LSState.LSSTATE_ERROR
|
||||
self._terminate_process()
|
||||
self._call(self._prune, max(self.outlet.rtt * 3, process.CallbackSubprocess.PROCESS_PIPE_TIME+5))
|
||||
|
||||
def _prune(self):
|
||||
self.state = LSState.LSSTATE_TEARDOWN
|
||||
RNS.log("Pruning session", RNS.LOG_DEBUG)
|
||||
with contextlib.suppress(ValueError):
|
||||
self.sessions.remove(self)
|
||||
with contextlib.suppress(Exception):
|
||||
self.outlet.teardown()
|
||||
with contextlib.suppress(ValueError): self.sessions.remove(self)
|
||||
with contextlib.suppress(Exception): self.outlet.teardown()
|
||||
|
||||
def _check_protocol_timeout(self, fail_condition: Callable[[], bool], name: str):
|
||||
timeout = True
|
||||
@@ -183,7 +180,6 @@ class ListenerSession:
|
||||
|
||||
def _link_closed(self, outlet: LSOutletBase):
|
||||
outlet.unset_link_closed_callback()
|
||||
|
||||
if outlet != self.outlet:
|
||||
RNS.log("Link closed received from incorrect outlet", RNS.LOG_DEBUG)
|
||||
return
|
||||
@@ -196,16 +192,22 @@ class ListenerSession:
|
||||
RNS.log("Identity received from incorrect outlet", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
RNS.log(f"initiator_identified {identity} on link {outlet}", RNS.LOG_INFO)
|
||||
RNS.log(f"Initiator identified {identity} on link {outlet}", RNS.LOG_INFO)
|
||||
if self.state not in [LSState.LSSTATE_WAIT_IDENT, LSState.LSSTATE_WAIT_VERS]:
|
||||
self._protocol_error(LSState.LSSTATE_WAIT_IDENT.name)
|
||||
return
|
||||
|
||||
if not self.allow_all and identity.hash not in self.allowed_identity_hashes and identity.hash not in self.allowed_file_identity_hashes:
|
||||
self.terminate("Identity is not allowed.")
|
||||
|
||||
if self.allow_all or identity.hash in self.allowed_identity_hashes or identity.hash in self.allowed_file_identity_hashes:
|
||||
RNS.log(f"Identity {identity} authenticated")
|
||||
self.authenticated = True
|
||||
self.remote_identity = identity
|
||||
self._set_state(LSState.LSSTATE_WAIT_VERS)
|
||||
|
||||
else:
|
||||
self.authenticated = False
|
||||
self.terminate("Identity not allowed")
|
||||
RNS.log(f"Identity {identity} not allowed")
|
||||
|
||||
@classmethod
|
||||
async def pump_all(cls) -> True:
|
||||
processed_any = False
|
||||
@@ -241,8 +243,7 @@ class ListenerSession:
|
||||
if compressed_length < max_data_len and compressed_length < chunk_segment_length:
|
||||
comp_success = True
|
||||
break
|
||||
else:
|
||||
comp_try += 1
|
||||
else: comp_try += 1
|
||||
|
||||
if comp_success:
|
||||
diff = max_data_len - len(compressed_chunk)
|
||||
@@ -255,10 +256,8 @@ class ListenerSession:
|
||||
return comp_success, processed_length, chunk
|
||||
|
||||
try:
|
||||
if self.state != LSState.LSSTATE_RUNNING:
|
||||
return False
|
||||
elif not self.channel.is_ready_to_send():
|
||||
return False
|
||||
if self.state != LSState.LSSTATE_RUNNING: return False
|
||||
elif not self.channel.is_ready_to_send(): return False
|
||||
elif len(self.stderr_buf) > 0:
|
||||
comp_success, processed_length, data = compress_adaptive(self.stderr_buf)
|
||||
self.stderr_buf = self.stderr_buf[processed_length:]
|
||||
@@ -267,8 +266,7 @@ class ListenerSession:
|
||||
msg = protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDERR,
|
||||
data, send_eof, comp_success)
|
||||
self.send(msg)
|
||||
if send_eof:
|
||||
self.stderr_eof_sent = True
|
||||
if send_eof: self.stderr_eof_sent = True
|
||||
return True
|
||||
elif len(self.stdout_buf) > 0:
|
||||
comp_success, processed_length, data = compress_adaptive(self.stdout_buf)
|
||||
@@ -278,8 +276,7 @@ class ListenerSession:
|
||||
msg = protocol.StreamDataMessage(protocol.StreamDataMessage.STREAM_ID_STDOUT,
|
||||
data, send_eof, comp_success)
|
||||
self.send(msg)
|
||||
if send_eof:
|
||||
self.stdout_eof_sent = True
|
||||
if send_eof: self.stdout_eof_sent = True
|
||||
return True
|
||||
elif self.return_code is not None and not self.return_code_sent:
|
||||
msg = protocol.CommandExitedMessage(self.return_code)
|
||||
@@ -295,22 +292,33 @@ class ListenerSession:
|
||||
|
||||
def _terminate_process(self):
|
||||
with contextlib.suppress(Exception):
|
||||
if self.process and self.process.running:
|
||||
self.process.terminate()
|
||||
if self.process and self.process.running: self.process.terminate()
|
||||
|
||||
def _start_cmd(self, cmdline: [str], pipe_stdin: bool, pipe_stdout: bool, pipe_stderr: bool, tcflags: [any],
|
||||
term: str | None, rows: int, cols: int, hpix: int, vpix: int):
|
||||
|
||||
self.cmdline = self.default_command
|
||||
if self.terminated:
|
||||
RNS.log(f"Attempt to start command on terminated session: {cmdline}", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
if not self.authenticated:
|
||||
RNS.log(f"Attempt to start command on unauthenticated session: {cmdline}", RNS.LOG_WARNING)
|
||||
self.terminate("Invalid state")
|
||||
return
|
||||
|
||||
if self.state != LSState.LSSTATE_RUNNING:
|
||||
RNS.log(f"Attempt to start command on session not in running state: {cmdline}", RNS.LOG_WARNING)
|
||||
self.terminate("Invalid state")
|
||||
return
|
||||
|
||||
self.cmdline = []
|
||||
self.cmdline.extend(self.default_command)
|
||||
if not self.allow_remote_command and cmdline and len(cmdline) > 0:
|
||||
self.terminate("Remote command line not allowed by listener")
|
||||
return
|
||||
|
||||
if self.remote_cmd_as_args and cmdline and len(cmdline) > 0:
|
||||
self.cmdline.extend(cmdline)
|
||||
elif cmdline and len(cmdline) > 0:
|
||||
self.cmdline = cmdline
|
||||
|
||||
if self.remote_cmd_as_args and cmdline and len(cmdline) > 0: self.cmdline.extend(cmdline)
|
||||
elif cmdline and len(cmdline) > 0: self.cmdline = cmdline
|
||||
|
||||
self.stdin_is_pipe = pipe_stdin
|
||||
self.stdout_is_pipe = pipe_stdout
|
||||
@@ -318,13 +326,11 @@ class ListenerSession:
|
||||
self.tcflags = tcflags
|
||||
self.term = term
|
||||
|
||||
def stdout(data: bytes):
|
||||
self.stdout_buf.extend(data)
|
||||
|
||||
def stderr(data: bytes):
|
||||
self.stderr_buf.extend(data)
|
||||
def stdout(data: bytes): self.stdout_buf.extend(data)
|
||||
def stderr(data: bytes): self.stderr_buf.extend(data)
|
||||
|
||||
try:
|
||||
RNS.log(f"Remote {self.remote_identity} executing: {self.cmdline}", RNS.LOG_VERBOSE)
|
||||
self.process = process.CallbackSubprocess(argv=self.cmdline,
|
||||
env={"TERM": self.term or os.environ.get("TERM") or "xterm",
|
||||
"RNS_REMOTE_IDENTITY": (RNS.prettyhexrep(self.remote_identity.hash)
|
||||
@@ -347,22 +353,28 @@ class ListenerSession:
|
||||
self.cols = cols
|
||||
self.hpix = hpix
|
||||
self.vpix = vpix
|
||||
with contextlib.suppress(Exception):
|
||||
self.process.set_winsize(rows, cols, hpix, vpix)
|
||||
with contextlib.suppress(Exception): self.process.set_winsize(rows, cols, hpix, vpix)
|
||||
|
||||
def _received_stdin(self, data: bytes, eof: bool):
|
||||
if data and len(data) > 0:
|
||||
self.process.write(data)
|
||||
if eof:
|
||||
self.process.close_stdin()
|
||||
if data and len(data) > 0: self.process.write(data)
|
||||
if eof: self.process.close_stdin()
|
||||
|
||||
def _handle_message(self, message: RNS.MessageBase):
|
||||
if self.terminated:
|
||||
RNS.log(f"Received packet, but session was terminated", RNS.LOG_ERROR)
|
||||
return
|
||||
if self.state > LSState.LSSTATE_RUNNING:
|
||||
RNS.log(f"Received packet, but in state {self.state.name}", RNS.LOG_ERROR)
|
||||
return
|
||||
if self.state == LSState.LSSTATE_WAIT_IDENT:
|
||||
# Ignore any messages until the initiator has identified to avoid race conditions
|
||||
# between identity announcement and early protocol messages.
|
||||
RNS.log("Ignoring message while waiting for identification", RNS.LOG_DEBUG)
|
||||
return
|
||||
if self.state == LSState.LSSTATE_WAIT_VERS:
|
||||
elif self.state == LSState.LSSTATE_WAIT_VERS:
|
||||
if not self.authenticated:
|
||||
self.terminate("Invalid state")
|
||||
return
|
||||
if not isinstance(message, protocol.VersionInfoMessage):
|
||||
self._protocol_error(self.state.name)
|
||||
return
|
||||
@@ -374,6 +386,9 @@ class ListenerSession:
|
||||
self._set_state(LSState.LSSTATE_WAIT_CMD)
|
||||
return
|
||||
elif self.state == LSState.LSSTATE_WAIT_CMD:
|
||||
if not self.authenticated:
|
||||
self.terminate("Invalid state")
|
||||
return
|
||||
if not isinstance(message, protocol.ExecuteCommandMesssage):
|
||||
return self._protocol_error(self.state.name)
|
||||
RNS.log(f"Execute command message on link {self.outlet}: {message.cmdline}", RNS.LOG_VERBOSE)
|
||||
@@ -382,6 +397,9 @@ class ListenerSession:
|
||||
message.tcflags, message.term, message.rows, message.cols, message.hpix, message.vpix)
|
||||
return
|
||||
elif self.state == LSState.LSSTATE_RUNNING:
|
||||
if not self.authenticated:
|
||||
self.terminate("Invalid state")
|
||||
return
|
||||
if isinstance(message, protocol.WindowSizeMessage):
|
||||
self._set_window_size(message.rows, message.cols, message.hpix, message.vpix)
|
||||
elif isinstance(message, protocol.StreamDataMessage):
|
||||
@@ -394,9 +412,6 @@ class ListenerSession:
|
||||
# echo noop only on listener--used for keepalive/connectivity check
|
||||
self.send(message)
|
||||
return
|
||||
elif self.state in [LSState.LSSTATE_ERROR, LSState.LSSTATE_TEARDOWN]:
|
||||
RNS.log(f"Received packet, but in state {self.state.name}", RNS.LOG_ERROR)
|
||||
return
|
||||
else:
|
||||
self._protocol_error("unexpected message")
|
||||
return
|
||||
@@ -405,29 +420,20 @@ class ListenerSession:
|
||||
class RNSOutlet(LSOutletBase):
|
||||
|
||||
def set_initiator_identified_callback(self, cb: Callable[[LSOutletBase, _TIdentity], None]):
|
||||
def inner_cb(link, identity: _TIdentity):
|
||||
cb(self, identity)
|
||||
|
||||
def inner_cb(link, identity: _TIdentity): cb(self, identity)
|
||||
self.link.set_remote_identified_callback(inner_cb)
|
||||
|
||||
def set_link_closed_callback(self, cb: Callable[[LSOutletBase], None]):
|
||||
def inner_cb(link):
|
||||
cb(self)
|
||||
|
||||
def inner_cb(link): cb(self)
|
||||
self.link.set_link_closed_callback(inner_cb)
|
||||
|
||||
def unset_link_closed_callback(self):
|
||||
self.link.set_link_closed_callback(None)
|
||||
|
||||
def teardown(self):
|
||||
self.link.teardown()
|
||||
def unset_link_closed_callback(self): self.link.set_link_closed_callback(None)
|
||||
def teardown(self): self.link.teardown()
|
||||
|
||||
@property
|
||||
def rtt(self) -> float:
|
||||
return self.link.rtt
|
||||
def rtt(self) -> float: return self.link.rtt
|
||||
|
||||
def __str__(self):
|
||||
return f"Outlet RNS Link {self.link}"
|
||||
def __str__(self): return f"Outlet on {self.link}"
|
||||
|
||||
def __init__(self, link: RNS.Link):
|
||||
self.link = link
|
||||
@@ -435,7 +441,5 @@ class RNSOutlet(LSOutletBase):
|
||||
|
||||
@staticmethod
|
||||
def get_outlet(link: RNS.Link):
|
||||
if hasattr(link, "lsoutlet"):
|
||||
return link.lsoutlet
|
||||
|
||||
if hasattr(link, "lsoutlet"): return link.lsoutlet
|
||||
return RNSOutlet(link)
|
||||
@@ -385,6 +385,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
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)
|
||||
if sorting == "gravity" or sorting == "g":
|
||||
interfaces.sort(key=lambda i: i["gravity"], reverse=not sort_reverse)
|
||||
|
||||
|
||||
for ifstat in interfaces:
|
||||
@@ -418,13 +420,16 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
if ifstat["status"]: ss = "Up"
|
||||
else: ss = "Down"
|
||||
|
||||
if "gravity" in ifstat and ifstat["gravity"]: ss += ", gravity "+str(ifstat["gravity"])
|
||||
|
||||
if ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ACCESS_POINT: modestr = "Access Point"
|
||||
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_POINT_TO_POINT: modestr = "Point-to-Point"
|
||||
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_ROAMING: modestr = "Roaming"
|
||||
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_BOUNDARY: modestr = "Boundary"
|
||||
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_GATEWAY: modestr = "Gateway"
|
||||
elif ifstat["mode"] == RNS.Interfaces.Interface.Interface.MODE_INTERNAL: modestr = "Internal"
|
||||
else: modestr = "Full"
|
||||
|
||||
if "announces_to_internal" in ifstat and ifstat["announces_to_internal"]: modestr += " (a>i)"
|
||||
|
||||
if ifstat["clients"] != None:
|
||||
clients = ifstat["clients"]
|
||||
@@ -449,6 +454,9 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
clients_string = ""
|
||||
else:
|
||||
clients_string = "Clients : "+str(clients)
|
||||
if "blocked_ips" in ifstat:
|
||||
p = ifstat["blocked_ips"] > 0
|
||||
if p: clients_string += "\n Blocked : "+str(ifstat["blocked_ips"])+" IP"+"s" if p else ""
|
||||
|
||||
else:
|
||||
clients = None
|
||||
@@ -467,7 +475,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
print(" "+clients_string)
|
||||
|
||||
if not (name.startswith("Shared Instance[") or name.startswith("TCPInterface[Client") or name.startswith("LocalInterface[")):
|
||||
print(" Mode : {mode}".format(mode=modestr))
|
||||
print(f" Mode : {modestr}")
|
||||
|
||||
if "bitrate" in ifstat and ifstat["bitrate"] != None:
|
||||
print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"])))
|
||||
|
||||
+3
-1
@@ -70,7 +70,8 @@ LOG_NOTICE = 3
|
||||
LOG_INFO = 4
|
||||
LOG_VERBOSE = 5
|
||||
LOG_DEBUG = 6
|
||||
LOG_EXTREME = 7
|
||||
LOG_PATHING = 7
|
||||
LOG_EXTREME = 8
|
||||
|
||||
LOG_STDOUT = 0x91
|
||||
LOG_FILE = 0x92
|
||||
@@ -102,6 +103,7 @@ def loglevelname(level):
|
||||
if (level == LOG_INFO): return "[Info] "
|
||||
if (level == LOG_VERBOSE): return "[Verbose] "
|
||||
if (level == LOG_DEBUG): return "[Debug] "
|
||||
if (level == LOG_PATHING): return "[Pathing] "
|
||||
if (level == LOG_EXTREME): return "[Extra] "
|
||||
|
||||
return "Unknown"
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "1.3.2"
|
||||
__version__ = "1.4.2"
|
||||
|
||||
Vendored
+21
-40
@@ -6,14 +6,12 @@ from . import utils
|
||||
from .log import logger
|
||||
|
||||
def parse_reply(data):
|
||||
if not data:
|
||||
raise ConnectionAbortedError("Empty response: SAM API went offline")
|
||||
if not data: raise ConnectionAbortedError("Empty response: SAM API went offline")
|
||||
|
||||
try:
|
||||
msg = sam.Message(data.decode().strip())
|
||||
logger.debug("SAM reply: "+str(msg))
|
||||
except:
|
||||
raise ConnectionAbortedError("Invalid SAM response")
|
||||
except: raise ConnectionAbortedError("Invalid SAM response")
|
||||
|
||||
return msg
|
||||
|
||||
@@ -28,8 +26,7 @@ async def get_sam_socket(sam_address=sam.DEFAULT_ADDRESS, loop=None):
|
||||
reader, writer = await asyncio.open_connection(*sam_address)
|
||||
writer.write(sam.hello("3.1", "3.1"))
|
||||
reply = parse_reply(await reader.readline())
|
||||
if reply.ok:
|
||||
return (reader, writer)
|
||||
if reply.ok: return (reader, writer)
|
||||
else:
|
||||
writer.close()
|
||||
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
|
||||
@@ -49,10 +46,8 @@ async def dest_lookup(domain, sam_address=sam.DEFAULT_ADDRESS,
|
||||
writer.write(sam.naming_lookup(domain))
|
||||
reply = parse_reply(await reader.readline())
|
||||
writer.close()
|
||||
if reply.ok:
|
||||
return sam.Destination(reply["VALUE"])
|
||||
else:
|
||||
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
|
||||
if reply.ok: return sam.Destination(reply["VALUE"])
|
||||
else: raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
|
||||
|
||||
async def new_destination(sam_address=sam.DEFAULT_ADDRESS, loop=None,
|
||||
sig_type=sam.Destination.default_sig_type):
|
||||
@@ -91,15 +86,10 @@ async def create_session(session_name, sam_address=sam.DEFAULT_ADDRESS,
|
||||
"""
|
||||
logger.debug("Creating session {}".format(session_name))
|
||||
if destination:
|
||||
if type(destination) == sam.Destination:
|
||||
destination = destination
|
||||
else:
|
||||
destination = sam.Destination(
|
||||
destination, has_private_key=True)
|
||||
|
||||
if type(destination) == sam.Destination: destination = destination
|
||||
else: destination = sam.Destination(destination, has_private_key=True)
|
||||
dest_string = destination.private_key.base64
|
||||
else:
|
||||
dest_string = sam.TRANSIENT_DESTINATION
|
||||
else: dest_string = sam.TRANSIENT_DESTINATION
|
||||
|
||||
options = " ".join(["{}={}".format(k, v) for k, v in options.items()])
|
||||
|
||||
@@ -109,9 +99,7 @@ async def create_session(session_name, sam_address=sam.DEFAULT_ADDRESS,
|
||||
|
||||
reply = parse_reply(await reader.readline())
|
||||
if reply.ok:
|
||||
if not destination:
|
||||
destination = sam.Destination(
|
||||
reply["DESTINATION"], has_private_key=True)
|
||||
if not destination: destination = sam.Destination(reply["DESTINATION"], has_private_key=True)
|
||||
logger.debug(destination.base32)
|
||||
logger.debug("Session created {}".format(session_name))
|
||||
return (reader, writer)
|
||||
@@ -130,10 +118,8 @@ async def stream_connect(session_name, destination,
|
||||
:return: A (reader, writer) pair
|
||||
"""
|
||||
logger.debug("Connecting stream {}".format(session_name))
|
||||
if isinstance(destination, str) and not destination.endswith(".i2p"):
|
||||
destination = sam.Destination(destination)
|
||||
elif isinstance(destination, str):
|
||||
destination = await dest_lookup(destination, sam_address, loop)
|
||||
if isinstance(destination, str) and not destination.endswith(".i2p"): destination = sam.Destination(destination)
|
||||
elif isinstance(destination, str): destination = await dest_lookup(destination, sam_address, loop)
|
||||
|
||||
reader, writer = await get_sam_socket(sam_address, loop)
|
||||
writer.write(sam.stream_connect(session_name, destination.base64,
|
||||
@@ -158,8 +144,7 @@ async def stream_accept(session_name, sam_address=sam.DEFAULT_ADDRESS,
|
||||
reader, writer = await get_sam_socket(sam_address, loop)
|
||||
writer.write(sam.stream_accept(session_name, silent="false"))
|
||||
reply = parse_reply(await reader.readline())
|
||||
if reply.ok:
|
||||
return (reader, writer)
|
||||
if reply.ok: return (reader, writer)
|
||||
else:
|
||||
writer.close()
|
||||
raise exceptions.SAM_EXCEPTIONS[reply["RESULT"]]()
|
||||
@@ -183,9 +168,9 @@ class Session:
|
||||
:return: :class:`Session` object
|
||||
"""
|
||||
def __init__(self, session_name, sam_address=sam.DEFAULT_ADDRESS,
|
||||
loop=None, style="STREAM",
|
||||
signature_type=sam.Destination.default_sig_type,
|
||||
loop=None, style="STREAM", signature_type=sam.Destination.default_sig_type,
|
||||
destination=None, options={}):
|
||||
|
||||
self.session_name = session_name
|
||||
self.sam_address = sam_address
|
||||
self.loop = loop
|
||||
@@ -195,9 +180,8 @@ class Session:
|
||||
self.options = options
|
||||
|
||||
async def __aenter__(self):
|
||||
self.reader, self.writer = await create_session(self.session_name,
|
||||
sam_address=self.sam_address, loop=self.loop, style=self.style,
|
||||
signature_type=self.signature_type,
|
||||
self.reader, self.writer = await create_session(self.session_name, sam_address=self.sam_address, loop=self.loop,
|
||||
style=self.style, signature_type=self.signature_type,
|
||||
destination=self.destination, options=self.options)
|
||||
return self
|
||||
|
||||
@@ -214,16 +198,15 @@ class StreamConnection:
|
||||
:param loop: (optional) Event loop instance
|
||||
:return: :class:`StreamConnection` object
|
||||
"""
|
||||
def __init__(self, session_name, destination,
|
||||
sam_address=sam.DEFAULT_ADDRESS, loop=None):
|
||||
def __init__(self, session_name, destination, sam_address=sam.DEFAULT_ADDRESS, loop=None):
|
||||
self.session_name = session_name
|
||||
self.sam_address = sam_address
|
||||
self.loop = loop
|
||||
self.destination = destination
|
||||
|
||||
async def __aenter__(self):
|
||||
self.reader, self.writer = await stream_connect(self.session_name,
|
||||
self.destination, sam_address=self.sam_address, loop=self.loop)
|
||||
self.reader, self.writer = await stream_connect(self.session_name, self.destination,
|
||||
sam_address=self.sam_address, loop=self.loop)
|
||||
self.read = self.reader.read
|
||||
self.write = self.writer.write
|
||||
return self
|
||||
@@ -240,15 +223,13 @@ class StreamAcceptor:
|
||||
:param loop: (optional) Event loop instance
|
||||
:return: :class:`StreamAcceptor` object
|
||||
"""
|
||||
def __init__(self, session_name, sam_address=sam.DEFAULT_ADDRESS,
|
||||
loop=None):
|
||||
def __init__(self, session_name, sam_address=sam.DEFAULT_ADDRESS, loop=None):
|
||||
self.session_name = session_name
|
||||
self.sam_address = sam_address
|
||||
self.loop = loop
|
||||
|
||||
async def __aenter__(self):
|
||||
self.reader, self.writer = await stream_accept(self.session_name,
|
||||
sam_address=self.sam_address, loop=self.loop)
|
||||
self.reader, self.writer = await stream_accept(self.session_name, sam_address=self.sam_address, loop=self.loop)
|
||||
self.read = self.reader.read
|
||||
self.write = self.writer.write
|
||||
return self
|
||||
|
||||
Vendored
+2
-5
@@ -30,8 +30,7 @@ class PeerNotFound(SAMException):
|
||||
class Timeout(SAMException):
|
||||
"""The peer cannot be found on the network"""
|
||||
|
||||
SAM_EXCEPTIONS = {
|
||||
"CANT_REACH_PEER": CantReachPeer,
|
||||
SAM_EXCEPTIONS = { "CANT_REACH_PEER": CantReachPeer,
|
||||
"DUPLICATED_DEST": DuplicatedDest,
|
||||
"DUPLICATED_ID": DuplicatedId,
|
||||
"I2P_ERROR": I2PError,
|
||||
@@ -39,6 +38,4 @@ SAM_EXCEPTIONS = {
|
||||
"INVALID_KEY": InvalidKey,
|
||||
"KEY_NOT_FOUND": KeyNotFound,
|
||||
"PEER_NOT_FOUND": PeerNotFound,
|
||||
"TIMEOUT": Timeout,
|
||||
}
|
||||
|
||||
"TIMEOUT": Timeout }
|
||||
Vendored
+2
-5
@@ -3,7 +3,6 @@ from hashlib import sha256
|
||||
import struct
|
||||
import re
|
||||
|
||||
|
||||
I2P_B64_CHARS = "-~"
|
||||
|
||||
def i2p_b64encode(x):
|
||||
@@ -41,11 +40,9 @@ class Message(object):
|
||||
return self.opts[key]
|
||||
|
||||
@property
|
||||
def ok(self):
|
||||
return self["RESULT"] == "OK"
|
||||
def ok(self): return self["RESULT"] == "OK"
|
||||
|
||||
def __repr__(self):
|
||||
return self._reply_string
|
||||
def __repr__(self): return self._reply_string
|
||||
|
||||
|
||||
# SAM request messages
|
||||
|
||||
Vendored
+54
-63
@@ -15,15 +15,13 @@ async def proxy_data(reader, writer):
|
||||
while True:
|
||||
data = await reader.read(BUFFER_SIZE)
|
||||
if not data:
|
||||
logger.debug('no more data')
|
||||
break
|
||||
writer.write(data)
|
||||
except Exception as e:
|
||||
logger.debug('proxy_data_task exception {}'.format(e))
|
||||
except Exception as e: logger.debug('proxy_data_task exception {}'.format(e))
|
||||
finally:
|
||||
try:
|
||||
writer.close()
|
||||
except RuntimeError:
|
||||
pass
|
||||
try: writer.close()
|
||||
except RuntimeError: pass
|
||||
logger.debug('close connection')
|
||||
|
||||
class I2PTunnel(object):
|
||||
@@ -44,6 +42,7 @@ class I2PTunnel(object):
|
||||
|
||||
def __init__(self, local_address, destination=None, session_name=None,
|
||||
options={}, loop=None, sam_address=sam.DEFAULT_ADDRESS):
|
||||
|
||||
self.local_address = local_address
|
||||
self.destination = destination
|
||||
self.session_name = session_name or utils.generate_session_id()
|
||||
@@ -53,16 +52,13 @@ class I2PTunnel(object):
|
||||
|
||||
async def _pre_run(self):
|
||||
if not self.destination:
|
||||
self.destination = await aiosam.new_destination(
|
||||
sam_address=self.sam_address, loop=self.loop)
|
||||
_, self.session_writer = await aiosam.create_session(
|
||||
self.session_name, style=self.style, options=self.options,
|
||||
self.destination = await aiosam.new_destination(sam_address=self.sam_address, loop=self.loop)
|
||||
|
||||
_, self.session_writer = await aiosam.create_session(self.session_name, style=self.style, options=self.options,
|
||||
sam_address=self.sam_address,
|
||||
loop=self.loop, destination=self.destination)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the tunnel"""
|
||||
self.session_writer.close()
|
||||
def stop(self): self.session_writer.close()
|
||||
|
||||
class ClientTunnel(I2PTunnel):
|
||||
"""Client tunnel, a subclass of tunnel.I2PTunnel
|
||||
@@ -85,20 +81,25 @@ class ClientTunnel(I2PTunnel):
|
||||
"""A coroutine used to run the tunnel"""
|
||||
await self._pre_run()
|
||||
|
||||
self.status = { "setup_ran": False, "setup_failed": False, "exception": None, "connect_tasks": [] }
|
||||
self.status = { "setup_ran": False, "setup_failed": False, "exception": None,
|
||||
"connect_tasks": set(), "background_tasks": set() }
|
||||
|
||||
async def handle_client(client_reader, client_writer):
|
||||
"""Handle local client connection"""
|
||||
try:
|
||||
sc_task = aiosam.stream_connect(
|
||||
self.session_name, self.remote_destination,
|
||||
sc_task = aiosam.stream_connect(self.session_name, self.remote_destination,
|
||||
sam_address=self.sam_address, loop=self.loop)
|
||||
self.status["connect_tasks"].append(sc_task)
|
||||
|
||||
self.status["connect_tasks"].add(sc_task)
|
||||
|
||||
remote_reader, remote_writer = await sc_task
|
||||
asyncio.ensure_future(proxy_data(remote_reader, client_writer),
|
||||
loop=self.loop)
|
||||
asyncio.ensure_future(proxy_data(client_reader, remote_writer),
|
||||
loop=self.loop)
|
||||
task = asyncio.ensure_future(proxy_data(remote_reader, client_writer), loop=self.loop)
|
||||
self.status["background_tasks"].add(task)
|
||||
task.add_done_callback(self.status["background_tasks"].discard)
|
||||
|
||||
task = asyncio.ensure_future(proxy_data(client_reader, remote_writer), loop=self.loop)
|
||||
self.status["background_tasks"].add(task)
|
||||
task.add_done_callback(self.status["background_tasks"].discard)
|
||||
|
||||
except Exception as e:
|
||||
self.status["setup_ran"] = True
|
||||
@@ -135,14 +136,15 @@ class ServerTunnel(I2PTunnel):
|
||||
"""A coroutine used to run the tunnel"""
|
||||
await self._pre_run()
|
||||
|
||||
self.status = { "setup_ran": False, "setup_failed": False, "exception": None, "connect_tasks": [] }
|
||||
self.status = { "setup_ran": False, "setup_failed": False, "exception": None,
|
||||
"connect_tasks": set(), "background_tasks": set() }
|
||||
|
||||
async def handle_client(incoming, client_reader, client_writer):
|
||||
try:
|
||||
# data and dest may come in one chunk
|
||||
dest, data = incoming.split(b"\n", 1)
|
||||
remote_destination = sam.Destination(dest.decode())
|
||||
logger.debug("{} client connected: {}.b32.i2p".format(
|
||||
self.session_name, remote_destination.base32))
|
||||
logger.debug("{} client connected: {}.b32.i2p".format(self.session_name, remote_destination.base32))
|
||||
|
||||
except Exception as e:
|
||||
self.status["exception"] = e
|
||||
@@ -150,21 +152,20 @@ class ServerTunnel(I2PTunnel):
|
||||
data = None
|
||||
|
||||
try:
|
||||
sc_task = asyncio.wait_for(
|
||||
asyncio.open_connection(
|
||||
host=self.local_address[0],
|
||||
port=self.local_address[1]),
|
||||
timeout=5)
|
||||
self.status["connect_tasks"].append(sc_task)
|
||||
sc_task = asyncio.wait_for(asyncio.open_connection(host=self.local_address[0], port=self.local_address[1]), timeout=5)
|
||||
self.status["connect_tasks"].add(sc_task)
|
||||
|
||||
remote_reader, remote_writer = await sc_task
|
||||
if data: remote_writer.write(data)
|
||||
asyncio.ensure_future(proxy_data(remote_reader, client_writer),
|
||||
loop=self.loop)
|
||||
asyncio.ensure_future(proxy_data(client_reader, remote_writer),
|
||||
loop=self.loop)
|
||||
task = asyncio.ensure_future(proxy_data(remote_reader, client_writer), loop=self.loop)
|
||||
self.status["background_tasks"].add(task)
|
||||
task.add_done_callback(self.status["background_tasks"].discard)
|
||||
|
||||
except ConnectionRefusedError:
|
||||
task = asyncio.ensure_future(proxy_data(client_reader, remote_writer), loop=self.loop)
|
||||
self.status["background_tasks"].add(task)
|
||||
task.add_done_callback(self.status["background_tasks"].discard)
|
||||
|
||||
except ConnectionRefusedError as e:
|
||||
client_writer.close()
|
||||
self.status["exception"] = e
|
||||
self.status["setup_failed"] = True
|
||||
@@ -172,14 +173,14 @@ class ServerTunnel(I2PTunnel):
|
||||
async def server_loop():
|
||||
try:
|
||||
while True:
|
||||
client_reader, client_writer = await aiosam.stream_accept(
|
||||
self.session_name, sam_address=self.sam_address,
|
||||
client_reader, client_writer = await aiosam.stream_accept(self.session_name, sam_address=self.sam_address,
|
||||
loop=self.loop)
|
||||
incoming = await client_reader.read(BUFFER_SIZE)
|
||||
asyncio.ensure_future(handle_client(
|
||||
incoming, client_reader, client_writer), loop=self.loop)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
task = asyncio.ensure_future(handle_client(incoming, client_reader, client_writer), loop=self.loop)
|
||||
self.status["background_tasks"].add(task)
|
||||
task.add_done_callback(self.status["background_tasks"].discard)
|
||||
|
||||
except asyncio.CancelledError: pass
|
||||
|
||||
self.server_loop = asyncio.ensure_future(server_loop(), loop=self.loop)
|
||||
self.status["setup_ran"] = True
|
||||
@@ -191,16 +192,11 @@ class ServerTunnel(I2PTunnel):
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('type', metavar="TYPE", choices=('server', 'client'),
|
||||
help="Tunnel type (server or client)")
|
||||
parser.add_argument('address', metavar="ADDRESS",
|
||||
help="Local address (e.g. 127.0.0.1:8000)")
|
||||
parser.add_argument('--debug', '-d', action='store_true',
|
||||
help='Debugging')
|
||||
parser.add_argument('--key', '-k', default='', metavar='PRIVATE_KEY',
|
||||
help='Path to private key file')
|
||||
parser.add_argument('--destination', '-D', default='',
|
||||
metavar='DESTINATION', help='Remote destination')
|
||||
parser.add_argument('type', metavar="TYPE", choices=('server', 'client'), help="Tunnel type (server or client)")
|
||||
parser.add_argument('address', metavar="ADDRESS", help="Local address (e.g. 127.0.0.1:8000)")
|
||||
parser.add_argument('--debug', '-d', action='store_true', help='Debugging')
|
||||
parser.add_argument('--key', '-k', default='', metavar='PRIVATE_KEY', help='Path to private key file')
|
||||
parser.add_argument('--destination', '-D', default='', metavar='DESTINATION', help='Remote destination')
|
||||
args = parser.parse_args()
|
||||
|
||||
SAM_ADDRESS = utils.get_sam_address()
|
||||
@@ -209,26 +205,21 @@ if __name__ == '__main__':
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.set_debug(args.debug)
|
||||
|
||||
if args.key:
|
||||
destination = sam.Destination(path=args.key, has_private_key=True)
|
||||
else:
|
||||
destination = None
|
||||
if args.key: destination = sam.Destination(path=args.key, has_private_key=True)
|
||||
else: destination = None
|
||||
|
||||
local_address = utils.address_from_string(args.address)
|
||||
|
||||
if args.type == "client":
|
||||
tunnel = ClientTunnel(args.destination, local_address, loop=loop,
|
||||
destination=destination, sam_address=SAM_ADDRESS)
|
||||
tunnel = ClientTunnel(args.destination, local_address, loop=loop, destination=destination, sam_address=SAM_ADDRESS)
|
||||
|
||||
elif args.type == "server":
|
||||
tunnel = ServerTunnel(local_address, loop=loop, destination=destination,
|
||||
sam_address=SAM_ADDRESS)
|
||||
tunnel = ServerTunnel(local_address, loop=loop, destination=destination, sam_address=SAM_ADDRESS)
|
||||
|
||||
asyncio.ensure_future(tunnel.run(), loop=loop)
|
||||
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
tunnel.stop()
|
||||
try: loop.run_forever()
|
||||
except KeyboardInterrupt: tunnel.stop()
|
||||
finally:
|
||||
loop.stop()
|
||||
loop.close()
|
||||
|
||||
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: af83acf0d95a344cb0386ad87adf5695
|
||||
config: 4e2f0f7390586c35b5c4b57cd1fc8b5b
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1326,7 +1326,7 @@ Each document is a numbered directory containing:
|
||||
|
||||
**Nomad Network Interface**
|
||||
|
||||
When the Nomad Network page node is enabled, work documents are viewable through the web interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.
|
||||
When the Nomad Network page node is enabled, work documents are viewable through the nomadnet interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.
|
||||
|
||||
Cryptographic Attribution
|
||||
-------------------------
|
||||
@@ -1491,3 +1491,22 @@ To view signature information for commits, use Git's standard ``--show-signature
|
||||
Refactored module
|
||||
|
||||
The output shows whether the commit signature is valid, and whether the author field matches the signing identity.
|
||||
|
||||
.. tip::
|
||||
If you want to display both the identity hash and LXMF address for authors, you can generate a ``.mailmap`` file that resolves identities to LXMF addresses with the following script:
|
||||
|
||||
.. code::
|
||||
#!/bin/bash
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
cd $DIR
|
||||
|
||||
id_regex="<([0-9a-f]{32})( .*)*>"
|
||||
extract_id="s/.*$id_regex/\1/g"
|
||||
|
||||
rm -f .mailmap
|
||||
git shortlog -se | grep -Ee "$id_regex" | sed -r "$extract_id" | while read -r id ; do
|
||||
if lxmf=$(rnid -i $id -H lxmf.delivery | grep -Ee "destination for this Identity is" | sed -r "$extract_id"); then
|
||||
echo "<$id lxmf:$lxmf> <$id>" >> .mailmap
|
||||
fi
|
||||
done
|
||||
@@ -230,6 +230,51 @@ specify the target Yggdrasil IPv6 address and port, like so:
|
||||
target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5
|
||||
target_port = 4343
|
||||
|
||||
Automated Blocking
|
||||
------------------
|
||||
Listener instances of ``BackboneInterface`` will automatically block fast-flapping clients,
|
||||
that repeatedly connect for a short amount of time, and then disconnect. Such behavior usually
|
||||
occurs from clients trying to spam the network with announces or path requests, by connecting, dumping
|
||||
a massive amount of requests, and then disconnecting in an attempt to bypass rate limits.
|
||||
|
||||
While such bypass attempts only have very limited effect on the amount of spam actually dumped (ingress limits
|
||||
trigger immediately once a client exceeds rate limits), the behavior is often seen anyways, and
|
||||
causes log noise and needless interface rotation. The fast-flapping block ensures such clients
|
||||
are never allocated an interface on the transport instance.
|
||||
|
||||
Another common cause can simply be clients using implementations that are broken, or automated
|
||||
scanning tools attempting to connect to the instance.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[Backbone Listener]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
listen_on = 0.0.0.0
|
||||
port = 4242
|
||||
|
||||
# Whether to enable blocking
|
||||
block_fast_flapping = yes
|
||||
|
||||
# How long an IP address stays
|
||||
# blocked, in minutes. Set to
|
||||
# 12 hours by default.
|
||||
fast_flapping_block_time = 720
|
||||
|
||||
# The minimum time, in seconds,
|
||||
# an interface must stay connected
|
||||
# to be considered not fast-flapping.
|
||||
fast_flapping_threshold = 20
|
||||
|
||||
# Amount of fast flaps a remote
|
||||
# IP can perform before having
|
||||
# blocking triggered.
|
||||
fast_flapping_grace = 5
|
||||
|
||||
The configuration options listed in the example above are the *default values*, and you do
|
||||
not need to add them for automated blocking to work, but you can use them to change the
|
||||
behavior from the defaults, if necessary.
|
||||
|
||||
.. _interfaces-tcps:
|
||||
|
||||
TCP Server Interface
|
||||
@@ -998,7 +1043,13 @@ On a real system, you should make the script robust enough to deal with intermit
|
||||
**Physical Location**
|
||||
|
||||
``latitude``, ``longitude``, ``height``
|
||||
Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters.
|
||||
Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters above mean sea level.
|
||||
|
||||
``location_cmd``
|
||||
Optional path to executable or script that returns the physical coordinates for the interface. This can be used instead of manually setting ``latitude``, ``longitude`` and ``height``. Reticulum expects the script to output the location data to ``stdout`` on a single line, separated by commas, with values as floating point numbers in the format ``LAT, LON, HEIGHT``. Coordinates should be in decimal degrees, height in meters above mean sea level.
|
||||
|
||||
.. note::
|
||||
The height value for interface discovery is specified in *height above mean sea level*! This is the geoid-corrected height value, and is distinct from GPS altitude. If you manually specify height, it will typically be set to what you find on a topographical map. If you are using a script to output the location data, make sure that you are using the geoid-corrected altitude, not height above the GPS ellipsoid. Most GPS systems will make both figures available.
|
||||
|
||||
**Radio Parameters**
|
||||
|
||||
@@ -1149,6 +1200,15 @@ These can be used to control various aspects of interface behaviour.
|
||||
expected to connect momentarily, use the network, and then
|
||||
disappear again.
|
||||
|
||||
- See the :ref:`Interface Modes<interfaces-modes>` section
|
||||
below for a reference on additional modes.
|
||||
|
||||
* | The ``gravity`` option specifies the pathing affinity of an interface.
|
||||
If not set specifically, and unless otherwise configured by
|
||||
``default_gravity`` or ``autoconnect_interface_gravity``,
|
||||
it defaults to ``0``. Positive values increase pathing affinity,
|
||||
and negative values decrease it.
|
||||
|
||||
* | The ``outgoing`` option sets whether an interface is allowed
|
||||
to transmit. Defaults to ``True``. If set to ``False`` or ``No``
|
||||
the interface will only receive data, and never transmit.
|
||||
@@ -1207,6 +1267,27 @@ These can be used to control various aspects of interface behaviour.
|
||||
link or a remote TCP tunnel) solely to discover better local
|
||||
infrastructure, which then supersedes the bootstrap interface.
|
||||
|
||||
|
||||
* | The ``recursive_prs`` option allows you to enable recursive path
|
||||
discovery on an interface regardless of its configured interface
|
||||
mode. When this option is enabled, Reticulum will attempt to
|
||||
recursively discover paths for path requests received on this
|
||||
interface.
|
||||
|
||||
|
||||
* | The ``announces_from_internal`` option controls whether any
|
||||
announces *received on* an ``internal`` mode interface will
|
||||
propagate *out* on the interface this option is set on. Note
|
||||
that this controls *announce propagation*; even if announces
|
||||
do not propagate out when received, paths to destinations on
|
||||
``internal`` mode interfaces may still be resolvable by means
|
||||
of path requests.
|
||||
|
||||
|
||||
* | To allow an interface that would *otherwise not* propagate
|
||||
announces to ``internal`` mode interfaces to do so, you can
|
||||
configure the ``announces_to_internal`` option to ``True``.
|
||||
|
||||
.. _interfaces-modes:
|
||||
|
||||
Interface Modes
|
||||
@@ -1284,6 +1365,15 @@ the default mode.
|
||||
public Transport Node available on the Internet, the interface
|
||||
connecting over the Internet should be set to ``boundary`` mode.
|
||||
|
||||
* | The ``internal`` mode designates interfaces that belong to a
|
||||
network different from any marked as ``boundary``. Announces from
|
||||
a ``boundary`` interface will not propagate to interfaces set as
|
||||
``internal``, but announces *will* propagate from ``internal`` *to*
|
||||
``boundary``. Devices on the ``internal`` side of the network will
|
||||
still be able to resolve paths to destinations across the boundary
|
||||
when needed, since recursive path requests are enabled for ``internal``
|
||||
mode interfaces by default.
|
||||
|
||||
For a table describing the impact of all modes on announce propagation,
|
||||
please see the :ref:`Announce Propagation Rules<understanding-announcepropagation>` section.
|
||||
|
||||
|
||||
@@ -707,32 +707,60 @@ Announce Propagation Rules
|
||||
|
||||
The following table illustrates the rules for automatically propagating announces from one interface type to another, for all possible combinations. For the purpose of announce propagation, the *Full* and *Gateway* modes are identical.
|
||||
|
||||
See the :ref:`Interface Modes<interfaces-modes>` section for a conceptual overview of the different interface modes, and how they are configured.
|
||||
|
||||
.. image:: graphics/if_mode_graph_b.png
|
||||
|
||||
See the :ref:`Interface Modes<interfaces-modes>` section for a conceptual overview of the different interface modes, and how they are configured.
|
||||
Or, represented in table form:
|
||||
|
||||
.. image:: graphics/if_mode_graph_c.png
|
||||
|
||||
..
|
||||
(.. code-block:: text)
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼───> Full >───┼── ✕ ── AP
|
||||
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✓ ──┼───> Full >───┼── ✓ ── Boundary
|
||||
Internal ── ✓ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✓ ──┘ └── ✓ ── Roaming
|
||||
|
||||
Full ────── ✕ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✕ ──┼────> AP >────┼── ✕ ── AP
|
||||
Boundary ── ✕ ──┤ ├── ✓ ── Boundary
|
||||
AP ──────── ✕ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✕ ──┼────> AP >────┼── ✓ ── Boundary
|
||||
Internal ── ✕ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✕ ──┘ └── ✓ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼─> Roaming >──┼── ✕ ── AP
|
||||
Boundary ── ✕ ──┤ ├── ✕ ── Boundary
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✕ ──┼─> Roaming >──┼── ✕ ── Boundary
|
||||
Internal ── ✕ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼─> Boundary >─┼── ✕ ── AP
|
||||
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✓ ──┼─> Boundary >─┼── ✓ ── Boundary
|
||||
Internal ── ✓ ──┤ ├── ✕ ── Internal
|
||||
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✕ ──┼─> Internal >─┼── ✓ ── Boundary
|
||||
Internal ── ✓ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✓ ──┘ └── ✕ ── Roaming
|
||||
|
||||
dest → Full AP Boundary Roaming Gateway Internal
|
||||
source ↓
|
||||
Full ✓ ✕ ✓ ✓ ✓ ✓
|
||||
Gateway ✓ ✕ ✓ ✓ ✓ ✓
|
||||
AP ✓ ✕ ✓ ✓ ✓ ✓
|
||||
Boundary ✓ ✕ ✓ ✕ ✓ ✕
|
||||
Internal ✓ ✕ ✓ ✕ ✓ ✓
|
||||
Roaming ✓ ✕ ✕ ✕ ✓ ✓
|
||||
|
||||
✓ Will rebroadcast announce
|
||||
✕ Will not rebroadcast announce
|
||||
|
||||
For a visually intuitive overview of how announce propagation rules function in practice, you can take a look at the `Announce Propagation Simulator <https://rns.moscow/announce-sim.html>`_ hosted by `rns.moscow <https://rns.moscow/>`_.
|
||||
|
||||
|
||||
.. _understanding-primitives:
|
||||
|
||||
|
||||
@@ -1003,9 +1003,9 @@ available. These are only recognised immediately after a newline character:
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config, -c CONFIG path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY
|
||||
path to identity file to use
|
||||
--config, -c PATH path to config directory
|
||||
--rnsconfig, -c PATH path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY path to identity file to use
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
@@ -1015,10 +1015,8 @@ available. These are only recognised immediately after a newline character:
|
||||
does not provide one or when remote command execution
|
||||
is disabled; if no command is specified, the default
|
||||
shell of the user running rnsh will be used
|
||||
-s, --service SERVICE
|
||||
service name for identity file if not the default
|
||||
-b, --announce PERIOD
|
||||
announce on startup and every PERIOD seconds; specify
|
||||
-s, --service SERVICE service name for identity file if not the default
|
||||
-b, --announce PERIOD announce on startup and every PERIOD seconds; specify
|
||||
0 to announce on startup only
|
||||
-a, --allowed HASH allow this identity to connect (may be specified
|
||||
multiple times); allowed identities can also be
|
||||
@@ -1028,13 +1026,11 @@ available. These are only recognised immediately after a newline character:
|
||||
-A, --remote-command-as-args
|
||||
concatenate remote command to the argument list of the
|
||||
default program or shell
|
||||
-C, --no-remote-command
|
||||
disable executing command lines received from the
|
||||
-C, --no-remote-command disable executing command lines received from the
|
||||
remote initiator
|
||||
-N, --no-id disable identity announcement on connect
|
||||
-m, --mirror return with the exit code of the remote process
|
||||
-w, --timeout SECONDS
|
||||
connect and request timeout in seconds
|
||||
-w, --timeout SECONDS connect and request timeout in seconds
|
||||
|
||||
When specifying a command to execute, separate rnsh options from the command
|
||||
and its arguments with --. For example:
|
||||
@@ -1224,6 +1220,12 @@ The configuration snippet below contains an example of setting these additional
|
||||
network_identity = ~/.reticulum/storage/identities/my_network
|
||||
...
|
||||
|
||||
For more fine-grained control over how discovered interfaces are auto-connected, additional options are provided for configuraiton. These are not necessary to set in most cases, but can be useful in certain situations.
|
||||
|
||||
* The ``autoconnect_interface_mode`` options specifies which mode discovered interfaces should be created with when auto-connecting.
|
||||
|
||||
* The ``autoconnect_announces_to_internal`` option allows you to specify that auto-connected interfaces should propagate announces to ``internal`` mode interfaces, even if the auto-connected interface's mode would normally not allow for this.
|
||||
|
||||
Remote Management
|
||||
-----------------
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ img[src*="if_mode_graph_b.png"] {
|
||||
background-color: rgb(169, 177, 186);
|
||||
}
|
||||
|
||||
img[src*="if_mode_graph_c.png"] {
|
||||
background-color: rgb(169, 177, 186);
|
||||
}
|
||||
|
||||
dt.sig {
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 1.75rem;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const DOCUMENTATION_OPTIONS = {
|
||||
VERSION: '1.3.2',
|
||||
VERSION: '1.4.2',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<link rel="prefetch" href="_static/rns_logo_512.png" as="image">
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<title>Distributed Development - Reticulum Network Stack 1.3.2 documentation</title>
|
||||
<title>Distributed Development - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -431,7 +431,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Code Examples - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 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">
|
||||
@@ -3648,7 +3648,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -296,7 +296,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +5,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 --><title>Index - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 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">
|
||||
@@ -720,6 +720,8 @@
|
||||
<li><a href="reference.html#RNS.Link.set_link_closed_callback">set_link_closed_callback() (RNS.Link method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Destination.set_link_established_callback">set_link_established_callback() (RNS.Destination method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Destination.set_max_request_size">set_max_request_size() (RNS.Destination method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Destination.set_packet_callback">set_packet_callback() (RNS.Destination method)</a>
|
||||
|
||||
@@ -732,11 +734,11 @@
|
||||
<li><a href="reference.html#RNS.Destination.set_proof_strategy">set_proof_strategy() (RNS.Destination method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Destination.set_ratchet_interval">set_ratchet_interval() (RNS.Destination method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Link.set_remote_identified_callback">set_remote_identified_callback() (RNS.Link method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Link.set_remote_identified_callback">set_remote_identified_callback() (RNS.Link method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Link.set_resource_callback">set_resource_callback() (RNS.Link method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Link.set_resource_concluded_callback">set_resource_concluded_callback() (RNS.Link method)</a>
|
||||
@@ -846,7 +848,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Getting Started Fast - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 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">
|
||||
@@ -776,7 +776,7 @@ section of this manual.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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>
|
||||
|
||||
+24
-6
@@ -7,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Git Over Reticulum - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 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">
|
||||
@@ -1306,7 +1306,7 @@ adm:9710b86ba12c42d1d8f30f74fe509286
|
||||
<li><p><code class="docutils literal notranslate"><span class="pre">N</span></code> - Numbered comment files (msgpack format)</p></li>
|
||||
</ul>
|
||||
<p><strong>Nomad Network Interface</strong></p>
|
||||
<p>When the Nomad Network page node is enabled, work documents are viewable through the web interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.</p>
|
||||
<p>When the Nomad Network page node is enabled, work documents are viewable through the nomadnet interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.</p>
|
||||
</section>
|
||||
<section id="cryptographic-attribution">
|
||||
<h3>Cryptographic Attribution<a class="headerlink" href="#cryptographic-attribution" title="Link to this heading">¶</a></h3>
|
||||
@@ -1437,6 +1437,24 @@ Date: Mon Jan 15 09:30:00 2026 +0100
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The output shows whether the commit signature is valid, and whether the author field matches the signing identity.</p>
|
||||
<div class="admonition tip">
|
||||
<p class="admonition-title">Tip</p>
|
||||
<p>If you want to display both the identity hash and LXMF address for authors, you can generate a <code class="docutils literal notranslate"><span class="pre">.mailmap</span></code> file that resolves identities to LXMF addresses with the following script:</p>
|
||||
<div class="highlight-#!/bin/bash notranslate"><div class="highlight"><pre><span></span>DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
cd $DIR
|
||||
|
||||
id_regex="<([0-9a-f]{32})( .*)*>"
|
||||
extract_id="s/.*$id_regex/\1/g"
|
||||
|
||||
rm -f .mailmap
|
||||
git shortlog -se | grep -Ee "$id_regex" | sed -r "$extract_id" | while read -r id ; do
|
||||
if lxmf=$(rnid -i $id -H lxmf.delivery | grep -Ee "destination for this Identity is" | sed -r "$extract_id"); then
|
||||
echo "<$id lxmf:$lxmf> <$id>" >> .mailmap
|
||||
fi
|
||||
done
|
||||
</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
@@ -1576,7 +1594,7 @@ Date: Mon Jan 15 09:30:00 2026 +0100
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Communications Hardware - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -676,7 +676,7 @@ can be used with Reticulum. This includes virtual software modems such as
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 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">
|
||||
@@ -486,6 +486,7 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#backbone-interface">Backbone Interface</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#listeners">Listeners</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#connecting-remotes">Connecting Remotes</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#automated-blocking">Automated Blocking</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#tcp-server-interface">TCP Server Interface</a></li>
|
||||
@@ -719,7 +720,7 @@ to participate in the development of Reticulum itself.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Configuring Interfaces - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 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">
|
||||
@@ -460,6 +460,47 @@ specify the target Yggdrasil IPv6 address and port, like so:</p>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="automated-blocking">
|
||||
<h3>Automated Blocking<a class="headerlink" href="#automated-blocking" title="Link to this heading">¶</a></h3>
|
||||
<p>Listener instances of <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code> will automatically block fast-flapping clients,
|
||||
that repeatedly connect for a short amount of time, and then disconnect. Such behavior usually
|
||||
occurs from clients trying to spam the network with announces or path requests, by connecting, dumping
|
||||
a massive amount of requests, and then disconnecting in an attempt to bypass rate limits.</p>
|
||||
<p>While such bypass attempts only have very limited effect on the amount of spam actually dumped (ingress limits
|
||||
trigger immediately once a client exceeds rate limits), the behavior is often seen anyways, and
|
||||
causes log noise and needless interface rotation. The fast-flapping block ensures such clients
|
||||
are never allocated an interface on the transport instance.</p>
|
||||
<p>Another common cause can simply be clients using implementations that are broken, or automated
|
||||
scanning tools attempting to connect to the instance.</p>
|
||||
<div class="highlight-ini notranslate"><div class="highlight"><pre><span></span><span class="k">[[Backbone Listener]]</span>
|
||||
<span class="w"> </span><span class="na">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">BackboneInterface</span>
|
||||
<span class="w"> </span><span class="na">enabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">yes</span>
|
||||
<span class="w"> </span><span class="na">listen_on</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">0.0.0.0</span>
|
||||
<span class="w"> </span><span class="na">port</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">4242</span>
|
||||
|
||||
<span class="w"> </span><span class="c1"># Whether to enable blocking</span>
|
||||
<span class="w"> </span><span class="na">block_fast_flapping</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">yes</span>
|
||||
|
||||
<span class="w"> </span><span class="c1"># How long an IP address stays</span>
|
||||
<span class="w"> </span><span class="c1"># blocked, in minutes. Set to</span>
|
||||
<span class="w"> </span><span class="c1"># 12 hours by default.</span>
|
||||
<span class="w"> </span><span class="na">fast_flapping_block_time</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">720</span>
|
||||
|
||||
<span class="w"> </span><span class="c1"># The minimum time, in seconds,</span>
|
||||
<span class="w"> </span><span class="c1"># an interface must stay connected</span>
|
||||
<span class="w"> </span><span class="c1"># to be considered not fast-flapping.</span>
|
||||
<span class="w"> </span><span class="na">fast_flapping_threshold</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">20</span>
|
||||
|
||||
<span class="w"> </span><span class="c1"># Amount of fast flaps a remote</span>
|
||||
<span class="w"> </span><span class="c1"># IP can perform before having</span>
|
||||
<span class="w"> </span><span class="c1"># blocking triggered.</span>
|
||||
<span class="w"> </span><span class="na">fast_flapping_grace</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">5</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>The configuration options listed in the example above are the <em>default values</em>, and you do
|
||||
not need to add them for automated blocking to work, but you can use them to change the
|
||||
behavior from the defaults, if necessary.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="tcp-server-interface">
|
||||
<span id="interfaces-tcps"></span><h2>TCP Server Interface<a class="headerlink" href="#tcp-server-interface" title="Link to this heading">¶</a></h2>
|
||||
@@ -1154,9 +1195,15 @@ curl<span class="w"> </span>-s<span class="w"> </span>ip.me
|
||||
</dl>
|
||||
<p><strong>Physical Location</strong></p>
|
||||
<dl class="simple">
|
||||
<dt><code class="docutils literal notranslate"><span class="pre">latitude</span></code>, <code class="docutils literal notranslate"><span class="pre">longitude</span></code>, <code class="docutils literal notranslate"><span class="pre">height</span></code></dt><dd><p>Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters.</p>
|
||||
<dt><code class="docutils literal notranslate"><span class="pre">latitude</span></code>, <code class="docutils literal notranslate"><span class="pre">longitude</span></code>, <code class="docutils literal notranslate"><span class="pre">height</span></code></dt><dd><p>Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters above mean sea level.</p>
|
||||
</dd>
|
||||
<dt><code class="docutils literal notranslate"><span class="pre">location_cmd</span></code></dt><dd><p>Optional path to executable or script that returns the physical coordinates for the interface. This can be used instead of manually setting <code class="docutils literal notranslate"><span class="pre">latitude</span></code>, <code class="docutils literal notranslate"><span class="pre">longitude</span></code> and <code class="docutils literal notranslate"><span class="pre">height</span></code>. Reticulum expects the script to output the location data to <code class="docutils literal notranslate"><span class="pre">stdout</span></code> on a single line, separated by commas, with values as floating point numbers in the format <code class="docutils literal notranslate"><span class="pre">LAT,</span> <span class="pre">LON,</span> <span class="pre">HEIGHT</span></code>. Coordinates should be in decimal degrees, height in meters above mean sea level.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>The height value for interface discovery is specified in <em>height above mean sea level</em>! This is the geoid-corrected height value, and is distinct from GPS altitude. If you manually specify height, it will typically be set to what you find on a topographical map. If you are using a script to output the location data, make sure that you are using the geoid-corrected altitude, not height above the GPS ellipsoid. Most GPS systems will make both figures available.</p>
|
||||
</div>
|
||||
<p><strong>Radio Parameters</strong></p>
|
||||
<p>For physical radio interfaces like <code class="docutils literal notranslate"><span class="pre">RNodeInterface</span></code> or <code class="docutils literal notranslate"><span class="pre">KISSInterface</span></code>, the following optional parameters allow you to broadcast the operating frequency and characteristics, allowing clients to verify compatibility before connecting:</p>
|
||||
<dl class="simple">
|
||||
@@ -1288,10 +1335,20 @@ someone is actually using them. An example of this could
|
||||
be a radio interface serving a wide area, where users are
|
||||
expected to connect momentarily, use the network, and then
|
||||
disappear again.</p></li>
|
||||
<li><p>See the <a class="reference internal" href="#interfaces-modes"><span class="std std-ref">Interface Modes</span></a> section
|
||||
below for a reference on additional modes.</p></li>
|
||||
</ul>
|
||||
</div></blockquote>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">gravity</span></code> option specifies the pathing affinity of an interface.
|
||||
If not set specifically, and unless otherwise configured by
|
||||
<code class="docutils literal notranslate"><span class="pre">default_gravity</span></code> or <code class="docutils literal notranslate"><span class="pre">autoconnect_interface_gravity</span></code>,
|
||||
it defaults to <code class="docutils literal notranslate"><span class="pre">0</span></code>. Positive values increase pathing affinity,
|
||||
and negative values decrease it.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">outgoing</span></code> option sets whether an interface is allowed
|
||||
to transmit. Defaults to <code class="docutils literal notranslate"><span class="pre">True</span></code>. If set to <code class="docutils literal notranslate"><span class="pre">False</span></code> or <code class="docutils literal notranslate"><span class="pre">No</span></code>
|
||||
the interface will only receive data, and never transmit.</div>
|
||||
@@ -1361,6 +1418,30 @@ link or a remote TCP tunnel) solely to discover better local
|
||||
infrastructure, which then supersedes the bootstrap interface.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">recursive_prs</span></code> option allows you to enable recursive path
|
||||
discovery on an interface regardless of its configured interface
|
||||
mode. When this option is enabled, Reticulum will attempt to
|
||||
recursively discover paths for path requests received on this
|
||||
interface.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">announces_from_internal</span></code> option controls whether any
|
||||
announces <em>received on</em> an <code class="docutils literal notranslate"><span class="pre">internal</span></code> mode interface will
|
||||
propagate <em>out</em> on the interface this option is set on. Note
|
||||
that this controls <em>announce propagation</em>; even if announces
|
||||
do not propagate out when received, paths to destinations on
|
||||
<code class="docutils literal notranslate"><span class="pre">internal</span></code> mode interfaces may still be resolvable by means
|
||||
of path requests.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">To allow an interface that would <em>otherwise not</em> propagate
|
||||
announces to <code class="docutils literal notranslate"><span class="pre">internal</span></code> mode interfaces to do so, you can
|
||||
configure the <code class="docutils literal notranslate"><span class="pre">announces_to_internal</span></code> option to <code class="docutils literal notranslate"><span class="pre">True</span></code>.</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div></blockquote>
|
||||
</section>
|
||||
@@ -1451,6 +1532,17 @@ public Transport Node available on the Internet, the interface
|
||||
connecting over the Internet should be set to <code class="docutils literal notranslate"><span class="pre">boundary</span></code> mode.</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><div class="line-block">
|
||||
<div class="line">The <code class="docutils literal notranslate"><span class="pre">internal</span></code> mode designates interfaces that belong to a
|
||||
network different from any marked as <code class="docutils literal notranslate"><span class="pre">boundary</span></code>. Announces from
|
||||
a <code class="docutils literal notranslate"><span class="pre">boundary</span></code> interface will not propagate to interfaces set as
|
||||
<code class="docutils literal notranslate"><span class="pre">internal</span></code>, but announces <em>will</em> propagate from <code class="docutils literal notranslate"><span class="pre">internal</span></code> <em>to</em>
|
||||
<code class="docutils literal notranslate"><span class="pre">boundary</span></code>. Devices on the <code class="docutils literal notranslate"><span class="pre">internal</span></code> side of the network will
|
||||
still be able to resolve paths to destinations across the boundary
|
||||
when needed, since recursive path requests are enabled for <code class="docutils literal notranslate"><span class="pre">internal</span></code>
|
||||
mode interfaces by default.</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div></blockquote>
|
||||
<p>For a table describing the impact of all modes on announce propagation,
|
||||
@@ -1738,6 +1830,7 @@ interface basis under the relevant interface configuration section.</p>
|
||||
<li><a class="reference internal" href="#backbone-interface">Backbone Interface</a><ul>
|
||||
<li><a class="reference internal" href="#listeners">Listeners</a></li>
|
||||
<li><a class="reference internal" href="#connecting-remotes">Connecting Remotes</a></li>
|
||||
<li><a class="reference internal" href="#automated-blocking">Automated Blocking</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#tcp-server-interface">TCP Server Interface</a></li>
|
||||
@@ -1774,7 +1867,7 @@ interface basis under the relevant interface configuration section.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Reticulum License - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -345,7 +345,7 @@ SOFTWARE.
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Building Networks - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -664,7 +664,7 @@ differently than a mobile device roaming between radio cells.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>API Reference - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 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">
|
||||
@@ -925,6 +925,20 @@ proofs should be returned for received packets.</p>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Destination.set_max_request_size">
|
||||
<span class="sig-name descname"><span class="pre">set_max_request_size</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">max_request_size</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Destination.set_max_request_size" title="Link to this definition">¶</a></dt>
|
||||
<dd><p>Sets the maximum accepted request size for registered request handlers.</p>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><p><strong>max_request_size</strong> – The maximum accepted request size in bytes, as an integer.</p>
|
||||
</dd>
|
||||
<dt class="field-even">Raises<span class="colon">:</span></dt>
|
||||
<dd class="field-even"><p><code class="docutils literal notranslate"><span class="pre">TypeError</span></code> or <code class="docutils literal notranslate"><span class="pre">ValueError</span></code> if any of the argument is invalid.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Destination.register_request_handler">
|
||||
<span class="sig-name descname"><span class="pre">register_request_handler</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">path</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">response_generator</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">allow</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">ALLOW_NONE</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">allowed_list</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">auto_compress</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">True</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Destination.register_request_handler" title="Link to this definition">¶</a></dt>
|
||||
@@ -1341,7 +1355,7 @@ thus preserved. This method can be used for authentication.</p>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Link.request">
|
||||
<span class="sig-name descname"><span class="pre">request</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">path</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">data</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">response_callback</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">failed_callback</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">progress_callback</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">timeout</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Link.request" title="Link to this definition">¶</a></dt>
|
||||
<span class="sig-name descname"><span class="pre">request</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">path</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">data</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">response_callback</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">failed_callback</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">progress_callback</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">timeout</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">max_response_size</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">None</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Link.request" title="Link to this definition">¶</a></dt>
|
||||
<dd><p>Sends a request to the remote peer.</p>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
@@ -1350,6 +1364,7 @@ thus preserved. This method can be used for authentication.</p>
|
||||
<li><p><strong>response_callback</strong> – An optional function or method with the signature <em>response_callback(request_receipt)</em> to be called when a response is received. See the <a class="reference internal" href="examples.html#example-request"><span class="std std-ref">Request Example</span></a> for more info.</p></li>
|
||||
<li><p><strong>failed_callback</strong> – An optional function or method with the signature <em>failed_callback(request_receipt)</em> to be called when a request fails. See the <a class="reference internal" href="examples.html#example-request"><span class="std std-ref">Request Example</span></a> for more info.</p></li>
|
||||
<li><p><strong>progress_callback</strong> – An optional function or method with the signature <em>progress_callback(request_receipt)</em> to be called when progress is made receiving the response. Progress can be accessed as a float between 0.0 and 1.0 by the <em>request_receipt.progress</em> property.</p></li>
|
||||
<li><p><strong>max_response_size</strong> – An optional maximum accepted response size, in bytes as an integer.</p></li>
|
||||
<li><p><strong>timeout</strong> – An optional timeout in seconds for the request. If <em>None</em> is supplied it will be calculated based on link RTT.</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
@@ -2373,6 +2388,7 @@ will announce it.</p>
|
||||
<li><a class="reference internal" href="#RNS.Destination.set_packet_callback"><code class="docutils literal notranslate"><span class="pre">set_packet_callback()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Destination.set_proof_requested_callback"><code class="docutils literal notranslate"><span class="pre">set_proof_requested_callback()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Destination.set_proof_strategy"><code class="docutils literal notranslate"><span class="pre">set_proof_strategy()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Destination.set_max_request_size"><code class="docutils literal notranslate"><span class="pre">set_max_request_size()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Destination.register_request_handler"><code class="docutils literal notranslate"><span class="pre">register_request_handler()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Destination.deregister_request_handler"><code class="docutils literal notranslate"><span class="pre">deregister_request_handler()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Destination.enable_ratchets"><code class="docutils literal notranslate"><span class="pre">enable_ratchets()</span></code></a></li>
|
||||
@@ -2519,7 +2535,7 @@ will announce it.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,11 +8,11 @@
|
||||
|
||||
<!-- Generated with Sphinx 8.2.3 and Furo 2025.09.25.dev1 -->
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>Search - Reticulum Network Stack 1.3.2 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css?v=d111a655" />
|
||||
<title>Search - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="#" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -304,7 +304,7 @@
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Programs Using Reticulum - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -513,7 +513,7 @@ plugin system for expandability.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Support Reticulum - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -383,7 +383,7 @@ circumstances, so we rely on old-fashioned human feedback.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Understanding Reticulum - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 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">
|
||||
@@ -969,8 +969,11 @@ but excluding any interface access codes.
|
||||
<section id="announce-propagation-rules">
|
||||
<span id="understanding-announcepropagation"></span><h3>Announce Propagation Rules<a class="headerlink" href="#announce-propagation-rules" title="Link to this heading">¶</a></h3>
|
||||
<p>The following table illustrates the rules for automatically propagating announces from one interface type to another, for all possible combinations. For the purpose of announce propagation, the <em>Full</em> and <em>Gateway</em> modes are identical.</p>
|
||||
<img alt="_images/if_mode_graph_b.png" src="_images/if_mode_graph_b.png" />
|
||||
<p>See the <a class="reference internal" href="interfaces.html#interfaces-modes"><span class="std std-ref">Interface Modes</span></a> section for a conceptual overview of the different interface modes, and how they are configured.</p>
|
||||
<img alt="_images/if_mode_graph_b.png" src="_images/if_mode_graph_b.png" />
|
||||
<p>Or, represented in table form:</p>
|
||||
<img alt="_images/if_mode_graph_c.png" src="_images/if_mode_graph_c.png" />
|
||||
<p>For a visually intuitive overview of how announce propagation rules function in practice, you can take a look at the <a class="reference external" href="https://rns.moscow/announce-sim.html">Announce Propagation Simulator</a> hosted by <a class="reference external" href="https://rns.moscow/">rns.moscow</a>.</p>
|
||||
</section>
|
||||
<section id="cryptographic-primitives">
|
||||
<span id="understanding-primitives"></span><h3>Cryptographic Primitives<a class="headerlink" href="#cryptographic-primitives" title="Link to this heading">¶</a></h3>
|
||||
@@ -1116,7 +1119,7 @@ those risks are acceptable to you.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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>
|
||||
|
||||
+17
-16
@@ -7,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Using Reticulum on Your System - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 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">
|
||||
@@ -1107,9 +1107,9 @@ positional arguments:
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config, -c CONFIG path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY
|
||||
path to identity file to use
|
||||
--config, -c PATH path to config directory
|
||||
--rnsconfig, -c PATH path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY path to identity file to use
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
@@ -1119,10 +1119,8 @@ options:
|
||||
does not provide one or when remote command execution
|
||||
is disabled; if no command is specified, the default
|
||||
shell of the user running rnsh will be used
|
||||
-s, --service SERVICE
|
||||
service name for identity file if not the default
|
||||
-b, --announce PERIOD
|
||||
announce on startup and every PERIOD seconds; specify
|
||||
-s, --service SERVICE service name for identity file if not the default
|
||||
-b, --announce PERIOD announce on startup and every PERIOD seconds; specify
|
||||
0 to announce on startup only
|
||||
-a, --allowed HASH allow this identity to connect (may be specified
|
||||
multiple times); allowed identities can also be
|
||||
@@ -1132,13 +1130,11 @@ options:
|
||||
-A, --remote-command-as-args
|
||||
concatenate remote command to the argument list of the
|
||||
default program or shell
|
||||
-C, --no-remote-command
|
||||
disable executing command lines received from the
|
||||
-C, --no-remote-command disable executing command lines received from the
|
||||
remote initiator
|
||||
-N, --no-id disable identity announcement on connect
|
||||
-m, --mirror return with the exit code of the remote process
|
||||
-w, --timeout SECONDS
|
||||
connect and request timeout in seconds
|
||||
-w, --timeout SECONDS connect and request timeout in seconds
|
||||
|
||||
When specifying a command to execute, separate rnsh options from the command
|
||||
and its arguments with --. For example:
|
||||
@@ -1307,6 +1303,11 @@ network_identity = ~/.reticulum/storage/identities/my_network
|
||||
...
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>For more fine-grained control over how discovered interfaces are auto-connected, additional options are provided for configuraiton. These are not necessary to set in most cases, but can be useful in certain situations.</p>
|
||||
<ul class="simple">
|
||||
<li><p>The <code class="docutils literal notranslate"><span class="pre">autoconnect_interface_mode</span></code> options specifies which mode discovered interfaces should be created with when auto-connecting.</p></li>
|
||||
<li><p>The <code class="docutils literal notranslate"><span class="pre">autoconnect_announces_to_internal</span></code> option allows you to specify that auto-connected interfaces should propagate announces to <code class="docutils literal notranslate"><span class="pre">internal</span></code> mode interfaces, even if the auto-connected interface’s mode would normally not allow for this.</p></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section id="remote-management">
|
||||
<h2>Remote Management<a class="headerlink" href="#remote-management" title="Link to this heading">¶</a></h2>
|
||||
@@ -1639,7 +1640,7 @@ systemctl --user enable rnsd.service
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>What is Reticulum? - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -505,7 +505,7 @@ network, and vice versa.</p>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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,12 +7,12 @@
|
||||
<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.3.2 documentation</title>
|
||||
<title>Zen of Reticulum - Reticulum Network Stack 1.4.2 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" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=bb3cebc5" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/custom.css?v=e1715074" />
|
||||
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.3.2 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 1.4.2 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.3.2 documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 1.4.2 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">
|
||||
@@ -677,7 +677,7 @@ Imagine a messaging app. You write it once. It works on a laptop connected to fi
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script src="_static/documentation_options.js?v=8ca9e7a0"></script>
|
||||
</div><script src="_static/documentation_options.js?v=02f2166e"></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>
|
||||
|
||||
@@ -1250,7 +1250,7 @@ Each document is a numbered directory containing:
|
||||
|
||||
**Nomad Network Interface**
|
||||
|
||||
When the Nomad Network page node is enabled, work documents are viewable through the web interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.
|
||||
When the Nomad Network page node is enabled, work documents are viewable through the nomadnet interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.
|
||||
|
||||
### Cryptographic Attribution
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ to participate in the development of Reticulum itself.
|
||||
* [Backbone Interface](interfaces.md#backbone-interface)
|
||||
* [Listeners](interfaces.md#listeners)
|
||||
* [Connecting Remotes](interfaces.md#connecting-remotes)
|
||||
* [Automated Blocking](interfaces.md#automated-blocking)
|
||||
* [TCP Server Interface](interfaces.md#tcp-server-interface)
|
||||
* [TCP Client Interface](interfaces.md#tcp-client-interface)
|
||||
* [UDP Interface](interfaces.md#udp-interface)
|
||||
|
||||
@@ -212,6 +212,51 @@ specify the target Yggdrasil IPv6 address and port, like so:
|
||||
target_port = 4343
|
||||
```
|
||||
|
||||
### Automated Blocking
|
||||
|
||||
Listener instances of `BackboneInterface` will automatically block fast-flapping clients,
|
||||
that repeatedly connect for a short amount of time, and then disconnect. Such behavior usually
|
||||
occurs from clients trying to spam the network with announces or path requests, by connecting, dumping
|
||||
a massive amount of requests, and then disconnecting in an attempt to bypass rate limits.
|
||||
|
||||
While such bypass attempts only have very limited effect on the amount of spam actually dumped (ingress limits
|
||||
trigger immediately once a client exceeds rate limits), the behavior is often seen anyways, and
|
||||
causes log noise and needless interface rotation. The fast-flapping block ensures such clients
|
||||
are never allocated an interface on the transport instance.
|
||||
|
||||
Another common cause can simply be clients using implementations that are broken, or automated
|
||||
scanning tools attempting to connect to the instance.
|
||||
|
||||
```ini
|
||||
[[Backbone Listener]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
listen_on = 0.0.0.0
|
||||
port = 4242
|
||||
|
||||
# Whether to enable blocking
|
||||
block_fast_flapping = yes
|
||||
|
||||
# How long an IP address stays
|
||||
# blocked, in minutes. Set to
|
||||
# 12 hours by default.
|
||||
fast_flapping_block_time = 720
|
||||
|
||||
# The minimum time, in seconds,
|
||||
# an interface must stay connected
|
||||
# to be considered not fast-flapping.
|
||||
fast_flapping_threshold = 20
|
||||
|
||||
# Amount of fast flaps a remote
|
||||
# IP can perform before having
|
||||
# blocking triggered.
|
||||
fast_flapping_grace = 5
|
||||
```
|
||||
|
||||
The configuration options listed in the example above are the *default values*, and you do
|
||||
not need to add them for automated blocking to work, but you can use them to change the
|
||||
behavior from the defaults, if necessary.
|
||||
|
||||
## TCP Server Interface
|
||||
|
||||
The TCP Server interface is suitable for allowing other peers to connect over
|
||||
@@ -939,7 +984,13 @@ If you enable `discovery_encrypt` but do not configure a valid `network_identity
|
||||
**Physical Location**
|
||||
|
||||
`latitude`, `longitude`, `height`
|
||||
: Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters.
|
||||
: Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters above mean sea level.
|
||||
|
||||
`location_cmd`
|
||||
: Optional path to executable or script that returns the physical coordinates for the interface. This can be used instead of manually setting `latitude`, `longitude` and `height`. Reticulum expects the script to output the location data to `stdout` on a single line, separated by commas, with values as floating point numbers in the format `LAT, LON, HEIGHT`. Coordinates should be in decimal degrees, height in meters above mean sea level.
|
||||
|
||||
#### NOTE
|
||||
The height value for interface discovery is specified in *height above mean sea level*! This is the geoid-corrected height value, and is distinct from GPS altitude. If you manually specify height, it will typically be set to what you find on a topographical map. If you are using a script to output the location data, make sure that you are using the geoid-corrected altitude, not height above the GPS ellipsoid. Most GPS systems will make both figures available.
|
||||
|
||||
**Radio Parameters**
|
||||
|
||||
@@ -1080,6 +1131,14 @@ These can be used to control various aspects of interface behaviour.
|
||||
> > be a radio interface serving a wide area, where users are
|
||||
> > expected to connect momentarily, use the network, and then
|
||||
> > disappear again.
|
||||
> > - See the [Interface Modes](#interfaces-modes) section
|
||||
> > below for a reference on additional modes.
|
||||
> * The `gravity` option specifies the pathing affinity of an interface.
|
||||
> If not set specifically, and unless otherwise configured by
|
||||
> `default_gravity` or `autoconnect_interface_gravity`,
|
||||
> it defaults to `0`. Positive values increase pathing affinity,
|
||||
> and negative values decrease it.
|
||||
>
|
||||
> * The `outgoing` option sets whether an interface is allowed
|
||||
> to transmit. Defaults to `True`. If set to `False` or `No`
|
||||
> the interface will only receive data, and never transmit.
|
||||
@@ -1136,6 +1195,24 @@ These can be used to control various aspects of interface behaviour.
|
||||
> link or a remote TCP tunnel) solely to discover better local
|
||||
> infrastructure, which then supersedes the bootstrap interface.
|
||||
>
|
||||
> * The `recursive_prs` option allows you to enable recursive path
|
||||
> discovery on an interface regardless of its configured interface
|
||||
> mode. When this option is enabled, Reticulum will attempt to
|
||||
> recursively discover paths for path requests received on this
|
||||
> interface.
|
||||
>
|
||||
> * The `announces_from_internal` option controls whether any
|
||||
> announces *received on* an `internal` mode interface will
|
||||
> propagate *out* on the interface this option is set on. Note
|
||||
> that this controls *announce propagation*; even if announces
|
||||
> do not propagate out when received, paths to destinations on
|
||||
> `internal` mode interfaces may still be resolvable by means
|
||||
> of path requests.
|
||||
>
|
||||
> * To allow an interface that would *otherwise not* propagate
|
||||
> announces to `internal` mode interfaces to do so, you can
|
||||
> configure the `announces_to_internal` option to `True`.
|
||||
>
|
||||
|
||||
## Interface Modes
|
||||
|
||||
@@ -1211,6 +1288,15 @@ the default mode.
|
||||
> public Transport Node available on the Internet, the interface
|
||||
> connecting over the Internet should be set to `boundary` mode.
|
||||
>
|
||||
> * The `internal` mode designates interfaces that belong to a
|
||||
> network different from any marked as `boundary`. Announces from
|
||||
> a `boundary` interface will not propagate to interfaces set as
|
||||
> `internal`, but announces *will* propagate from `internal` *to*
|
||||
> `boundary`. Devices on the `internal` side of the network will
|
||||
> still be able to resolve paths to destinations across the boundary
|
||||
> when needed, since recursive path requests are enabled for `internal`
|
||||
> mode interfaces by default.
|
||||
>
|
||||
|
||||
For a table describing the impact of all modes on announce propagation,
|
||||
please see the [Announce Propagation Rules](understanding.md#understanding-announcepropagation) section.
|
||||
|
||||
@@ -474,6 +474,16 @@ Sets the destinations proof strategy.
|
||||
* **Parameters:**
|
||||
**proof_strategy** – One of `RNS.Destination.PROVE_NONE`, `RNS.Destination.PROVE_ALL` or `RNS.Destination.PROVE_APP`. If `RNS.Destination.PROVE_APP` is set, the proof_requested_callback will be called to determine whether a proof should be sent or not.
|
||||
|
||||
#### `set_max_request_size(max_request_size)`
|
||||
|
||||
Sets the maximum accepted request size for registered request handlers.
|
||||
|
||||
* **Parameters:**
|
||||
**max_request_size** – The maximum accepted request size in bytes, as an integer.
|
||||
|
||||
* **Raises:**
|
||||
`TypeError` or `ValueError` if any of the argument is invalid.
|
||||
|
||||
#### `register_request_handler(path, response_generator=None, allow=ALLOW_NONE, allowed_list=None, auto_compress=True)`
|
||||
|
||||
Registers a request handler.
|
||||
@@ -757,7 +767,7 @@ thus preserved. This method can be used for authentication.
|
||||
* **Parameters:**
|
||||
**identity** – An RNS.Identity instance to identify as.
|
||||
|
||||
#### `request(path, data=None, response_callback=None, failed_callback=None, progress_callback=None, timeout=None)`
|
||||
#### `request(path, data=None, response_callback=None, failed_callback=None, progress_callback=None, timeout=None, max_response_size=None)`
|
||||
|
||||
Sends a request to the remote peer.
|
||||
|
||||
@@ -766,6 +776,7 @@ Sends a request to the remote peer.
|
||||
* **response_callback** – An optional function or method with the signature *response_callback(request_receipt)* to be called when a response is received. See the [Request Example](examples.md#example-request) for more info.
|
||||
* **failed_callback** – An optional function or method with the signature *failed_callback(request_receipt)* to be called when a request fails. See the [Request Example](examples.md#example-request) for more info.
|
||||
* **progress_callback** – An optional function or method with the signature *progress_callback(request_receipt)* to be called when progress is made receiving the response. Progress can be accessed as a float between 0.0 and 1.0 by the *request_receipt.progress* property.
|
||||
* **max_response_size** – An optional maximum accepted response size, in bytes as an integer.
|
||||
* **timeout** – An optional timeout in seconds for the request. If *None* is supplied it will be calculated based on link RTT.
|
||||
|
||||
* **Returns:**
|
||||
|
||||
@@ -632,30 +632,57 @@ but excluding any interface access codes.
|
||||
|
||||
The following table illustrates the rules for automatically propagating announces from one interface type to another, for all possible combinations. For the purpose of announce propagation, the *Full* and *Gateway* modes are identical.
|
||||
|
||||

|
||||
|
||||
See the [Interface Modes](interfaces.md#interfaces-modes) section for a conceptual overview of the different interface modes, and how they are configured.
|
||||
|
||||

|
||||
|
||||
Or, represented in table form:
|
||||
|
||||

|
||||
<!-- (.. code-block:: text)
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼───> Full >───┼── ✕ ── AP
|
||||
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✓ ──┼───> Full >───┼── ✓ ── Boundary
|
||||
Internal ── ✓ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✓ ──┘ └── ✓ ── Roaming
|
||||
|
||||
Full ────── ✕ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✕ ──┼────> AP >────┼── ✕ ── AP
|
||||
Boundary ── ✕ ──┤ ├── ✓ ── Boundary
|
||||
AP ──────── ✕ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✕ ──┼────> AP >────┼── ✓ ── Boundary
|
||||
Internal ── ✕ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✕ ──┘ └── ✓ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼─> Roaming >──┼── ✕ ── AP
|
||||
Boundary ── ✕ ──┤ ├── ✕ ── Boundary
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✕ ──┼─> Roaming >──┼── ✕ ── Boundary
|
||||
Internal ── ✕ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼─> Boundary >─┼── ✕ ── AP
|
||||
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
|
||||
Roaming ─── ✕ ──┘ └── ✕ ── Roaming -->
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✓ ──┼─> Boundary >─┼── ✓ ── Boundary
|
||||
Internal ── ✓ ──┤ ├── ✕ ── Internal
|
||||
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✕ ──┼─> Internal >─┼── ✓ ── Boundary
|
||||
Internal ── ✓ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✓ ──┘ └── ✕ ── Roaming
|
||||
|
||||
dest → Full AP Boundary Roaming Gateway Internal
|
||||
source ↓
|
||||
Full ✓ ✕ ✓ ✓ ✓ ✓
|
||||
Gateway ✓ ✕ ✓ ✓ ✓ ✓
|
||||
AP ✓ ✕ ✓ ✓ ✓ ✓
|
||||
Boundary ✓ ✕ ✓ ✕ ✓ ✕
|
||||
Internal ✓ ✕ ✓ ✕ ✓ ✓
|
||||
Roaming ✓ ✕ ✕ ✕ ✓ ✓
|
||||
|
||||
✓ Will rebroadcast announce
|
||||
✕ Will not rebroadcast announce -->
|
||||
|
||||
For a visually intuitive overview of how announce propagation rules function in practice, you can take a look at the [Announce Propagation Simulator](https://rns.moscow/announce-sim.html) hosted by [rns.moscow](https://rns.moscow/).
|
||||
|
||||
### Cryptographic Primitives
|
||||
|
||||
|
||||
+12
-11
@@ -966,9 +966,9 @@ positional arguments:
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config, -c CONFIG path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY
|
||||
path to identity file to use
|
||||
--config, -c PATH path to config directory
|
||||
--rnsconfig, -c PATH path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY path to identity file to use
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
@@ -978,10 +978,8 @@ options:
|
||||
does not provide one or when remote command execution
|
||||
is disabled; if no command is specified, the default
|
||||
shell of the user running rnsh will be used
|
||||
-s, --service SERVICE
|
||||
service name for identity file if not the default
|
||||
-b, --announce PERIOD
|
||||
announce on startup and every PERIOD seconds; specify
|
||||
-s, --service SERVICE service name for identity file if not the default
|
||||
-b, --announce PERIOD announce on startup and every PERIOD seconds; specify
|
||||
0 to announce on startup only
|
||||
-a, --allowed HASH allow this identity to connect (may be specified
|
||||
multiple times); allowed identities can also be
|
||||
@@ -991,13 +989,11 @@ options:
|
||||
-A, --remote-command-as-args
|
||||
concatenate remote command to the argument list of the
|
||||
default program or shell
|
||||
-C, --no-remote-command
|
||||
disable executing command lines received from the
|
||||
-C, --no-remote-command disable executing command lines received from the
|
||||
remote initiator
|
||||
-N, --no-id disable identity announcement on connect
|
||||
-m, --mirror return with the exit code of the remote process
|
||||
-w, --timeout SECONDS
|
||||
connect and request timeout in seconds
|
||||
-w, --timeout SECONDS connect and request timeout in seconds
|
||||
|
||||
When specifying a command to execute, separate rnsh options from the command
|
||||
and its arguments with --. For example:
|
||||
@@ -1179,6 +1175,11 @@ network_identity = ~/.reticulum/storage/identities/my_network
|
||||
...
|
||||
```
|
||||
|
||||
For more fine-grained control over how discovered interfaces are auto-connected, additional options are provided for configuraiton. These are not necessary to set in most cases, but can be useful in certain situations.
|
||||
|
||||
* The `autoconnect_interface_mode` options specifies which mode discovered interfaces should be created with when auto-connecting.
|
||||
* The `autoconnect_announces_to_internal` option allows you to specify that auto-connected interfaces should propagate announces to `internal` mode interfaces, even if the auto-connected interface’s mode would normally not allow for this.
|
||||
|
||||
## Remote Management
|
||||
|
||||
It is possible to allow remote management of Reticulum
|
||||
|
||||
@@ -14,6 +14,10 @@ img[src*="if_mode_graph_b.png"] {
|
||||
background-color: rgb(169, 177, 186);
|
||||
}
|
||||
|
||||
img[src*="if_mode_graph_c.png"] {
|
||||
background-color: rgb(169, 177, 186);
|
||||
}
|
||||
|
||||
dt.sig {
|
||||
margin-bottom: 0.75rem;
|
||||
margin-top: 1.75rem;
|
||||
|
||||
+20
-1
@@ -1326,7 +1326,7 @@ Each document is a numbered directory containing:
|
||||
|
||||
**Nomad Network Interface**
|
||||
|
||||
When the Nomad Network page node is enabled, work documents are viewable through the web interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.
|
||||
When the Nomad Network page node is enabled, work documents are viewable through the nomadnet interface. The work page lists all documents with their status, and clicking a document shows its full content and updates.
|
||||
|
||||
Cryptographic Attribution
|
||||
-------------------------
|
||||
@@ -1491,3 +1491,22 @@ To view signature information for commits, use Git's standard ``--show-signature
|
||||
Refactored module
|
||||
|
||||
The output shows whether the commit signature is valid, and whether the author field matches the signing identity.
|
||||
|
||||
.. tip::
|
||||
If you want to display both the identity hash and LXMF address for authors, you can generate a ``.mailmap`` file that resolves identities to LXMF addresses with the following script:
|
||||
|
||||
.. code::
|
||||
#!/bin/bash
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
cd $DIR
|
||||
|
||||
id_regex="<([0-9a-f]{32})( .*)*>"
|
||||
extract_id="s/.*$id_regex/\1/g"
|
||||
|
||||
rm -f .mailmap
|
||||
git shortlog -se | grep -Ee "$id_regex" | sed -r "$extract_id" | while read -r id ; do
|
||||
if lxmf=$(rnid -i $id -H lxmf.delivery | grep -Ee "destination for this Identity is" | sed -r "$extract_id"); then
|
||||
echo "<$id lxmf:$lxmf> <$id>" >> .mailmap
|
||||
fi
|
||||
done
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 35 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
@@ -230,6 +230,51 @@ specify the target Yggdrasil IPv6 address and port, like so:
|
||||
target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5
|
||||
target_port = 4343
|
||||
|
||||
Automated Blocking
|
||||
------------------
|
||||
Listener instances of ``BackboneInterface`` will automatically block fast-flapping clients,
|
||||
that repeatedly connect for a short amount of time, and then disconnect. Such behavior usually
|
||||
occurs from clients trying to spam the network with announces or path requests, by connecting, dumping
|
||||
a massive amount of requests, and then disconnecting in an attempt to bypass rate limits.
|
||||
|
||||
While such bypass attempts only have very limited effect on the amount of spam actually dumped (ingress limits
|
||||
trigger immediately once a client exceeds rate limits), the behavior is often seen anyways, and
|
||||
causes log noise and needless interface rotation. The fast-flapping block ensures such clients
|
||||
are never allocated an interface on the transport instance.
|
||||
|
||||
Another common cause can simply be clients using implementations that are broken, or automated
|
||||
scanning tools attempting to connect to the instance.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[Backbone Listener]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
listen_on = 0.0.0.0
|
||||
port = 4242
|
||||
|
||||
# Whether to enable blocking
|
||||
block_fast_flapping = yes
|
||||
|
||||
# How long an IP address stays
|
||||
# blocked, in minutes. Set to
|
||||
# 12 hours by default.
|
||||
fast_flapping_block_time = 720
|
||||
|
||||
# The minimum time, in seconds,
|
||||
# an interface must stay connected
|
||||
# to be considered not fast-flapping.
|
||||
fast_flapping_threshold = 20
|
||||
|
||||
# Amount of fast flaps a remote
|
||||
# IP can perform before having
|
||||
# blocking triggered.
|
||||
fast_flapping_grace = 5
|
||||
|
||||
The configuration options listed in the example above are the *default values*, and you do
|
||||
not need to add them for automated blocking to work, but you can use them to change the
|
||||
behavior from the defaults, if necessary.
|
||||
|
||||
.. _interfaces-tcps:
|
||||
|
||||
TCP Server Interface
|
||||
@@ -998,7 +1043,13 @@ On a real system, you should make the script robust enough to deal with intermit
|
||||
**Physical Location**
|
||||
|
||||
``latitude``, ``longitude``, ``height``
|
||||
Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters.
|
||||
Optional physical coordinates for the interface. These are useful for mapping discovered interfaces geographically or for clients to automatically select the nearest access point. Coordinates should be in decimal degrees, height in meters above mean sea level.
|
||||
|
||||
``location_cmd``
|
||||
Optional path to executable or script that returns the physical coordinates for the interface. This can be used instead of manually setting ``latitude``, ``longitude`` and ``height``. Reticulum expects the script to output the location data to ``stdout`` on a single line, separated by commas, with values as floating point numbers in the format ``LAT, LON, HEIGHT``. Coordinates should be in decimal degrees, height in meters above mean sea level.
|
||||
|
||||
.. note::
|
||||
The height value for interface discovery is specified in *height above mean sea level*! This is the geoid-corrected height value, and is distinct from GPS altitude. If you manually specify height, it will typically be set to what you find on a topographical map. If you are using a script to output the location data, make sure that you are using the geoid-corrected altitude, not height above the GPS ellipsoid. Most GPS systems will make both figures available.
|
||||
|
||||
**Radio Parameters**
|
||||
|
||||
@@ -1149,6 +1200,15 @@ These can be used to control various aspects of interface behaviour.
|
||||
expected to connect momentarily, use the network, and then
|
||||
disappear again.
|
||||
|
||||
- See the :ref:`Interface Modes<interfaces-modes>` section
|
||||
below for a reference on additional modes.
|
||||
|
||||
* | The ``gravity`` option specifies the pathing affinity of an interface.
|
||||
If not set specifically, and unless otherwise configured by
|
||||
``default_gravity`` or ``autoconnect_interface_gravity``,
|
||||
it defaults to ``0``. Positive values increase pathing affinity,
|
||||
and negative values decrease it.
|
||||
|
||||
* | The ``outgoing`` option sets whether an interface is allowed
|
||||
to transmit. Defaults to ``True``. If set to ``False`` or ``No``
|
||||
the interface will only receive data, and never transmit.
|
||||
@@ -1207,6 +1267,27 @@ These can be used to control various aspects of interface behaviour.
|
||||
link or a remote TCP tunnel) solely to discover better local
|
||||
infrastructure, which then supersedes the bootstrap interface.
|
||||
|
||||
|
||||
* | The ``recursive_prs`` option allows you to enable recursive path
|
||||
discovery on an interface regardless of its configured interface
|
||||
mode. When this option is enabled, Reticulum will attempt to
|
||||
recursively discover paths for path requests received on this
|
||||
interface.
|
||||
|
||||
|
||||
* | The ``announces_from_internal`` option controls whether any
|
||||
announces *received on* an ``internal`` mode interface will
|
||||
propagate *out* on the interface this option is set on. Note
|
||||
that this controls *announce propagation*; even if announces
|
||||
do not propagate out when received, paths to destinations on
|
||||
``internal`` mode interfaces may still be resolvable by means
|
||||
of path requests.
|
||||
|
||||
|
||||
* | To allow an interface that would *otherwise not* propagate
|
||||
announces to ``internal`` mode interfaces to do so, you can
|
||||
configure the ``announces_to_internal`` option to ``True``.
|
||||
|
||||
.. _interfaces-modes:
|
||||
|
||||
Interface Modes
|
||||
@@ -1284,6 +1365,15 @@ the default mode.
|
||||
public Transport Node available on the Internet, the interface
|
||||
connecting over the Internet should be set to ``boundary`` mode.
|
||||
|
||||
* | The ``internal`` mode designates interfaces that belong to a
|
||||
network different from any marked as ``boundary``. Announces from
|
||||
a ``boundary`` interface will not propagate to interfaces set as
|
||||
``internal``, but announces *will* propagate from ``internal`` *to*
|
||||
``boundary``. Devices on the ``internal`` side of the network will
|
||||
still be able to resolve paths to destinations across the boundary
|
||||
when needed, since recursive path requests are enabled for ``internal``
|
||||
mode interfaces by default.
|
||||
|
||||
For a table describing the impact of all modes on announce propagation,
|
||||
please see the :ref:`Announce Propagation Rules<understanding-announcepropagation>` section.
|
||||
|
||||
|
||||
@@ -707,32 +707,60 @@ Announce Propagation Rules
|
||||
|
||||
The following table illustrates the rules for automatically propagating announces from one interface type to another, for all possible combinations. For the purpose of announce propagation, the *Full* and *Gateway* modes are identical.
|
||||
|
||||
See the :ref:`Interface Modes<interfaces-modes>` section for a conceptual overview of the different interface modes, and how they are configured.
|
||||
|
||||
.. image:: graphics/if_mode_graph_b.png
|
||||
|
||||
See the :ref:`Interface Modes<interfaces-modes>` section for a conceptual overview of the different interface modes, and how they are configured.
|
||||
Or, represented in table form:
|
||||
|
||||
.. image:: graphics/if_mode_graph_c.png
|
||||
|
||||
..
|
||||
(.. code-block:: text)
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼───> Full >───┼── ✕ ── AP
|
||||
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✓ ──┼───> Full >───┼── ✓ ── Boundary
|
||||
Internal ── ✓ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✓ ──┘ └── ✓ ── Roaming
|
||||
|
||||
Full ────── ✕ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✕ ──┼────> AP >────┼── ✕ ── AP
|
||||
Boundary ── ✕ ──┤ ├── ✓ ── Boundary
|
||||
AP ──────── ✕ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✕ ──┼────> AP >────┼── ✓ ── Boundary
|
||||
Internal ── ✕ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✕ ──┘ └── ✓ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼─> Roaming >──┼── ✕ ── AP
|
||||
Boundary ── ✕ ──┤ ├── ✕ ── Boundary
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✕ ──┼─> Roaming >──┼── ✕ ── Boundary
|
||||
Internal ── ✕ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┼─> Boundary >─┼── ✕ ── AP
|
||||
Boundary ── ✓ ──┤ ├── ✓ ── Boundary
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✓ ──┼─> Boundary >─┼── ✓ ── Boundary
|
||||
Internal ── ✓ ──┤ ├── ✕ ── Internal
|
||||
Roaming ─── ✕ ──┘ └── ✕ ── Roaming
|
||||
|
||||
Full ────── ✓ ──┐ ┌── ✓ ── Full
|
||||
AP ──────── ✓ ──┤ ├── ✕ ── AP
|
||||
Boundary ── ✕ ──┼─> Internal >─┼── ✓ ── Boundary
|
||||
Internal ── ✓ ──┤ ├── ✓ ── Internal
|
||||
Roaming ─── ✓ ──┘ └── ✕ ── Roaming
|
||||
|
||||
dest → Full AP Boundary Roaming Gateway Internal
|
||||
source ↓
|
||||
Full ✓ ✕ ✓ ✓ ✓ ✓
|
||||
Gateway ✓ ✕ ✓ ✓ ✓ ✓
|
||||
AP ✓ ✕ ✓ ✓ ✓ ✓
|
||||
Boundary ✓ ✕ ✓ ✕ ✓ ✕
|
||||
Internal ✓ ✕ ✓ ✕ ✓ ✓
|
||||
Roaming ✓ ✕ ✕ ✕ ✓ ✓
|
||||
|
||||
✓ Will rebroadcast announce
|
||||
✕ Will not rebroadcast announce
|
||||
|
||||
For a visually intuitive overview of how announce propagation rules function in practice, you can take a look at the `Announce Propagation Simulator <https://rns.moscow/announce-sim.html>`_ hosted by `rns.moscow <https://rns.moscow/>`_.
|
||||
|
||||
|
||||
.. _understanding-primitives:
|
||||
|
||||
|
||||
+13
-11
@@ -1003,9 +1003,9 @@ available. These are only recognised immediately after a newline character:
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config, -c CONFIG path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY
|
||||
path to identity file to use
|
||||
--config, -c PATH path to config directory
|
||||
--rnsconfig, -c PATH path to alternative Reticulum config directory
|
||||
--identity, -i IDENTITY path to identity file to use
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
@@ -1015,10 +1015,8 @@ available. These are only recognised immediately after a newline character:
|
||||
does not provide one or when remote command execution
|
||||
is disabled; if no command is specified, the default
|
||||
shell of the user running rnsh will be used
|
||||
-s, --service SERVICE
|
||||
service name for identity file if not the default
|
||||
-b, --announce PERIOD
|
||||
announce on startup and every PERIOD seconds; specify
|
||||
-s, --service SERVICE service name for identity file if not the default
|
||||
-b, --announce PERIOD announce on startup and every PERIOD seconds; specify
|
||||
0 to announce on startup only
|
||||
-a, --allowed HASH allow this identity to connect (may be specified
|
||||
multiple times); allowed identities can also be
|
||||
@@ -1028,13 +1026,11 @@ available. These are only recognised immediately after a newline character:
|
||||
-A, --remote-command-as-args
|
||||
concatenate remote command to the argument list of the
|
||||
default program or shell
|
||||
-C, --no-remote-command
|
||||
disable executing command lines received from the
|
||||
-C, --no-remote-command disable executing command lines received from the
|
||||
remote initiator
|
||||
-N, --no-id disable identity announcement on connect
|
||||
-m, --mirror return with the exit code of the remote process
|
||||
-w, --timeout SECONDS
|
||||
connect and request timeout in seconds
|
||||
-w, --timeout SECONDS connect and request timeout in seconds
|
||||
|
||||
When specifying a command to execute, separate rnsh options from the command
|
||||
and its arguments with --. For example:
|
||||
@@ -1224,6 +1220,12 @@ The configuration snippet below contains an example of setting these additional
|
||||
network_identity = ~/.reticulum/storage/identities/my_network
|
||||
...
|
||||
|
||||
For more fine-grained control over how discovered interfaces are auto-connected, additional options are provided for configuraiton. These are not necessary to set in most cases, but can be useful in certain situations.
|
||||
|
||||
* The ``autoconnect_interface_mode`` options specifies which mode discovered interfaces should be created with when auto-connecting.
|
||||
|
||||
* The ``autoconnect_announces_to_internal`` option allows you to specify that auto-connected interfaces should propagate announces to ``internal`` mode interfaces, even if the auto-connected interface's mode would normally not allow for this.
|
||||
|
||||
Remote Management
|
||||
-----------------
|
||||
|
||||
|
||||
Binary file not shown.
+3
-3
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
import threading
|
||||
import RNS
|
||||
from RNS.Channel import MessageState, ChannelOutletBase, Channel, MessageBase
|
||||
from RNS.Channel import MessageState, LinkChannelOutlet, Channel, MessageBase
|
||||
import RNS.Buffer
|
||||
from RNS.vendor import umsgpack
|
||||
from typing import Callable
|
||||
@@ -71,7 +71,7 @@ class Packet:
|
||||
self.delivered_callback(self)
|
||||
|
||||
|
||||
class ChannelOutletTest(ChannelOutletBase):
|
||||
class ChannelOutletTest(LinkChannelOutlet):
|
||||
def get_packet_state(self, packet: Packet) -> MessageState:
|
||||
return packet.state
|
||||
|
||||
@@ -93,7 +93,7 @@ class ChannelOutletTest(ChannelOutletBase):
|
||||
self._usable = True
|
||||
self.packets = []
|
||||
self.lock = threading.RLock()
|
||||
self.packet_callback: Callable[[ChannelOutletBase, bytes], None] | None = None
|
||||
self.packet_callback: Callable[[LinkChannelOutlet, bytes], None] | None = None
|
||||
|
||||
def send(self, raw: bytes) -> Packet:
|
||||
with self.lock:
|
||||
|
||||
Reference in New Issue
Block a user