mirror of
https://github.com/markqvist/Reticulum.git
synced 2026-06-24 04:44:31 -07:00
Compare commits
387 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4eb5dbc633 | |||
| a1e6ce2357 | |||
| 16e833ddb7 | |||
| 4af35bd7ea | |||
| 7d305527e9 | |||
| 1d84dc94a0 | |||
| f825ba38a0 | |||
| f076c2d143 | |||
| 58a20fffb5 | |||
| 15a123875f | |||
| 7cadb3af8b | |||
| 01984a33eb | |||
| 7329817d95 | |||
| ad4af7dd50 | |||
| f2a778ffa4 | |||
| 1a77b5752c | |||
| 2b3d6a0989 | |||
| 0b508a04b8 | |||
| 13aebeecf9 | |||
| 47d3c640d6 | |||
| 19f27598d9 | |||
| f2ef22e1a0 | |||
| 251e1b8a35 | |||
| 5de4e24a9f | |||
| 5e4d32c4c0 | |||
| e1327842b1 | |||
| c13412369a | |||
| 18e4e66db8 | |||
| 5392d635dd | |||
| e56e80aade | |||
| 994c4fd699 | |||
| ef64fefa96 | |||
| 344ff21c1e | |||
| d34e06cb8c | |||
| 8f65a0320b | |||
| b42e1c93da | |||
| e0ca14eb21 | |||
| 48fe97291b | |||
| f400fd7b60 | |||
| fd1d464f06 | |||
| 28afdb36fe | |||
| 6c7db096fc | |||
| 5a7fcb0ec3 | |||
| d647da7a4a | |||
| d7df390bb4 | |||
| 9d36ff48dd | |||
| 8743388263 | |||
| 58486654d5 | |||
| 326d719a49 | |||
| c9b6dc007a | |||
| 1bcac5e234 | |||
| dad58e14e2 | |||
| db85939322 | |||
| 4f4eb1fce5 | |||
| e55000ee1a | |||
| 9c2bf9fba8 | |||
| 563784573b | |||
| e2903f18da | |||
| 2f47456668 | |||
| 79b3101fe0 | |||
| 9788675934 | |||
| 10c63fcaa2 | |||
| 707c012318 | |||
| 3f30e17eb4 | |||
| 9eff138c3c | |||
| b0fb5d1898 | |||
| d542da38b2 | |||
| c8b446ecaf | |||
| 6ed6af5b98 | |||
| 12d39916b9 | |||
| 12d4de0619 | |||
| 7ab87f688a | |||
| 9024a277ac | |||
| fc00d9a5aa | |||
| 106a773f22 | |||
| 93d9cb3b69 | |||
| 99504b7f7d | |||
| 72c1995551 | |||
| 3d8c6c3839 | |||
| 0a06ffd074 | |||
| 12abb544bf | |||
| 78fe132cc2 | |||
| b516d7f092 | |||
| 0961df316f | |||
| 8ad2986877 | |||
| 6214487fb3 | |||
| 2219a5454c | |||
| 712a5d1b06 | |||
| cbc3b800fb | |||
| e7348d0812 | |||
| 59e638402c | |||
| bcd6de015d | |||
| b798c84160 | |||
| 708f666787 | |||
| 4f03302ae2 | |||
| d8f6ab206b | |||
| 472e69fe9a | |||
| aeed5279f8 | |||
| f3b8965fa6 | |||
| 1bbaab1db9 | |||
| bf2fcbba37 | |||
| a63dd67a07 | |||
| b27f9836ae | |||
| 9504c5b863 | |||
| 9767b3453e | |||
| 643fbbbc84 | |||
| 2a5bcd5f52 | |||
| 237c3160eb | |||
| fcdcf1a2a8 | |||
| 7c99aca1d0 | |||
| 309f1999e7 | |||
| fa6de7ff79 | |||
| 47dfcab170 | |||
| 8abd19800f | |||
| b2d6ed733d | |||
| 1179757893 | |||
| d328ef5ce0 | |||
| f577d3018f | |||
| e6db629915 | |||
| acaab30b91 | |||
| 76cedeed07 | |||
| 5beea74eb3 | |||
| 1f91a8f6f2 | |||
| 080216bd55 | |||
| aa37172293 | |||
| 5836d7f8ba | |||
| a699d7c110 | |||
| 8eedbb9d91 | |||
| 2afa85db60 | |||
| a5672e7afe | |||
| 77d40215c8 | |||
| 0896df05b6 | |||
| cf0b1c6237 | |||
| 08b129c8e0 | |||
| e2ea397715 | |||
| 25a73e6ef9 | |||
| f1c4bba3f2 | |||
| 704019ded8 | |||
| 79f5b92bae | |||
| 5aaa743ef8 | |||
| 9721c0bf85 | |||
| 56848cdb63 | |||
| 41ad089ff7 | |||
| 2df355d7b4 | |||
| 39a63b0643 | |||
| ddf14e5636 | |||
| 7138749307 | |||
| af7697f223 | |||
| 0bcb4b8573 | |||
| 6d47b59b1e | |||
| 3d8eaffe9a | |||
| d8039aca17 | |||
| 4e4d379486 | |||
| 87faffa785 | |||
| adef3f80f0 | |||
| 319c798f78 | |||
| 8579a7f2a5 | |||
| ffbbba7395 | |||
| e66745c9ef | |||
| 45fc9338a7 | |||
| f2969bd1b0 | |||
| e0f1f3f947 | |||
| e3827f2e25 | |||
| fad1d4972c | |||
| 2c33ce6c98 | |||
| c0d7f42f17 | |||
| d5a8e4b056 | |||
| 76dd50a060 | |||
| 6f9a9a7ad9 | |||
| eaec9a493b | |||
| d3c8555b39 | |||
| 446f5c0989 | |||
| f3b72a8a3c | |||
| d2c5a1f34b | |||
| 182b49cc04 | |||
| cc8bd34cd4 | |||
| 957ece7394 | |||
| 762343adf9 | |||
| 8d32b378d9 | |||
| 41e816d299 | |||
| 4226a62f23 | |||
| 5dda28559b | |||
| d055ca50d6 | |||
| 799bcfc7aa | |||
| 045cb662ef | |||
| 51e3983bf8 | |||
| 95fdc41845 | |||
| d795fbeaf3 | |||
| 13037d68ed | |||
| 6da5df9f21 | |||
| 8128f573ef | |||
| accf104553 | |||
| 5387264dcb | |||
| 308a6906db | |||
| 96ce7e3f47 | |||
| f186b6266b | |||
| 756029e5af | |||
| c1673f39b6 | |||
| 30a08c4192 | |||
| d680f4d411 | |||
| 29a52e19cf | |||
| 11511168dc | |||
| d4ea698236 | |||
| 11e06b477e | |||
| 4e4c68071f | |||
| 5f502746a4 | |||
| 17bbb9c0b4 | |||
| 8b13d6e08b | |||
| efa512be32 | |||
| 594f5fba1e | |||
| 2912fb2184 | |||
| 02496f39f7 | |||
| 4e31f113c6 | |||
| 9aded3e1da | |||
| 3337d18e9a | |||
| 2cb6d019f9 | |||
| 3dc260a300 | |||
| 4d7f5b8ca6 | |||
| 48be5f65d8 | |||
| b5d854a55c | |||
| 552663c625 | |||
| e6f0b92464 | |||
| 08a6820aa0 | |||
| cc1faa55be | |||
| 840966f3e6 | |||
| 763078a1ae | |||
| 5fb6abd019 | |||
| 7065856229 | |||
| 668ef9253a | |||
| 6f333b8234 | |||
| 32c839f497 | |||
| cbdef1d538 | |||
| c398b34dd8 | |||
| 9a1884cfec | |||
| 378dc1e931 | |||
| be821b6927 | |||
| af46e98865 | |||
| 65b1667ae7 | |||
| 5bc1fc2bde | |||
| 4ae0f28aa0 | |||
| 62ecc0549d | |||
| cbf4c71a73 | |||
| 1d27fae370 | |||
| 05b9a80a07 | |||
| 38241452d3 | |||
| 40e040807a | |||
| 437da99d63 | |||
| 3cbcbec942 | |||
| bc7a8cd09f | |||
| ab0ac46d5a | |||
| d7791c60e2 | |||
| 5dc8cdc6dc | |||
| cdc33a25c5 | |||
| 2b6766f68a | |||
| e871bbdc07 | |||
| 6a98158ba6 | |||
| ef8d44c257 | |||
| 6a48a4d1c0 | |||
| 4d2ba28934 | |||
| 98d4f1c69e | |||
| a0f0d73204 | |||
| 1dbb1a6a35 | |||
| cc50ca82b8 | |||
| 373790c890 | |||
| ef30d21b58 | |||
| c4cafed6aa | |||
| 828eec5e0d | |||
| a8c50fe7d4 | |||
| ab9fc7b370 | |||
| 0dc972f7c9 | |||
| 796cffe29d | |||
| a0f6c99fb5 | |||
| eff0c91ed0 | |||
| dba6cd8393 | |||
| e7daceec82 | |||
| a65473f6ab | |||
| 1851fda9e0 | |||
| 80eec131f8 | |||
| bfe5b876de | |||
| da8a0ee5e9 | |||
| 3269384439 | |||
| 9a766eac8c | |||
| 9d2456500a | |||
| df85beac3e | |||
| 3dd020cb86 | |||
| 67da6be040 | |||
| d2efd6c3e4 | |||
| ea4a525db6 | |||
| c83043b087 | |||
| c07e968218 | |||
| a6eeac14d2 | |||
| a65bc3bc7b | |||
| 8e4b0b3b16 | |||
| d34cefe31d | |||
| 3a68a3fc02 | |||
| a4b6a64611 | |||
| 4f189f5319 | |||
| cb69085280 | |||
| f4d13986af | |||
| 6125c835f7 | |||
| 3049049d5b | |||
| 628c4984a3 | |||
| b58cb3c0ed | |||
| b267687c7f | |||
| 581b16f87c | |||
| f9d42082a2 | |||
| f8925eaed1 | |||
| f4c1ece10a | |||
| d13b034cab | |||
| 008afd88d1 | |||
| 68ca903db4 | |||
| 8f4b4fa82d | |||
| 768f562437 | |||
| 9f0a4bfe69 | |||
| 13b4291840 | |||
| 6dc33126a5 | |||
| fa31dced22 | |||
| 194f6aef1d | |||
| a12b630a4e | |||
| c3ff73591a | |||
| 1967811d68 | |||
| 0e24a0d8bb | |||
| 5913f61e7d | |||
| 9a7e517c73 | |||
| 99af71de75 | |||
| 06848b6731 | |||
| 4ece3a6140 | |||
| ae92432878 | |||
| a4468da9b1 | |||
| 187931a0ea | |||
| d3533e17e8 | |||
| b0944429db | |||
| 7170573da7 | |||
| 4cd94c776a | |||
| 3483de1fc2 | |||
| df3c2cffb3 | |||
| f0e3bc0c14 | |||
| b4d1d54ccb | |||
| de3438248f | |||
| 456eea9c13 | |||
| 3cdebb6e8a | |||
| e0a9dad114 | |||
| b1aa355d5b | |||
| 129591392f | |||
| e51f0f14d9 | |||
| 2c520bb936 | |||
| d3bccb2b4e | |||
| e28f44cfe5 | |||
| 45e5c85868 | |||
| c5bc92e4ea | |||
| ebb8a35129 | |||
| f2046b2453 | |||
| f7351a3eb5 | |||
| 28d55279d8 | |||
| 8104db4fcc | |||
| b8658cd47c | |||
| ecaa8d53e0 | |||
| ca1ec1acef | |||
| 13283cb8e2 | |||
| 5a42adb05b | |||
| 98afe98870 | |||
| f5420d3be3 | |||
| 50b5ab80c4 | |||
| e6371d74b5 | |||
| 0ab38faeac | |||
| b0444104cc | |||
| 4757d6ee87 | |||
| 1780965ef8 | |||
| aaa88e9b7d | |||
| 17ce91a4a2 | |||
| 08751a762a | |||
| 77c0beecf2 | |||
| 28bcf6a8ac | |||
| 61004b4dfb | |||
| e5c22b8a3f | |||
| 001d0f30aa | |||
| fbe4bb03d1 | |||
| 3469b6beb8 | |||
| c696efe0bc | |||
| d0ca61f373 | |||
| 350687eda9 | |||
| d898641e6a | |||
| db576d73bb | |||
| 5fcdd17665 | |||
| e8f2bd9b0c | |||
| 3002023a70 | |||
| 95cea24527 |
@@ -12,6 +12,7 @@ Before creating a bug report on this issue tracker, you **must** read the [Contr
|
||||
|
||||
- The issue tracker is used by developers of this project. **Do not use it to ask general questions, or for support requests**.
|
||||
- Ideas and feature requests can be made on the [Discussions](https://github.com/markqvist/Reticulum/discussions). **Only** feature requests accepted by maintainers and developers are tracked and included on the issue tracker. **Do not post feature requests here**.
|
||||
- Do not submit code written using large language models (LLMs) or other generative 'AI' programs (see the [Generative AI Policy](/Contributing.md#generative-ai-policy) for details).
|
||||
- After reading the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md), **delete this section only** (*"Read the Contribution Guidelines"*) from your bug report, **and fill in all the other sections**.
|
||||
|
||||
**Describe the Bug**
|
||||
|
||||
@@ -28,8 +28,10 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: make test
|
||||
python-version: 3.11
|
||||
- run: |
|
||||
python -m pip install -q cryptography
|
||||
make test
|
||||
|
||||
package:
|
||||
needs: test
|
||||
@@ -41,7 +43,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
python-version: 3.11
|
||||
- run: |
|
||||
python -m pip install -q build wheel setuptools
|
||||
make remove_symlinks
|
||||
@@ -91,6 +93,6 @@ jobs:
|
||||
# .artifacts/documentation/latex/reticulumnetworkstack.pdf
|
||||
# .artifacts/documentation/epub/ReticulumNetworkStack.epub
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
prerelease: ${{ contains(github.ref, '-') }}
|
||||
fail_on_unmatched_files: true
|
||||
|
||||
@@ -13,3 +13,4 @@ tests/rnsconfig/storage
|
||||
tests/rnsconfig/logfile*
|
||||
*.data
|
||||
*.result
|
||||
.buildinfo.bak
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
compiling = False
|
||||
noticed = False
|
||||
notice_delay = 0.3
|
||||
import time
|
||||
import sys
|
||||
import threading
|
||||
from importlib.util import find_spec
|
||||
if find_spec("pyximport") and find_spec("cython"):
|
||||
import pyximport; pyxloader = pyximport.install(pyimport=True, language_level=3)[1]
|
||||
|
||||
def notice_job():
|
||||
global noticed
|
||||
started = time.time()
|
||||
while compiling:
|
||||
if time.time() > started+notice_delay and compiling:
|
||||
noticed = True
|
||||
print("Compiling RNS object code... ", end="")
|
||||
sys.stdout.flush()
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
compiling = True
|
||||
threading.Thread(target=notice_job, daemon=True).start()
|
||||
import RNS; compiling = False
|
||||
if noticed: print("Done."); sys.stdout.flush()
|
||||
+227
@@ -1,3 +1,230 @@
|
||||
### 2026-01-04: RNS 1.1.0
|
||||
|
||||
Enjoy.
|
||||
|
||||
**Changes**
|
||||
- Added on-network global interface discovery. Hello world.
|
||||
- Added discovered interface auto-connection. Robotic.
|
||||
- Added external IP resolution for discovery-enabled interfaces. Snip-snip.
|
||||
- Added encrypted interface discovery announces. Welcome home.
|
||||
- Added bootstrap interface functionality. Decent.
|
||||
- Added blackhole handling and management. Thank the Chinese guy.
|
||||
- Added distributed blackhole list publishing and updating. Spammers go home.
|
||||
- Added foundational network identity implementation. All your base.
|
||||
- Added `await_path` method to API. Tick-tock.
|
||||
- Added reverse-unicast peer discovery packet mechanism to AutoInterface. Ping-pong.
|
||||
- Added custom identity support to `rncp`, thanks MikelCalvo!
|
||||
- Added monitor mode to `rnstatus`, thanks MikelCalvo!
|
||||
- Improved announce processing. Swoosh.
|
||||
- Updated documentation quite a bit. Looky.
|
||||
- Enabled per-peer ingress limiting on Weave and Auto interfaces. Hammertime.
|
||||
- Fixed **the** typo, yes it's the olny one I'm sure.
|
||||
- Fixed bugs. Squish.
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
180b8baec2ec7d21abe2cec25ff763e70b2129c012fb02fc23c2fd654f94c1f5 dist/rns-1.1.0-py3-none-any.whl
|
||||
d9e32caf66a9c53199e901d2c173e1de1bf50f1f0c9d5250e5d1b3b07bedcd7c dist/rnspure-1.1.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-11-19: RNS 1.0.4
|
||||
|
||||
This maintenance release adds improved handling for RNodes with a PA/LNA combo.
|
||||
|
||||
**Changes**
|
||||
- Improved handling for RNodes with PA/LNA combo
|
||||
- Added interference detection stats to `rnstatus` output for RNode interfaces
|
||||
- Updated documentation
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
7a2b7893410833b42c0fa7f9a9e3369cebb085cdd26bd83f3031fa6c1051653c rns-1.0.4-py3-none-any.whl
|
||||
ee647e7b3b94abdf1fab618a861390531a4aacc93eecce12c9e97280195c0e2d rnspure-1.0.4-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-11-19: RNS 1.0.3
|
||||
|
||||
This release includes updates to RNode BLE reliability, and adds support for connecting RNodes to a host over WiFi and Ethernet.
|
||||
|
||||
**Changes**
|
||||
- Added support for connecting RNode devices over WiFi and Ethernet
|
||||
- Added support for configuring RNode WiFi and IP settings to `rnodeconf`
|
||||
- Updated BLE connection read timeouts on Android, fixes intermittent BLE connection resets in areas with high 2.4GHz spectrum utilization
|
||||
- Added handling for edge case where RNode serial port was never opened due to failure on interface detach
|
||||
- Fixed broken links in documentation
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
6bafde4c838ad778bf6878967e84c798e34d6ca621b255f59a60f38cb04ac138 rns-1.0.3-py3-none-any.whl
|
||||
f277899f95c1189c6bf3beb40ac656c8b36dfd3d7e4cfb2bc3b4a1e6dc3484fa rnspure-1.0.3-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-11-10: RNS 1.0.2
|
||||
|
||||
This maintenance release adds support for high-power RNodes with a LoRa PA and/or LNA.
|
||||
|
||||
**Changes**
|
||||
- Added support for RNodes with a PA and/or LNA
|
||||
- Added support for monitoring RNode CPU temperature via `rnodeconf`
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
723bcf0a839025060ff680c4202b09fa766b35093a4a08506bb85485b8a1f154 rns-1.0.2-py3-none-any.whl
|
||||
b02de8aeb1381ed2610f27f78799bab031367ed7bf500951fb8d5c2542d4a409 rnspure-1.0.2-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-11-02: RNS 1.0.1
|
||||
|
||||
This release brings a number of bugfixes, as well as stability and reliability improvements. It also adds support for using Weave devices as Reticulum interfaces, fixes long-standing Bluetooth Low Energy connection issues on Android, and includes several API and usability improvements.
|
||||
|
||||
**Changes**
|
||||
- Added path response signalling to announce handler API
|
||||
- Added interface module for Weave devices
|
||||
- Added support for connecting to Weave devices over serial/USB on Android
|
||||
- Added support for allow files to `rnx`
|
||||
- Added detection and logging of multicast echoes never arriving on AutoInterface system devices.
|
||||
- Added Heltec v4 support to `rnodeconf`
|
||||
- Implemented handler for ensuring dynamic destination app data can be generated and sent even on first system-internal discovery announce
|
||||
- Updated documentation and manual
|
||||
- Improved `AutoInterface` peering timing
|
||||
- Fixed RNodeInterface Bluetooth Low Energy connection hangs on Android
|
||||
- Fixed RNodeInterface Bluetooth Low Energy MTU not being configured correctly on Android
|
||||
- Fixed command byte collision in RNodeInterface and RNodeMultiInterface
|
||||
- Fixed string formatting for Android log output
|
||||
- Updated output formatting for `rnid`
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
aa77b4c8e1b6899117666e1e55b05b3250416ab5fea2826254358ae320e8b3ed rns-1.0.1-py3-none-any.whl
|
||||
b3ddfa0b533631d9f1213043a0282952ae6e9f72c3072bbca053ac48e0483f7e rnspure-1.0.1-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-07-14: RNS 1.0.0
|
||||
|
||||
We're out of beta. Thanks to **everyone** who helped make it this far.
|
||||
|
||||
This release brings a number of bugfixes, as well as stability and reliability improvements.
|
||||
|
||||
**Changes**
|
||||
- Improved BLE device discovery on Android
|
||||
- Improved BLE MTU configuration on Android
|
||||
- Fixed a bug in handling of link requests with invalid link mode bytes
|
||||
- Fixed potential AutoInterface peer discovery add before final init complete
|
||||
- Fixed a potential EPOLL backend hang on interface failure
|
||||
- Fixed various log statements
|
||||
- Fixed announce cap crash for `RNodeMultiInterface` with transport mode enabled
|
||||
- Updated documentation
|
||||
- Removed legacy AES-128 handlers
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
5a9f18840510b69f89c6706d130177e2843c9e19c774707ae2661030d693dfc1 rns-1.0.0-py3-none-any.whl
|
||||
acfd52af9bf41f78be017579ca06c0abe748d0b98dbdc9baacf140a0606599ec rnspure-1.0.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-05-15: RNS β 0.9.6
|
||||
|
||||
This release activates AES-256 as the default encryption mode for all communication. It is the last release that will support the old AES-128 based modes, which will be entirely phased out in the next release.
|
||||
|
||||
This release also includes a number of API and resource consumption improvements, and fixes a bug.
|
||||
|
||||
**Changes**
|
||||
- Enabled AES-256 as default encryption mode for all traffic
|
||||
- Added dynamic link keepalive and timeout calculation
|
||||
- Added ability to efficiently transfer files as responses in the `Request` API
|
||||
- Added ability to include metadata on `Resource` transfers
|
||||
- Added option to specify `Resource` auto-compression limits
|
||||
- Added option to specify `Request` response auto-compression limits
|
||||
- Added `Resource` transfer example
|
||||
- Added allow overwrite option to `rncp`
|
||||
- Improved hardware MTU auto-configuration
|
||||
- Improved handling of file transfers using the `Resource` API
|
||||
- Improved `Resource` transfer memory consumption
|
||||
- Improved memory consumption of applications connected to a shared instance
|
||||
- Improved `rncp` memory consumption for large files
|
||||
- Fixed announce handlers not triggering after shared instance disappearance
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
a23c64a04c1e83fd0ab449f564ac904da7fd4f61c0faf68a063f486cc48b44bd rns-0.9.6-py3-none-any.whl
|
||||
4544882dea902b18b00d8a04c9ab93201974573b7b63c3db06cb310b0acec240 rnspure-0.9.6-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-05-09: RNS β 0.9.5
|
||||
|
||||
This release initiates migration of Reticulum from AES-128 to AES-256 as the default link and packet cipher mode. It is a compatibility/migration release, that while supporting AES-256 doesn't use it by default. It will work with both the old AES-128 based modes, and the new AES-256 based modes. There's a very slight penalty in performance to support both the old and new modes at the same time, but only for single packet APIs (not links), and it really shouldn't be noticeable in any everyday use.
|
||||
|
||||
In the next release, version `0.9.6`, Reticulum will transition fully to AES-256 and use it by default for all communications. That means that both single packets and links will use AES-256 by default. The old AES-128 link mode may or may not be available for a few releases, but will ultimately be phased out entirely.
|
||||
|
||||
The update requires no intervention, configuration changes or anything similar from a users or developers perspective. Everything should simply work. This goes both for the `0.9.5` update, and the next `0.9.6` update that transitions fully to AES-256.
|
||||
|
||||
**Changes**
|
||||
- Added support for AES-256 mode to links and packets
|
||||
- Added dynamic link mode support
|
||||
- Added temporary backwards compatibility for AES-128 link and packet modes
|
||||
- Added `get_mode()` method to link API
|
||||
- Added tests for all enabled link modes
|
||||
- Added `instance_name` option and description to default config file
|
||||
- Improved ratchet persist reliability if Reticulum is force killed while persisting ratchets
|
||||
- Fixed interface string representation for some interfaces
|
||||
- Fixed instance name config option being overwritten if option was not last in section
|
||||
- Fixed unhandled potential exception on fast-flapping `BackboneInterface` connections
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
ae6587c86c98cae0df73567af093cc92fe204e71bb01f2506da9aec626a27e97 rns-0.9.5-py3-none-any.whl
|
||||
96208c1d1234e3e4b1c18ca986bad5d4693aeb431453efd7ade33b87f35600e1 rnspure-0.9.5-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-04-15: RNS β 0.9.4
|
||||
|
||||
This release significantly improves memory utilisation and performance. It also includes a few new features and general improvements to the included utilities and programs.
|
||||
|
||||
**Changes**
|
||||
- Significantly improved memory utilisation, thread count and performance on nodes with many interfaces or clients
|
||||
- Switched local instance communication to run over abstract domain sockets on Linux and Android
|
||||
- Switched instance IPC to run over abstract domain sockets on Linux and Android
|
||||
- Added kernel event based I/O backend on Linux and Android
|
||||
- Added fast `BackboneInterface` type
|
||||
- Added support for XIAO-ESP32S3 to `rnodeconf`
|
||||
- Added interactive shell option to `rnsd`
|
||||
- Added API option to search for identity by identity hash
|
||||
- Added option to run TCP and Backbone interfaces in AP mode
|
||||
- Improved `RNodeMultiInterface` host communications specification
|
||||
- Improved `rncp` statistics output
|
||||
- Improved link and reverse-table culling
|
||||
- Fixed an occasional I/O thread hang on instance shutdown, that would result in an error printed to the console
|
||||
- Fixed various minor interface logging inconsistencies
|
||||
- Fixed various minor interface checking inconsistencies
|
||||
- Updated internal `configobj` implementation
|
||||
- Refactored various parts of the transport core code
|
||||
- Swicthed to using internal `netinfo` implementation instead of including full `ifaddr` library
|
||||
- Cleaned out unneeded dependencies
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
737294f29e013f9fa9c8c1326006d0547497607156828fee3dc2a0d3ddd754e7 rns-0.9.4-py3-none-any.whl
|
||||
0bd8a908af115c27733484853d779574d6383ebc1d78160e5a72c14ed9692a13 rnspure-0.9.4-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-03-13: RNS β 0.9.3
|
||||
|
||||
This maintenance release improves performance and fixes a number of bugs.
|
||||
|
||||
**Changes**
|
||||
- Enabled link MTU discovery by default
|
||||
- Added on-demand object code compilation and loader shim
|
||||
- Added link API methods
|
||||
- Added child interface spawning for AutoInterface
|
||||
- Fixed corrupt ratchet files not being removed on maintenance cleaning
|
||||
- Fixed `rnid` not waiting for announce timebase tick before announcing
|
||||
|
||||
**Release Hashes**
|
||||
```
|
||||
0270c988a2b898b28348cd78138667115d4ef3f7e09c86531baaefbee35ef851 rns-0.9.3-py3-none-any.whl
|
||||
eee1a6c4c9c0f04bb17b12b8fb37b9c4cec12a99c87a046730eb7c9a6ffd999f rnspure-0.9.3-py3-none-any.whl
|
||||
```
|
||||
|
||||
### 2025-01-19: RNS β 0.9.2
|
||||
|
||||
This maintenance release fixes a number of bugs.
|
||||
|
||||
+26
-11
@@ -6,23 +6,30 @@ Apart from writing code, there are many ways in which you can contribute. Before
|
||||
|
||||
## Expected Conduct
|
||||
|
||||
First and foremost, there is one simple requirement for taking part in this community: While we primarily interact virtually, your actions matter and have real consequences. Therefore: **Act like a responsible, civilized person** - also in the face of disputes and heated disagreements. Speak your mind here, discussions are welcome. Just do so in the spirit of being face-to-face with everyone else. Thank you.
|
||||
First and foremost, there is one simple requirement for taking part in this community: While we primarily interact virtually, your actions matter and have real consequences. Therefore: **Act like a responsible, civilized person** - especially in the face of disputes and heated disagreements. Speak your mind here; discussions are welcome. Just do so in the spirit of being face-to-face with everyone else. Thank you.
|
||||
|
||||
In order to keep the discussion forums and issue trackers navigable and useful, the following types of posts will be deleted without notice:
|
||||
|
||||
- Spam.
|
||||
- Questions that have already been adequately answered elsewhere. Use the search function.
|
||||
- Low-effort posts or comments that contain no actual information or useful content. This is not a tea-house.
|
||||
- Post or comments solely containing personal opinions or beliefs without adding anything to the discussion. Facebook and X exists.
|
||||
- Content that simply waste the developer's / maintainer's time with completely obvious "ideas", "insights" or "recommendations". Yes, we have *at least* 8 neurons ourselves.
|
||||
- Posts that fail to understand that developing a highly complex software project with a very small amount of resources and people takes time. Imagining perfection on our behalf is useless.
|
||||
|
||||
If you're new to the community and start out your engagement with any of the above transgressions, you will simply be banned without notice or explanation, and your post will be deleted.
|
||||
|
||||
If you find this "harsh", "unfair" or "unwelcoming", go somewhere else. This is not social club, but a work environment for the people contributing to the project.
|
||||
|
||||
## Asking Questions
|
||||
|
||||
If you want to ask a question, **do not open an issue**. The issue tracker is used by people *working on Reticulum* to track bugs, issues and improvements.
|
||||
If you want to ask a question, **do not open an issue**. The issue tracker is used by people *working on Reticulum* to track bugs, issues and improvements. Instead, ask away on the [discussions](https://github.com/markqvist/Reticulum/discussions).
|
||||
|
||||
Instead, ask away on the [discussions](https://github.com/markqvist/Reticulum/discussions) or on the [Reticulum Matrix channel](https://matrix.to/#/#reticulum:matrix.org) at `#reticulum:matrix.org`
|
||||
|
||||
## Providing Feedback & Ideas
|
||||
|
||||
Likewise, feedback, ideas and feature requests are a very welcome way to contribute, and should also be posted on the [discussions](https://github.com/markqvist/Reticulum/discussions), or on the [Reticulum Matrix channel](https://matrix.to/#/#reticulum:matrix.org) at `#reticulum:matrix.org`.
|
||||
|
||||
Please do not post feature requests or general ideas on the issue tracker, or in direct messages to the primary developers. You are much more likely to get a response and start a constructive discussion by posting your ideas in the public channels created for these purposes.
|
||||
Do not post feature requests or general ideas on the issue tracker, or in direct messages to the primary developers. You are much more likely to get a response and start a constructive discussion by posting your ideas in the public channels created for these purposes.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you have found a bug or issue in this project, please report it using the [issue tracker](https://github.com/markqvist/Reticulum/issues). If at all possible, be sure to include details on how to reproduce the bug.
|
||||
If you have found a bug or issue in this project, please report it using the [issue tracker](https://github.com/markqvist/Reticulum/issues). Be sure to include details on how to reproduce the bug.
|
||||
|
||||
Anything submitted to the issue tracker that does not follow these guidelines will be closed and removed without comments or explanation.
|
||||
|
||||
@@ -40,4 +47,12 @@ Pull requests have a high chance of being accepted if they are:
|
||||
|
||||
Even new ideas and proposals that have not been approved by a maintainer, or fall outside the established roadmap, are *occasionally* accepted - if they possess the remaining of the above qualities. If not, they will be closed and removed without comments or explanation.
|
||||
|
||||
By contributing code to this project, you agree that copyright for the code is transferred to the Reticulum maintainers and that the code is irrevocably placed under the [MIT license](./LICENSE).
|
||||
## Generative AI Policy
|
||||
|
||||
Contributions written using large language models (LLMs) or other generative 'AI' programs are prohibited. LLMs produce errors so frequently and in a way that is so unlike human error that such issues are incredibly time-consuming to spot and fix. This is not a worthwhile tradeoff for Reticulum.
|
||||
|
||||
This applies to all Reticulum-related projects and documentation, as well as all submitted issues and discussion in official channels, except in cases where language translation and/or speech recogntion technologies are required for communication.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
By contributing code to this project, you agree that copyright for the code is transferred to the Reticulum maintainers and that the code is irrevocably placed under the [Reticulum License](./LICENSE).
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# MIT License - Copyright (c) 2024 Mark Qvist / unsigned.io
|
||||
|
||||
# This example illustrates creating a custom interface
|
||||
# definition, that can be loaded and used by Reticulum at
|
||||
# runtime. Any number of custom interfaces can be created
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
##########################################################
|
||||
# This RNS example demonstrates how to set perform #
|
||||
# requests and receive responses over a link. #
|
||||
# This RNS example demonstrates how to perform requests #
|
||||
# and receive responses over a link. #
|
||||
##########################################################
|
||||
|
||||
import os
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
##########################################################
|
||||
# This RNS example demonstrates how to transfer a #
|
||||
# resource over an established link #
|
||||
##########################################################
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import random
|
||||
import argparse
|
||||
import RNS
|
||||
|
||||
# Let's define an app name. We'll use this for all
|
||||
# destinations we create. Since this echo example
|
||||
# is part of a range of example utilities, we'll put
|
||||
# them all within the app namespace "example_utilities"
|
||||
APP_NAME = "example_utilities"
|
||||
|
||||
##########################################################
|
||||
#### Server Part #########################################
|
||||
##########################################################
|
||||
|
||||
# A reference to the latest client link that connected
|
||||
latest_client_link = None
|
||||
|
||||
# This initialisation is executed when the users chooses
|
||||
# to run as a server
|
||||
def server(configpath):
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
|
||||
# Randomly create a new identity for our link example
|
||||
server_identity = RNS.Identity()
|
||||
|
||||
# We create a destination that clients can connect to. We
|
||||
# want clients to create links to this destination, so we
|
||||
# need to create a "single" destination type.
|
||||
server_destination = RNS.Destination(
|
||||
server_identity,
|
||||
RNS.Destination.IN,
|
||||
RNS.Destination.SINGLE,
|
||||
APP_NAME,
|
||||
"resourceexample"
|
||||
)
|
||||
|
||||
# We configure a function that will get called every time
|
||||
# a new client creates a link to this destination.
|
||||
server_destination.set_link_established_callback(client_connected)
|
||||
|
||||
# Everything's ready!
|
||||
# Let's Wait for client resources or user input
|
||||
server_loop(server_destination)
|
||||
|
||||
def server_loop(destination):
|
||||
# Let the user know that everything is ready
|
||||
RNS.log(
|
||||
"Resource example "+
|
||||
RNS.prettyhexrep(destination.hash)+
|
||||
" running, waiting for a connection."
|
||||
)
|
||||
|
||||
RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
|
||||
|
||||
# We enter a loop that runs until the users exits.
|
||||
# If the user hits enter, we will announce our server
|
||||
# destination on the network, which will let clients
|
||||
# know how to create messages directed towards it.
|
||||
while True:
|
||||
entered = input()
|
||||
destination.announce()
|
||||
RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
|
||||
|
||||
# When a client establishes a link to our server
|
||||
# destination, this function will be called with
|
||||
# a reference to the link.
|
||||
def client_connected(link):
|
||||
global latest_client_link
|
||||
RNS.log("Client connected")
|
||||
|
||||
# We configure the link to accept all resources
|
||||
# and set a callback for completed resources
|
||||
link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
|
||||
link.set_resource_concluded_callback(resource_concluded)
|
||||
|
||||
link.set_link_closed_callback(client_disconnected)
|
||||
latest_client_link = link
|
||||
|
||||
def client_disconnected(link):
|
||||
RNS.log("Client disconnected")
|
||||
|
||||
def resource_concluded(resource):
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
RNS.log(f"Resource {resource} received")
|
||||
RNS.log(f"Metadata: {resource.metadata}")
|
||||
RNS.log(f"Data length: {os.stat(resource.data.name).st_size}")
|
||||
RNS.log(f"Data can be read directly from: {resource.data}")
|
||||
RNS.log(f"Data can be moved or copied from: {resource.data.name}")
|
||||
RNS.log(f"First 32 bytes of data: {RNS.hexrep(resource.data.read(32))}")
|
||||
else:
|
||||
RNS.log(f"Receiving resource {resource} failed")
|
||||
|
||||
|
||||
|
||||
##########################################################
|
||||
#### Client Part #########################################
|
||||
##########################################################
|
||||
|
||||
# A reference to the server link
|
||||
server_link = None
|
||||
|
||||
def random_text_generator():
|
||||
texts = ["They looked up", "On each full moon", "Becky was upset", "I’ll stay away from it", "The pet shop stocks everything"]
|
||||
return texts[random.randint(0, len(texts)-1)]
|
||||
|
||||
# This initialisation is executed when the users chooses
|
||||
# to run as a client
|
||||
def client(destination_hexhash, configpath):
|
||||
# We need a binary representation of the destination
|
||||
# hash that was entered on the command line
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError(
|
||||
"Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
|
||||
)
|
||||
|
||||
destination_hash = bytes.fromhex(destination_hexhash)
|
||||
except:
|
||||
RNS.log("Invalid destination entered. Check your input!\n")
|
||||
sys.exit(0)
|
||||
|
||||
# We must first initialise Reticulum
|
||||
reticulum = RNS.Reticulum(configpath)
|
||||
|
||||
# Check if we know a path to the destination
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
while not RNS.Transport.has_path(destination_hash):
|
||||
time.sleep(0.1)
|
||||
|
||||
# Recall the server identity
|
||||
server_identity = RNS.Identity.recall(destination_hash)
|
||||
|
||||
# Inform the user that we'll begin connecting
|
||||
RNS.log("Establishing link with server...")
|
||||
|
||||
# When the server identity is known, we set
|
||||
# up a destination
|
||||
server_destination = RNS.Destination(
|
||||
server_identity,
|
||||
RNS.Destination.OUT,
|
||||
RNS.Destination.SINGLE,
|
||||
APP_NAME,
|
||||
"resourceexample"
|
||||
)
|
||||
|
||||
# And create a link
|
||||
link = RNS.Link(server_destination)
|
||||
|
||||
# We'll set up functions to inform the
|
||||
# user when the link is established or closed
|
||||
link.set_link_established_callback(link_established)
|
||||
link.set_link_closed_callback(link_closed)
|
||||
|
||||
# Everything is set up, so let's enter a loop
|
||||
# for the user to interact with the example
|
||||
client_loop()
|
||||
|
||||
def client_loop():
|
||||
global server_link
|
||||
|
||||
# Wait for the link to become active
|
||||
while not server_link:
|
||||
time.sleep(0.1)
|
||||
|
||||
should_quit = False
|
||||
while not should_quit:
|
||||
try:
|
||||
print("> ", end=" ")
|
||||
text = input()
|
||||
|
||||
# Check if we should quit the example
|
||||
if text == "quit" or text == "q" or text == "exit":
|
||||
should_quit = True
|
||||
server_link.teardown()
|
||||
|
||||
else:
|
||||
# Generate 32 megabytes of random data
|
||||
data = os.urandom(32*1024*1024)
|
||||
RNS.log(f"Data length: {len(data)}")
|
||||
RNS.log(f"First 32 bytes of data: {RNS.hexrep(data[:32])}")
|
||||
|
||||
# Generate some metadata
|
||||
metadata = {"text": random_text_generator(), "numbers": [1,2,3,4], "blob": os.urandom(16)}
|
||||
|
||||
# Send the resource
|
||||
resource = RNS.Resource(data, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False)
|
||||
|
||||
# Alternatively, you can stream data
|
||||
# directly from an open file descriptor
|
||||
|
||||
# with open("/path/to/file", "rb") as data_file:
|
||||
# resource = RNS.Resource(data_file, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while sending resource over the link: "+str(e))
|
||||
should_quit = True
|
||||
server_link.teardown()
|
||||
|
||||
def resource_concluded_sending(resource):
|
||||
if resource.status == RNS.Resource.COMPLETE: RNS.log(f"The resource {resource} was sent successfully")
|
||||
else: RNS.log(f"Sending the resource {resource} failed")
|
||||
|
||||
# This function is called when a link
|
||||
# has been established with the server
|
||||
def link_established(link):
|
||||
# We store a reference to the link
|
||||
# instance for later use
|
||||
global server_link
|
||||
server_link = link
|
||||
|
||||
# Inform the user that the server is
|
||||
# connected
|
||||
RNS.log("Link established with server, hit enter to sand a resource, or type in \"quit\" to quit")
|
||||
|
||||
# When a link is closed, we'll inform the
|
||||
# user, and exit the program
|
||||
def link_closed(link):
|
||||
if link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
RNS.log("The link timed out, exiting now")
|
||||
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
|
||||
RNS.log("The link was closed by the server, exiting now")
|
||||
else:
|
||||
RNS.log("Link closed, exiting now")
|
||||
|
||||
time.sleep(1.5)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
##########################################################
|
||||
#### Program Startup #####################################
|
||||
##########################################################
|
||||
|
||||
# This part of the program runs at startup,
|
||||
# and parses input of from the user, and then
|
||||
# starts up the desired program mode.
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description="Simple resource example")
|
||||
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--server",
|
||||
action="store_true",
|
||||
help="wait for incoming resources from clients"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
action="store",
|
||||
default=None,
|
||||
help="path to alternative Reticulum config directory",
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"destination",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="hexadecimal hash of the server destination",
|
||||
type=str
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.config:
|
||||
configarg = args.config
|
||||
else:
|
||||
configarg = None
|
||||
|
||||
if args.server:
|
||||
server(configarg)
|
||||
else:
|
||||
if (args.destination == None):
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
else:
|
||||
client(args.destination, configarg)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"drips": {
|
||||
"ethereum": {
|
||||
"ownedBy": "0xae89F3B94fC4AD6563F0864a55F9a697a90261ff"
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
liberapay: Reticulum
|
||||
ko_fi: markqvist
|
||||
custom: "https://unsigned.io/donate"
|
||||
custom: "https://unsigned.io/donate"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License, unless otherwise noted
|
||||
Reticulum License
|
||||
|
||||
Copyright (c) 2016-2024 Mark Qvist / unsigned.io
|
||||
Copyright (c) 2016-2025 Mark Qvist
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
- The Software shall not be used in any kind of system which includes amongst
|
||||
its functions the ability to purposefully do harm to human beings.
|
||||
|
||||
- The Software shall not be used, directly or indirectly, in the creation of
|
||||
an artificial intelligence, machine learning or language model training
|
||||
dataset, including but not limited to any use that contributes to the
|
||||
training or development of such a model or algorithm.
|
||||
|
||||
- The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
This repository is a public mirror. All potential future development is happening elsewhere.
|
||||
|
||||
I am stepping back from all public-facing interaction with this project. Reticulum has always been primarily my work, and continuing in the current public, internet-facing model is no longer sustainable.
|
||||
|
||||
The software remains available for use as-is. Occasional updates may appear at unpredictable intervals, but there will be no support, no responses to issues, no discussions, and no community management in this or any other public venue. If it doesn't work for you, it doesn't work. That is the entire extent of available troubleshooting assistance I can offer you.
|
||||
|
||||
If you've followed this project for a while, you already know what this means. You know who designed, wrote and tested this, and you know how many years of my life it took. You'll also know about both my particular challenges and strengths, and how I believe anything worth building needs to be built and maintained with our own hands.
|
||||
|
||||
Seven months ago, I said I needed to step back, that I was exhausted, and that I needed to recover. I believed a public resolve would be enough to effectuate that, but while striving to get just a few more useful features and protocols out, the unproductive requests and demands also ramped up, and I got pulled back into the same patterns and draining interactions that I'd explicitly said I couldn't sustain anymore.
|
||||
|
||||
So here's what you might have already guessed: I'm done playing the game by rules I can't win at.
|
||||
|
||||
Everything you need is right here, and by any sensible measure, it's done. Anyone who wants to invest the time, skill and persistence can build on it, or completely re-imagine it with different priorities. That was always the point.
|
||||
|
||||
The people who actually contributed - you know who you are, and you know I mean it when I say: Thank you. All of you who've used this to build something real - that was the goal, and you did it without needing me to hold your hand.
|
||||
|
||||
The rest of you: You have what you need. Use it or don't. I am not going to be the person who explains it to you anymore.
|
||||
|
||||
This is not a temporary break. It's not "see you after some rest", but a recognition that the current model is fundamentally incompatible with my life, my health, and my reality.
|
||||
|
||||
If you want to support continued work, you can do so at the donation links listed in this repository. But please understand, that this is not purchasing support or guaranteeing updates. It is support for work that happens on my timeline, according to my capacity, which at the moment is not what it was.
|
||||
|
||||
If you want Reticulum to continue evolving, you have the power to make that happen. The protocol is public domain. The code is open source. Everything you need is right here. I've provided the tools, but building what comes next is not my responsibility anymore. It's yours.
|
||||
|
||||
To the small group of people who has actually been here, and understood what this work was and what it cost - you already know where to find me if it actually matters.
|
||||
|
||||
To everyone else: This is where we part ways. No hard feelings. It's just time.
|
||||
|
||||
---
|
||||
|
||||
असतो मा सद्गमय
|
||||
तमसो मा ज्योतिर्गमय
|
||||
मृत्योर्मा अमृतं गमय
|
||||
@@ -24,6 +24,12 @@ clean:
|
||||
@make -C docs clean
|
||||
@echo Done
|
||||
|
||||
purge_docs:
|
||||
@echo Purging documentation build...
|
||||
@-rm -rf ./docs/manual
|
||||
@-rm -rf ./docs/*.pdf
|
||||
@-rm -rf ./docs/*.epub
|
||||
|
||||
remove_symlinks:
|
||||
@echo Removing symlinks for build...
|
||||
-rm Examples/RNS
|
||||
@@ -34,14 +40,14 @@ create_symlinks:
|
||||
-ln -s ../RNS ./Examples/
|
||||
-ln -s ../../RNS ./RNS/Utilities/
|
||||
|
||||
build_sdist_only:
|
||||
build_sdist: purge_docs
|
||||
python3 setup.py sdist
|
||||
|
||||
build_wheel:
|
||||
python3 setup.py sdist bdist_wheel
|
||||
python3 setup.py bdist_wheel
|
||||
|
||||
build_pure_wheel:
|
||||
python3 setup.py sdist bdist_wheel --pure
|
||||
python3 setup.py bdist_wheel --pure
|
||||
|
||||
documentation:
|
||||
make -C docs html
|
||||
@@ -49,7 +55,9 @@ documentation:
|
||||
manual:
|
||||
make -C docs latexpdf epub
|
||||
|
||||
release: test remove_symlinks build_wheel build_pure_wheel documentation manual create_symlinks
|
||||
build_spkg: remove_symlinks build_sdist create_symlinks
|
||||
|
||||
release: test remove_symlinks build_sdist build_wheel build_pure_wheel documentation manual create_symlinks
|
||||
|
||||
debug: remove_symlinks build_wheel build_pure_wheel create_symlinks
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
Reticulum Network Stack β <img align="right" src="https://static.pepy.tech/personalized-badge/rns?period=total&units=international_system&left_color=grey&right_color=blue&left_text=Installs" style="padding-left:10px"/><a href="https://github.com/markqvist/Reticulum/actions/workflows/build.yml"><img align="right" src="https://github.com/markqvist/Reticulum/actions/workflows/build.yml/badge.svg"/></a>
|
||||
Reticulum Network Stack <img align="right" src="https://static.pepy.tech/personalized-badge/rns?period=month&units=international_system&left_color=grey&right_color=blue&left_text=Installs/month" style="padding-left:10px"/><a href="https://github.com/markqvist/Reticulum/actions/workflows/build.yml"><img align="right" src="https://github.com/markqvist/Reticulum/actions/workflows/build.yml/badge.svg"/></a>
|
||||
==========
|
||||
|
||||
<p align="center"><img width="200" src="https://raw.githubusercontent.com/markqvist/Reticulum/master/docs/source/graphics/rns_logo_512.png"></p>
|
||||
|
||||
*This repository is [a public mirror](./MIRROR.md). All development is happening elsewhere.*
|
||||
|
||||
Reticulum is the cryptography-based networking stack for building local and wide-area
|
||||
networks with readily available hardware. It can operate even with very high latency
|
||||
and extremely low bandwidth. Reticulum allows you to build wide-area networks
|
||||
@@ -52,7 +54,7 @@ For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ
|
||||
- Forward Secrecy is available for all communication types, both for single packets and over links
|
||||
- Reticulum uses the following format for encrypted tokens:
|
||||
- Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519
|
||||
- AES-128 in CBC mode with PKCS7 padding
|
||||
- AES-256 in CBC mode with PKCS7 padding
|
||||
- HMAC using SHA256 for authentication
|
||||
- IVs are generated through os.urandom()
|
||||
- Unforgeable packet delivery confirmations
|
||||
@@ -62,7 +64,7 @@ For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ
|
||||
- Easily create your own custom interfaces for communicating over anything
|
||||
- Authentication and virtual network segmentation on all supported interface types
|
||||
- An intuitive and easy-to-use API
|
||||
- Simpler and easier to use than sockets APIs and simpler, but more powerful
|
||||
- Simpler and easier to use than sockets APIs, but more powerful
|
||||
- Makes building distributed and decentralised applications much simpler
|
||||
- Reliable and efficient transfer of arbitrary amounts of data
|
||||
- Reticulum can handle a few bytes of data or files of many gigabytes
|
||||
@@ -86,9 +88,10 @@ following resources.
|
||||
|
||||
- You can use the [rnsh](https://github.com/acehoss/rnsh) program to establish remote shell sessions over Reticulum.
|
||||
- [LXMF](https://github.com/markqvist/lxmf) is a distributed, delay and disruption tolerant message transfer protocol built on Reticulum
|
||||
- The [LXST](https://github.com/markqvist/lxst) protocol and framework provides real-time audio and signals transport over Reticulum. It includes primitives and utilities for building voice-based applications and hardware devices, such as the `rnphone` program, that can be used to build hardware telephones.
|
||||
- For an off-grid, encrypted and resilient mesh communications platform, see [Nomad Network](https://github.com/markqvist/NomadNet)
|
||||
- The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and focuses on ease of use.
|
||||
- [MeshChat](https://github.com/liamcottle/reticulum-meshchat) is a user-friendly LXMF client, that also supports voice calls.
|
||||
- The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and many advanced features, such as file transfers, image and voice messages, real-time voice calls, a distributed telemetry system, mapping capabilities and full plugin extensibility.
|
||||
- [MeshChat](https://github.com/liamcottle/reticulum-meshchat) is a user-friendly LXMF client with a web-based interface, that also supports image and voice messages, as well as file transfers. It also includes a built-in page browser for browsing Nomad Network nodes.
|
||||
|
||||
## Where can Reticulum be used?
|
||||
Over practically any medium that can support at least a half-duplex channel
|
||||
@@ -205,15 +208,14 @@ provide a dynamic performance envelope from 250 bits per second, to 1 gigabit
|
||||
per second on normal hardware.
|
||||
|
||||
Currently, the usable performance envelope is approximately 150 bits per second
|
||||
to 40 megabits per second, with physical mediums faster than that not being
|
||||
to 500 megabits per second, with physical mediums faster than that not being
|
||||
saturated. Performance beyond the current level is intended for future
|
||||
upgrades, but not highly prioritised at this point in time.
|
||||
|
||||
## Current Status
|
||||
Reticulum should currently be considered beta software. All core protocol
|
||||
features are implemented and functioning, but additions will probably occur as
|
||||
real-world use is explored. There will be bugs. The API and wire-format can be
|
||||
considered relatively stable at the moment, but could change if warranted.
|
||||
All core protocol features are implemented and functioning, but additions will
|
||||
probably occur as real-world use is explored and understood. The API and wire-format
|
||||
can be considered stable.
|
||||
|
||||
## Dependencies
|
||||
The installation of the default `rns` package requires the dependencies listed
|
||||
@@ -280,14 +282,9 @@ file:
|
||||
target_host = reticulum.betweentheborders.com
|
||||
target_port = 4242
|
||||
|
||||
# Interface to Testnet I2P Hub
|
||||
[[RNS Testnet I2P Hub]]
|
||||
type = I2PInterface
|
||||
enabled = yes
|
||||
peers = g3br23bvx3lq5uddcsjii74xgmn6y5q325ovrkq2zw2wbzbqgbuq.b32.i2p
|
||||
```
|
||||
|
||||
The testnet also contains a number of [Nomad Network](https://github.com/markqvist/nomadnet) nodes, and LXMF propagation nodes.
|
||||
As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, the public Amsterdam Testnet entrypoint is slated for de-commisioning in the first quarter of 2026. If your own instances rely on this entrypoint for connectivity, it is high time to start configuring alternatives. Reticulum now includes a full on-network interface discovery and connectivity bootstrapping system. Read the [Bootstrapping Connectivity](https://reticulum.network/manual/gettingstartedfast.html#bootstrapping-connectivity) section of the manual for pointers.
|
||||
|
||||
## Support Reticulum
|
||||
You can help support the continued development of open, free and private communications systems by donating via one of the following channels:
|
||||
@@ -296,23 +293,30 @@ You can help support the continued development of open, free and private communi
|
||||
```
|
||||
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
||||
```
|
||||
- Ethereum
|
||||
```
|
||||
0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73
|
||||
```
|
||||
- Bitcoin
|
||||
```
|
||||
35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH
|
||||
bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6
|
||||
```
|
||||
- Ko-Fi: https://ko-fi.com/markqvist
|
||||
- Ethereum
|
||||
```
|
||||
0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E
|
||||
```
|
||||
- Liberapay: https://liberapay.com/Reticulum/
|
||||
|
||||
Are certain features in the development roadmap are important to you or your
|
||||
organisation? Make them a reality quickly by sponsoring their implementation.
|
||||
- Ko-Fi: https://ko-fi.com/markqvist
|
||||
|
||||
## Cryptographic Primitives
|
||||
Reticulum uses a simple suite of efficient, strong and well-tested cryptographic
|
||||
primitives, with widely available implementations that can be used both on
|
||||
general-purpose CPUs and on microcontrollers. The utilised primitives are:
|
||||
general-purpose CPUs and on microcontrollers.
|
||||
|
||||
One of the primary considerations for choosing this particular set of primitives is
|
||||
that they can be implemented *safely* with relatively few pitfalls, on practically
|
||||
all current computing platforms.
|
||||
|
||||
The primitives listed here **are authoritative**. Anything claiming to be Reticulum,
|
||||
but not using these exact primitives **is not** Reticulum, and possibly an
|
||||
intentionally compromised or weakened clone. The utilised primitives are:
|
||||
|
||||
- Reticulum Identity Keys are 512-bit Curve25519 keysets
|
||||
- A 256-bit Ed25519 key for signatures
|
||||
@@ -320,15 +324,15 @@ general-purpose CPUs and on microcontrollers. The utilised primitives are:
|
||||
- HKDF for key derivation
|
||||
- Encrypted tokens are based on the [Fernet spec](https://github.com/fernet/spec/)
|
||||
- Ephemeral keys derived from an ECDH key exchange on Curve25519
|
||||
- AES-128 in CBC mode with PKCS7 padding
|
||||
- HMAC using SHA256 for message authentication
|
||||
- IVs are generated through os.urandom()
|
||||
- IVs must be generated through `os.urandom()` or better
|
||||
- AES-256 in CBC mode with PKCS7 padding
|
||||
- No Fernet version and timestamp metadata fields
|
||||
- SHA-256
|
||||
- SHA-512
|
||||
|
||||
In the default installation configuration, the `X25519`, `Ed25519` and
|
||||
`AES-128-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/)
|
||||
In the default installation configuration, the `X25519`, `Ed25519`,
|
||||
and `AES-256-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/)
|
||||
(via the [PyCA/cryptography](https://github.com/pyca/cryptography) package).
|
||||
The hashing functions `SHA-256` and `SHA-512` are provided by the standard
|
||||
Python [hashlib](https://docs.python.org/3/library/hashlib.html). The `HKDF`,
|
||||
@@ -342,13 +346,19 @@ provided by the following internal implementations:
|
||||
|
||||
|
||||
Reticulum also includes a complete implementation of all necessary primitives
|
||||
in pure Python. If OpenSSL & PyCA are not available on the system when
|
||||
in pure Python. If OpenSSL and PyCA are not available on the system when
|
||||
Reticulum is started, Reticulum will instead use the internal pure-python
|
||||
primitives. A trivial consequence of this is performance, with the OpenSSL
|
||||
backend being *much* faster. The most important consequence however, is the
|
||||
potential loss of security by using primitives that has not seen the same
|
||||
amount of scrutiny, testing and review as those from OpenSSL.
|
||||
|
||||
Please note that by default, installing Reticulum will **require** OpenSSL and
|
||||
PyCA to also be automatically installed if not already available. It is only
|
||||
possible to use the pure-python primitives if this requirement is specifically
|
||||
overridden by the user, for example by installing the `rnspure` package instead
|
||||
of the normal `rns` package, or by running directly from local source-code.
|
||||
|
||||
If you want to use the internal pure-python primitives, it is **highly
|
||||
advisable** that you have a good understanding of the risks that this pose, and
|
||||
make an informed decision on whether those risks are acceptable to you.
|
||||
@@ -372,12 +382,12 @@ projects:
|
||||
- [PyCA/cryptography](https://github.com/pyca/cryptography), *BSD License*
|
||||
- [Pure-25519](https://github.com/warner/python-pure25519) by [Brian Warner](https://github.com/warner), *MIT License*
|
||||
- [Pysha2](https://github.com/thomdixon/pysha2) by [Thom Dixon](https://github.com/thomdixon), *MIT License*
|
||||
- [Python-AES](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License*
|
||||
- [Python AES-128](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License*
|
||||
- [Python AES-256](https://github.com/boppreh/aes) by [BoppreH](https://github.com/boppreh), *MIT License*
|
||||
- [Curve25519.py](https://gist.github.com/nickovs/cc3c22d15f239a2640c185035c06f8a3#file-curve25519-py) by [Nicko van Someren](https://gist.github.com/nickovs), *Public Domain*
|
||||
- [I2Plib](https://github.com/l-n-s/i2plib) by [Viktor Villainov](https://github.com/l-n-s)
|
||||
- [PySerial](https://github.com/pyserial/pyserial) by Chris Liechti, *BSD License*
|
||||
- [Configobj](https://github.com/DiffSK/configobj) by Michael Foord, Nicola Larosa, Rob Dennis & Eli Courtwright, *BSD License*
|
||||
- [Six](https://github.com/benjaminp/six) by [Benjamin Peterson](https://github.com/benjaminp), *MIT License*
|
||||
- [ifaddr](https://github.com/pydron/ifaddr) by [Pydron](https://github.com/pydron), *MIT License*
|
||||
- [ifaddr](https://github.com/pydron/ifaddr) by Stefan C. Mueller, *MIT License*
|
||||
- [Umsgpack.py](https://github.com/vsergeev/u-msgpack-python) by [Ivan A. Sergeev](https://github.com/vsergeev)
|
||||
- [Python](https://www.python.org)
|
||||
|
||||
+12
-4
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
+12
-4
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
+55
-12
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -24,23 +32,57 @@ import RNS.Cryptography.Provider as cp
|
||||
import RNS.vendor.platformutils as pu
|
||||
|
||||
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
|
||||
from .aes import AES
|
||||
from .aes import AES128
|
||||
from .aes import AES256
|
||||
|
||||
elif cp.PROVIDER == cp.PROVIDER_PYCA:
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
if pu.cryptography_old_api():
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
if pu.cryptography_old_api(): from cryptography.hazmat.backends import default_backend
|
||||
|
||||
|
||||
class AES_128_CBC:
|
||||
|
||||
@staticmethod
|
||||
def encrypt(plaintext, key, iv):
|
||||
if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
|
||||
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
|
||||
cipher = AES(key)
|
||||
cipher = AES128(key)
|
||||
return cipher.encrypt(plaintext, iv)
|
||||
|
||||
elif cp.PROVIDER == cp.PROVIDER_PYCA:
|
||||
if not pu.cryptography_old_api():
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
||||
else:
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
|
||||
encryptor = cipher.encryptor()
|
||||
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
||||
return ciphertext
|
||||
|
||||
@staticmethod
|
||||
def decrypt(ciphertext, key, iv):
|
||||
if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
|
||||
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
|
||||
cipher = AES128(key)
|
||||
return cipher.decrypt(ciphertext, iv)
|
||||
|
||||
elif cp.PROVIDER == cp.PROVIDER_PYCA:
|
||||
if not pu.cryptography_old_api():
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
||||
else:
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
|
||||
|
||||
decryptor = cipher.decryptor()
|
||||
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
||||
return plaintext
|
||||
|
||||
class AES_256_CBC:
|
||||
@staticmethod
|
||||
def encrypt(plaintext, key, iv):
|
||||
if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
|
||||
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
|
||||
cipher = AES256(key)
|
||||
return cipher.encrypt_cbc(plaintext, iv)
|
||||
|
||||
elif cp.PROVIDER == cp.PROVIDER_PYCA:
|
||||
if not pu.cryptography_old_api():
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
||||
@@ -53,9 +95,10 @@ class AES_128_CBC:
|
||||
|
||||
@staticmethod
|
||||
def decrypt(ciphertext, key, iv):
|
||||
if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for {self}")
|
||||
if cp.PROVIDER == cp.PROVIDER_INTERNAL:
|
||||
cipher = AES(key)
|
||||
return cipher.decrypt(ciphertext, iv)
|
||||
cipher = AES256(key)
|
||||
return cipher.decrypt_cbc(ciphertext, iv)
|
||||
|
||||
elif cp.PROVIDER == cp.PROVIDER_PYCA:
|
||||
if not pu.cryptography_old_api():
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
from .pure25519 import ed25519_oop as ed25519
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
@@ -1,4 +1,34 @@
|
||||
import importlib
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('hashlib') != None:
|
||||
import hashlib
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
@@ -1,16 +1,47 @@
|
||||
import importlib
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import importlib.util
|
||||
|
||||
PROVIDER_NONE = 0x00
|
||||
PROVIDER_INTERNAL = 0x01
|
||||
PROVIDER_PYCA = 0x02
|
||||
|
||||
FORCE_INTERNAL = False
|
||||
PROVIDER = PROVIDER_NONE
|
||||
|
||||
pyca_v = None
|
||||
use_pyca = False
|
||||
|
||||
try:
|
||||
if importlib.util.find_spec('cryptography') != None:
|
||||
if not FORCE_INTERNAL and importlib.util.find_spec('cryptography') != None:
|
||||
import cryptography
|
||||
pyca_v = cryptography.__version__
|
||||
v = pyca_v.split(".")
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
||||
|
||||
+48
-44
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -25,7 +33,9 @@ import time
|
||||
|
||||
from RNS.Cryptography import HMAC
|
||||
from RNS.Cryptography import PKCS7
|
||||
from RNS.Cryptography import AES
|
||||
from RNS.Cryptography.AES import AES_128_CBC
|
||||
from RNS.Cryptography.AES import AES_256_CBC
|
||||
|
||||
class Token():
|
||||
"""
|
||||
@@ -40,71 +50,65 @@ class Token():
|
||||
TOKEN_OVERHEAD = 48 # Bytes
|
||||
|
||||
@staticmethod
|
||||
def generate_key():
|
||||
return os.urandom(32)
|
||||
def generate_key(mode=AES_256_CBC):
|
||||
if mode == AES_128_CBC: return os.urandom(32)
|
||||
elif mode == AES_256_CBC: return os.urandom(64)
|
||||
else: raise TypeError(f"Invalid token mode: {mode}")
|
||||
|
||||
def __init__(self, key = None):
|
||||
if key == None:
|
||||
raise ValueError("Token key cannot be None")
|
||||
def __init__(self, key=None, mode=AES):
|
||||
if key == None: raise ValueError("Token key cannot be None")
|
||||
|
||||
if len(key) != 32:
|
||||
raise ValueError("Token key must be 32 bytes, not "+str(len(key)))
|
||||
|
||||
self._signing_key = key[:16]
|
||||
self._encryption_key = key[16:]
|
||||
if mode == AES:
|
||||
if len(key) == 32:
|
||||
self.mode = AES_128_CBC
|
||||
self._signing_key = key[:16]
|
||||
self._encryption_key = key[16:]
|
||||
|
||||
elif len(key) == 64:
|
||||
self.mode = AES_256_CBC
|
||||
self._signing_key = key[:32]
|
||||
self._encryption_key = key[32:]
|
||||
|
||||
else: raise ValueError("Token key must be 128 or 256 bits, not "+str(len(key)*8))
|
||||
|
||||
else: raise TypeError(f"Invalid token mode: {mode}")
|
||||
|
||||
|
||||
def verify_hmac(self, token):
|
||||
if len(token) <= 32:
|
||||
raise ValueError("Cannot verify HMAC on token of only "+str(len(token))+" bytes")
|
||||
if len(token) <= 32: raise ValueError("Cannot verify HMAC on token of only "+str(len(token))+" bytes")
|
||||
else:
|
||||
received_hmac = token[-32:]
|
||||
expected_hmac = HMAC.new(self._signing_key, token[:-32]).digest()
|
||||
|
||||
if received_hmac == expected_hmac:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
if received_hmac == expected_hmac: return True
|
||||
else: return False
|
||||
|
||||
|
||||
def encrypt(self, data = None):
|
||||
if not isinstance(data, bytes): raise TypeError("Token plaintext input must be bytes")
|
||||
iv = os.urandom(16)
|
||||
current_time = int(time.time())
|
||||
|
||||
if not isinstance(data, bytes):
|
||||
raise TypeError("Token plaintext input must be bytes")
|
||||
|
||||
ciphertext = AES_128_CBC.encrypt(
|
||||
ciphertext = self.mode.encrypt(
|
||||
plaintext = PKCS7.pad(data),
|
||||
key = self._encryption_key,
|
||||
iv = iv,
|
||||
)
|
||||
iv = iv)
|
||||
|
||||
signed_parts = iv+ciphertext
|
||||
|
||||
return signed_parts + HMAC.new(self._signing_key, signed_parts).digest()
|
||||
|
||||
|
||||
def decrypt(self, token = None):
|
||||
if not isinstance(token, bytes):
|
||||
raise TypeError("Token must be bytes")
|
||||
|
||||
if not self.verify_hmac(token):
|
||||
raise ValueError("Token HMAC was invalid")
|
||||
if not isinstance(token, bytes): raise TypeError("Token must be bytes")
|
||||
if not self.verify_hmac(token): raise ValueError("Token HMAC was invalid")
|
||||
|
||||
iv = token[:16]
|
||||
ciphertext = token[16:-32]
|
||||
|
||||
try:
|
||||
plaintext = PKCS7.unpad(
|
||||
AES_128_CBC.decrypt(
|
||||
ciphertext,
|
||||
self._encryption_key,
|
||||
iv,
|
||||
)
|
||||
)
|
||||
return PKCS7.unpad(
|
||||
self.mode.decrypt(
|
||||
ciphertext = ciphertext,
|
||||
key = self._encryption_key,
|
||||
iv = iv))
|
||||
|
||||
return plaintext
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError("Could not decrypt token")
|
||||
except Exception as e: raise ValueError(f"Could not decrypt token: {e}")
|
||||
|
||||
@@ -82,10 +82,13 @@ def _fix_secret(n):
|
||||
n |= 64 << 8 * 31
|
||||
return n
|
||||
|
||||
def _fix_base_point(n):
|
||||
n &= ~(2**255)
|
||||
return n
|
||||
|
||||
def curve25519(base_point_raw, secret_raw):
|
||||
"""Raise the base point to a given power"""
|
||||
base_point = _unpack_number(base_point_raw)
|
||||
base_point = _fix_base_point(_unpack_number(base_point_raw))
|
||||
secret = _fix_secret(_unpack_number(secret_raw))
|
||||
return _pack_number(_raw_curve25519(base_point, secret))
|
||||
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import glob
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from .aes import AES
|
||||
from .aes128 import AES128
|
||||
from .aes256 import AES256
|
||||
@@ -1,271 +0,0 @@
|
||||
# MIT License
|
||||
|
||||
# Copyright (c) 2021 Or Gur Arie
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from .utils import *
|
||||
|
||||
|
||||
class AES:
|
||||
# AES-128 block size
|
||||
block_size = 16
|
||||
# AES-128 encrypts messages with 10 rounds
|
||||
_rounds = 10
|
||||
|
||||
|
||||
# initiate the AES objecy
|
||||
def __init__(self, key):
|
||||
"""
|
||||
Initializes the object with a given key.
|
||||
"""
|
||||
# make sure key length is right
|
||||
assert len(key) == AES.block_size
|
||||
|
||||
# ExpandKey
|
||||
self._round_keys = self._expand_key(key)
|
||||
|
||||
|
||||
# will perform the AES ExpandKey phase
|
||||
def _expand_key(self, master_key):
|
||||
"""
|
||||
Expands and returns a list of key matrices for the given master_key.
|
||||
"""
|
||||
|
||||
# Initialize round keys with raw key material.
|
||||
key_columns = bytes2matrix(master_key)
|
||||
iteration_size = len(master_key) // 4
|
||||
|
||||
# Each iteration has exactly as many columns as the key material.
|
||||
i = 1
|
||||
while len(key_columns) < (self._rounds + 1) * 4:
|
||||
# Copy previous word.
|
||||
word = list(key_columns[-1])
|
||||
|
||||
# Perform schedule_core once every "row".
|
||||
if len(key_columns) % iteration_size == 0:
|
||||
# Circular shift.
|
||||
word.append(word.pop(0))
|
||||
# Map to S-BOX.
|
||||
word = [s_box[b] for b in word]
|
||||
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
|
||||
word[0] ^= r_con[i]
|
||||
i += 1
|
||||
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
|
||||
# Run word through S-box in the fourth iteration when using a
|
||||
# 256-bit key.
|
||||
word = [s_box[b] for b in word]
|
||||
|
||||
# XOR with equivalent word from previous iteration.
|
||||
word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size]))
|
||||
key_columns.append(word)
|
||||
|
||||
# Group key words in 4x4 byte matrices.
|
||||
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
|
||||
|
||||
|
||||
# encrypt a single block of data with AES
|
||||
def _encrypt_block(self, plaintext):
|
||||
"""
|
||||
Encrypts a single block of 16 byte long plaintext.
|
||||
"""
|
||||
# length of a single block
|
||||
assert len(plaintext) == AES.block_size
|
||||
|
||||
# perform on a matrix
|
||||
state = bytes2matrix(plaintext)
|
||||
|
||||
# AddRoundKey
|
||||
add_round_key(state, self._round_keys[0])
|
||||
|
||||
# 9 main rounds
|
||||
for i in range(1, self._rounds):
|
||||
# SubBytes
|
||||
sub_bytes(state)
|
||||
# ShiftRows
|
||||
shift_rows(state)
|
||||
# MixCols
|
||||
mix_columns(state)
|
||||
# AddRoundKey
|
||||
add_round_key(state, self._round_keys[i])
|
||||
|
||||
# last round, w/t AddRoundKey step
|
||||
sub_bytes(state)
|
||||
shift_rows(state)
|
||||
add_round_key(state, self._round_keys[-1])
|
||||
|
||||
# return the encrypted matrix as bytes
|
||||
return matrix2bytes(state)
|
||||
|
||||
|
||||
# decrypt a single block of data with AES
|
||||
def _decrypt_block(self, ciphertext):
|
||||
"""
|
||||
Decrypts a single block of 16 byte long ciphertext.
|
||||
"""
|
||||
# length of a single block
|
||||
assert len(ciphertext) == AES.block_size
|
||||
|
||||
# perform on a matrix
|
||||
state = bytes2matrix(ciphertext)
|
||||
|
||||
# in reverse order, last round is first
|
||||
add_round_key(state, self._round_keys[-1])
|
||||
inv_shift_rows(state)
|
||||
inv_sub_bytes(state)
|
||||
|
||||
for i in range(self._rounds - 1, 0, -1):
|
||||
# nain rounds
|
||||
add_round_key(state, self._round_keys[i])
|
||||
inv_mix_columns(state)
|
||||
inv_shift_rows(state)
|
||||
inv_sub_bytes(state)
|
||||
|
||||
# initial AddRoundKey phase
|
||||
add_round_key(state, self._round_keys[0])
|
||||
|
||||
# return bytes
|
||||
return matrix2bytes(state)
|
||||
|
||||
|
||||
# will encrypt the entire data
|
||||
def encrypt(self, plaintext, iv):
|
||||
"""
|
||||
Encrypts `plaintext` using CBC mode and PKCS#7 padding, with the given
|
||||
initialization vector (iv).
|
||||
"""
|
||||
# iv length must be same as block size
|
||||
assert len(iv) == AES.block_size
|
||||
|
||||
assert len(plaintext) % AES.block_size == 0
|
||||
|
||||
ciphertext_blocks = []
|
||||
|
||||
previous = iv
|
||||
for plaintext_block in split_blocks(plaintext):
|
||||
# in CBC mode every block is XOR'd with the previous block
|
||||
xorred = xor_bytes(plaintext_block, previous)
|
||||
|
||||
# encrypt current block
|
||||
block = self._encrypt_block(xorred)
|
||||
previous = block
|
||||
|
||||
# append to ciphertext
|
||||
ciphertext_blocks.append(block)
|
||||
|
||||
# return as bytes
|
||||
return b''.join(ciphertext_blocks)
|
||||
|
||||
|
||||
# will decrypt the entire data
|
||||
def decrypt(self, ciphertext, iv):
|
||||
"""
|
||||
Decrypts `ciphertext` using CBC mode and PKCS#7 padding, with the given
|
||||
initialization vector (iv).
|
||||
"""
|
||||
# iv length must be same as block size
|
||||
assert len(iv) == AES.block_size
|
||||
|
||||
plaintext_blocks = []
|
||||
|
||||
previous = iv
|
||||
for ciphertext_block in split_blocks(ciphertext):
|
||||
# in CBC mode every block is XOR'd with the previous block
|
||||
xorred = xor_bytes(previous, self._decrypt_block(ciphertext_block))
|
||||
|
||||
# append plaintext
|
||||
plaintext_blocks.append(xorred)
|
||||
previous = ciphertext_block
|
||||
|
||||
return b''.join(plaintext_blocks)
|
||||
|
||||
|
||||
def test():
|
||||
# modules and classes requiered for test only
|
||||
import os
|
||||
class bcolors:
|
||||
OK = '\033[92m' #GREEN
|
||||
WARNING = '\033[93m' #YELLOW
|
||||
FAIL = '\033[91m' #RED
|
||||
RESET = '\033[0m' #RESET COLOR
|
||||
|
||||
# will test AES class by performing an encryption / decryption
|
||||
print("AES Tests")
|
||||
print("=========")
|
||||
|
||||
# generate a secret key and print details
|
||||
key = os.urandom(AES.block_size)
|
||||
_aes = AES(key)
|
||||
print(f"Algorithm: AES-CBC-{AES.block_size*8}")
|
||||
print(f"Secret Key: {key.hex()}")
|
||||
print()
|
||||
|
||||
# test single block encryption / decryption
|
||||
iv = os.urandom(AES.block_size)
|
||||
|
||||
single_block_text = b"SingleBlock Text"
|
||||
print("Single Block Tests")
|
||||
print("------------------")
|
||||
print(f"iv: {iv.hex()}")
|
||||
|
||||
print(f"plain text: '{single_block_text.decode()}'")
|
||||
ciphertext_block = _aes._encrypt_block(single_block_text)
|
||||
plaintext_block = _aes._decrypt_block(ciphertext_block)
|
||||
print(f"Ciphertext Hex: {ciphertext_block.hex()}")
|
||||
print(f"Plaintext: {plaintext_block.decode()}")
|
||||
assert plaintext_block == single_block_text
|
||||
print(bcolors.OK + "Single Block Test Passed Successfully" + bcolors.RESET)
|
||||
print()
|
||||
|
||||
# test a less than a block length phrase
|
||||
iv = os.urandom(AES.block_size)
|
||||
|
||||
short_text = b"Just Text"
|
||||
print("Short Text Tests")
|
||||
print("----------------")
|
||||
print(f"iv: {iv.hex()}")
|
||||
print(f"plain text: '{short_text.decode()}'")
|
||||
ciphertext_short = _aes.encrypt(short_text, iv)
|
||||
plaintext_short = _aes.decrypt(ciphertext_short, iv)
|
||||
print(f"Ciphertext Hex: {ciphertext_short.hex()}")
|
||||
print(f"Plaintext: {plaintext_short.decode()}")
|
||||
assert short_text == plaintext_short
|
||||
print(bcolors.OK + "Short Text Test Passed Successfully" + bcolors.RESET)
|
||||
print()
|
||||
|
||||
# test an arbitrary length phrase
|
||||
iv = os.urandom(AES.block_size)
|
||||
|
||||
text = b"This Text is longer than one block"
|
||||
print("Arbitrary Length Tests")
|
||||
print("----------------------")
|
||||
print(f"iv: {iv.hex()}")
|
||||
print(f"plain text: '{text.decode()}'")
|
||||
ciphertext = _aes.encrypt(text, iv)
|
||||
plaintext = _aes.decrypt(ciphertext, iv)
|
||||
print(f"Ciphertext Hex: {ciphertext.hex()}")
|
||||
print(f"Plaintext: {plaintext.decode()}")
|
||||
assert text == plaintext
|
||||
print(bcolors.OK + "Arbitrary Length Text Test Passed Successfully" + bcolors.RESET)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# test AES class
|
||||
test()
|
||||
@@ -0,0 +1,326 @@
|
||||
# MIT License
|
||||
|
||||
# Copyright (c) 2021 Or Gur Arie
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
## AES lookup tables
|
||||
# resource: https://en.wikipedia.org/wiki/Rijndael_S-box
|
||||
s_box = (
|
||||
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
|
||||
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
|
||||
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
|
||||
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
|
||||
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
|
||||
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
|
||||
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
|
||||
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
|
||||
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
|
||||
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
|
||||
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
|
||||
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
|
||||
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
|
||||
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
|
||||
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
|
||||
)
|
||||
|
||||
inv_s_box = (
|
||||
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
|
||||
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
|
||||
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
|
||||
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
|
||||
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
|
||||
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
|
||||
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
|
||||
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
|
||||
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
|
||||
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
|
||||
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
|
||||
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
|
||||
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
|
||||
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
|
||||
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
|
||||
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
|
||||
)
|
||||
|
||||
|
||||
## AES AddRoundKey
|
||||
# Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants
|
||||
r_con = (
|
||||
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
|
||||
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
|
||||
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
|
||||
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
|
||||
)
|
||||
|
||||
def add_round_key(s, k):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
s[i][j] ^= k[i][j]
|
||||
|
||||
|
||||
## AES SubBytes
|
||||
def sub_bytes(s):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
s[i][j] = s_box[s[i][j]]
|
||||
|
||||
|
||||
def inv_sub_bytes(s):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
s[i][j] = inv_s_box[s[i][j]]
|
||||
|
||||
|
||||
## AES ShiftRows
|
||||
def shift_rows(s):
|
||||
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
|
||||
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
|
||||
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
|
||||
|
||||
|
||||
def inv_shift_rows(s):
|
||||
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
|
||||
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
|
||||
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
|
||||
|
||||
|
||||
## AES MixColumns
|
||||
# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
|
||||
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
|
||||
|
||||
|
||||
def mix_single_column(a):
|
||||
# see Sec 4.1.2 in The Design of Rijndael
|
||||
t = a[0] ^ a[1] ^ a[2] ^ a[3]
|
||||
u = a[0]
|
||||
a[0] ^= t ^ xtime(a[0] ^ a[1])
|
||||
a[1] ^= t ^ xtime(a[1] ^ a[2])
|
||||
a[2] ^= t ^ xtime(a[2] ^ a[3])
|
||||
a[3] ^= t ^ xtime(a[3] ^ u)
|
||||
|
||||
|
||||
def mix_columns(s):
|
||||
for i in range(4):
|
||||
mix_single_column(s[i])
|
||||
|
||||
|
||||
def inv_mix_columns(s):
|
||||
# see Sec 4.1.3 in The Design of Rijndael
|
||||
for i in range(4):
|
||||
u = xtime(xtime(s[i][0] ^ s[i][2]))
|
||||
v = xtime(xtime(s[i][1] ^ s[i][3]))
|
||||
s[i][0] ^= u
|
||||
s[i][1] ^= v
|
||||
s[i][2] ^= u
|
||||
s[i][3] ^= v
|
||||
|
||||
mix_columns(s)
|
||||
|
||||
## AES Bytes
|
||||
def bytes2matrix(text):
|
||||
""" Converts a 16-byte array into a 4x4 matrix. """
|
||||
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
|
||||
|
||||
def matrix2bytes(matrix):
|
||||
""" Converts a 4x4 matrix into a 16-byte array. """
|
||||
return bytes(sum(matrix, []))
|
||||
|
||||
|
||||
def xor_bytes(a, b):
|
||||
""" Returns a new byte array with the elements xor'ed. """
|
||||
return bytes(i^j for i, j in zip(a, b))
|
||||
|
||||
|
||||
def split_blocks(message, block_size=16, require_padding=True):
|
||||
assert len(message) % block_size == 0 or not require_padding
|
||||
return [message[i:i+16] for i in range(0, len(message), block_size)]
|
||||
|
||||
class AES128:
|
||||
# AES-128 block size
|
||||
block_size = 16
|
||||
# AES-128 encrypts messages with 10 rounds
|
||||
_rounds = 10
|
||||
|
||||
|
||||
# initiate the AES objecy
|
||||
def __init__(self, key):
|
||||
"""
|
||||
Initializes the object with a given key.
|
||||
"""
|
||||
# make sure key length is right
|
||||
assert len(key) == AES128.block_size
|
||||
|
||||
# ExpandKey
|
||||
self._round_keys = self._expand_key(key)
|
||||
|
||||
|
||||
# will perform the AES ExpandKey phase
|
||||
def _expand_key(self, master_key):
|
||||
"""
|
||||
Expands and returns a list of key matrices for the given master_key.
|
||||
"""
|
||||
|
||||
# Initialize round keys with raw key material.
|
||||
key_columns = bytes2matrix(master_key)
|
||||
iteration_size = len(master_key) // 4
|
||||
|
||||
# Each iteration has exactly as many columns as the key material.
|
||||
i = 1
|
||||
while len(key_columns) < (self._rounds + 1) * 4:
|
||||
# Copy previous word.
|
||||
word = list(key_columns[-1])
|
||||
|
||||
# Perform schedule_core once every "row".
|
||||
if len(key_columns) % iteration_size == 0:
|
||||
# Circular shift.
|
||||
word.append(word.pop(0))
|
||||
# Map to S-BOX.
|
||||
word = [s_box[b] for b in word]
|
||||
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
|
||||
word[0] ^= r_con[i]
|
||||
i += 1
|
||||
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
|
||||
# Run word through S-box in the fourth iteration when using a
|
||||
# 256-bit key.
|
||||
word = [s_box[b] for b in word]
|
||||
|
||||
# XOR with equivalent word from previous iteration.
|
||||
word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size]))
|
||||
key_columns.append(word)
|
||||
|
||||
# Group key words in 4x4 byte matrices.
|
||||
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
|
||||
|
||||
|
||||
# encrypt a single block of data with AES
|
||||
def _encrypt_block(self, plaintext):
|
||||
"""
|
||||
Encrypts a single block of 16 byte long plaintext.
|
||||
"""
|
||||
# length of a single block
|
||||
assert len(plaintext) == AES128.block_size
|
||||
|
||||
# perform on a matrix
|
||||
state = bytes2matrix(plaintext)
|
||||
|
||||
# AddRoundKey
|
||||
add_round_key(state, self._round_keys[0])
|
||||
|
||||
# 9 main rounds
|
||||
for i in range(1, self._rounds):
|
||||
# SubBytes
|
||||
sub_bytes(state)
|
||||
# ShiftRows
|
||||
shift_rows(state)
|
||||
# MixCols
|
||||
mix_columns(state)
|
||||
# AddRoundKey
|
||||
add_round_key(state, self._round_keys[i])
|
||||
|
||||
# last round, w/t AddRoundKey step
|
||||
sub_bytes(state)
|
||||
shift_rows(state)
|
||||
add_round_key(state, self._round_keys[-1])
|
||||
|
||||
# return the encrypted matrix as bytes
|
||||
return matrix2bytes(state)
|
||||
|
||||
|
||||
# decrypt a single block of data with AES
|
||||
def _decrypt_block(self, ciphertext):
|
||||
"""
|
||||
Decrypts a single block of 16 byte long ciphertext.
|
||||
"""
|
||||
# length of a single block
|
||||
assert len(ciphertext) == AES128.block_size
|
||||
|
||||
# perform on a matrix
|
||||
state = bytes2matrix(ciphertext)
|
||||
|
||||
# in reverse order, last round is first
|
||||
add_round_key(state, self._round_keys[-1])
|
||||
inv_shift_rows(state)
|
||||
inv_sub_bytes(state)
|
||||
|
||||
for i in range(self._rounds - 1, 0, -1):
|
||||
# nain rounds
|
||||
add_round_key(state, self._round_keys[i])
|
||||
inv_mix_columns(state)
|
||||
inv_shift_rows(state)
|
||||
inv_sub_bytes(state)
|
||||
|
||||
# initial AddRoundKey phase
|
||||
add_round_key(state, self._round_keys[0])
|
||||
|
||||
# return bytes
|
||||
return matrix2bytes(state)
|
||||
|
||||
|
||||
# will encrypt the entire data
|
||||
def encrypt(self, plaintext, iv):
|
||||
"""
|
||||
Encrypts `plaintext` using CBC mode and PKCS#7 padding, with the given
|
||||
initialization vector (iv).
|
||||
"""
|
||||
# iv length must be same as block size
|
||||
assert len(iv) == AES128.block_size
|
||||
|
||||
assert len(plaintext) % AES128.block_size == 0
|
||||
|
||||
ciphertext_blocks = []
|
||||
|
||||
previous = iv
|
||||
for plaintext_block in split_blocks(plaintext):
|
||||
# in CBC mode every block is XOR'd with the previous block
|
||||
xorred = xor_bytes(plaintext_block, previous)
|
||||
|
||||
# encrypt current block
|
||||
block = self._encrypt_block(xorred)
|
||||
previous = block
|
||||
|
||||
# append to ciphertext
|
||||
ciphertext_blocks.append(block)
|
||||
|
||||
# return as bytes
|
||||
return b''.join(ciphertext_blocks)
|
||||
|
||||
|
||||
# will decrypt the entire data
|
||||
def decrypt(self, ciphertext, iv):
|
||||
"""
|
||||
Decrypts `ciphertext` using CBC mode and PKCS#7 padding, with the given
|
||||
initialization vector (iv).
|
||||
"""
|
||||
# iv length must be same as block size
|
||||
assert len(iv) == AES128.block_size
|
||||
|
||||
plaintext_blocks = []
|
||||
|
||||
previous = iv
|
||||
for ciphertext_block in split_blocks(ciphertext):
|
||||
# in CBC mode every block is XOR'd with the previous block
|
||||
xorred = xor_bytes(previous, self._decrypt_block(ciphertext_block))
|
||||
|
||||
# append plaintext
|
||||
plaintext_blocks.append(xorred)
|
||||
previous = ciphertext_block
|
||||
|
||||
return b''.join(plaintext_blocks)
|
||||
@@ -1,17 +1,17 @@
|
||||
# MIT License
|
||||
|
||||
# Copyright (c) 2021 Or Gur Arie
|
||||
|
||||
#
|
||||
# Copyright (c) 2024 BoppreH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
@@ -20,12 +20,6 @@
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
'''
|
||||
Utils class for AES encryption / decryption
|
||||
'''
|
||||
|
||||
## AES lookup tables
|
||||
# resource: https://en.wikipedia.org/wiki/Rijndael_S-box
|
||||
s_box = (
|
||||
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
|
||||
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
|
||||
@@ -64,53 +58,33 @@ inv_s_box = (
|
||||
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
|
||||
)
|
||||
|
||||
|
||||
## AES AddRoundKey
|
||||
# Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants
|
||||
r_con = (
|
||||
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
|
||||
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
|
||||
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
|
||||
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
|
||||
)
|
||||
|
||||
def add_round_key(s, k):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
s[i][j] ^= k[i][j]
|
||||
|
||||
|
||||
## AES SubBytes
|
||||
def sub_bytes(s):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
s[i][j] = s_box[s[i][j]]
|
||||
|
||||
|
||||
def inv_sub_bytes(s):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
s[i][j] = inv_s_box[s[i][j]]
|
||||
|
||||
|
||||
## AES ShiftRows
|
||||
def shift_rows(s):
|
||||
s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
|
||||
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
|
||||
s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
|
||||
|
||||
|
||||
def inv_shift_rows(s):
|
||||
s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1]
|
||||
s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
|
||||
s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3]
|
||||
|
||||
def add_round_key(s, k):
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
s[i][j] ^= k[i][j]
|
||||
|
||||
## AES MixColumns
|
||||
# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c
|
||||
xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1)
|
||||
|
||||
|
||||
def mix_single_column(a):
|
||||
# see Sec 4.1.2 in The Design of Rijndael
|
||||
t = a[0] ^ a[1] ^ a[2] ^ a[3]
|
||||
@@ -120,12 +94,10 @@ def mix_single_column(a):
|
||||
a[2] ^= t ^ xtime(a[2] ^ a[3])
|
||||
a[3] ^= t ^ xtime(a[3] ^ u)
|
||||
|
||||
|
||||
def mix_columns(s):
|
||||
for i in range(4):
|
||||
mix_single_column(s[i])
|
||||
|
||||
|
||||
def inv_mix_columns(s):
|
||||
# see Sec 4.1.3 in The Design of Rijndael
|
||||
for i in range(4):
|
||||
@@ -138,22 +110,127 @@ def inv_mix_columns(s):
|
||||
|
||||
mix_columns(s)
|
||||
|
||||
|
||||
## AES Bytes
|
||||
def bytes2matrix(text):
|
||||
""" Converts a 16-byte array into a 4x4 matrix. """
|
||||
return [list(text[i:i+4]) for i in range(0, len(text), 4)]
|
||||
|
||||
def matrix2bytes(matrix):
|
||||
""" Converts a 4x4 matrix into a 16-byte array. """
|
||||
return bytes(sum(matrix, []))
|
||||
r_con = (
|
||||
0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40,
|
||||
0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A,
|
||||
0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A,
|
||||
0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39,
|
||||
)
|
||||
|
||||
|
||||
def xor_bytes(a, b):
|
||||
""" Returns a new byte array with the elements xor'ed. """
|
||||
return bytes(i^j for i, j in zip(a, b))
|
||||
def bytes2matrix(text): return [list(text[i:i+4]) for i in range(0, len(text), 4)]
|
||||
def matrix2bytes(matrix): return bytes(sum(matrix, []))
|
||||
def xor_bytes(a, b): return bytes(i^j for i, j in zip(a, b))
|
||||
|
||||
def inc_bytes(a):
|
||||
out = list(a)
|
||||
for i in reversed(range(len(out))):
|
||||
if out[i] == 0xFF:
|
||||
out[i] = 0
|
||||
else:
|
||||
out[i] += 1
|
||||
break
|
||||
return bytes(out)
|
||||
|
||||
def split_blocks(message, block_size=16, require_padding=True):
|
||||
assert len(message) % block_size == 0 or not require_padding
|
||||
return [message[i:i+16] for i in range(0, len(message), block_size)]
|
||||
assert len(message) % block_size == 0 or not require_padding
|
||||
return [message[i:i+16] for i in range(0, len(message), block_size)]
|
||||
|
||||
class AES256:
|
||||
rounds_by_key_size = {32: 14}
|
||||
def __init__(self, master_key):
|
||||
assert len(master_key) in AES256.rounds_by_key_size
|
||||
self.n_rounds = AES256.rounds_by_key_size[len(master_key)]
|
||||
self._key_matrices = self._expand_key(master_key)
|
||||
|
||||
def _expand_key(self, master_key):
|
||||
# Initialize round keys with raw key material.
|
||||
key_columns = bytes2matrix(master_key)
|
||||
iteration_size = len(master_key) // 4
|
||||
|
||||
i = 1
|
||||
while len(key_columns) < (self.n_rounds + 1) * 4:
|
||||
# Copy previous word.
|
||||
word = list(key_columns[-1])
|
||||
|
||||
# Perform schedule_core once every "row".
|
||||
if len(key_columns) % iteration_size == 0:
|
||||
# Circular shift.
|
||||
word.append(word.pop(0))
|
||||
# Map to S-BOX.
|
||||
word = [s_box[b] for b in word]
|
||||
# XOR with first byte of R-CON, since the others bytes of R-CON are 0.
|
||||
word[0] ^= r_con[i]
|
||||
i += 1
|
||||
elif len(master_key) == 32 and len(key_columns) % iteration_size == 4:
|
||||
# Run word through S-box in the fourth iteration when using a
|
||||
# 256-bit key.
|
||||
word = [s_box[b] for b in word]
|
||||
|
||||
# XOR with equivalent word from previous iteration.
|
||||
word = xor_bytes(word, key_columns[-iteration_size])
|
||||
key_columns.append(word)
|
||||
|
||||
# Group key words in 4x4 byte matrices.
|
||||
return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)]
|
||||
|
||||
def encrypt_block(self, plaintext):
|
||||
assert len(plaintext) == 16
|
||||
|
||||
plain_state = bytes2matrix(plaintext)
|
||||
|
||||
add_round_key(plain_state, self._key_matrices[0])
|
||||
|
||||
for i in range(1, self.n_rounds):
|
||||
sub_bytes(plain_state)
|
||||
shift_rows(plain_state)
|
||||
mix_columns(plain_state)
|
||||
add_round_key(plain_state, self._key_matrices[i])
|
||||
|
||||
sub_bytes(plain_state)
|
||||
shift_rows(plain_state)
|
||||
add_round_key(plain_state, self._key_matrices[-1])
|
||||
|
||||
return matrix2bytes(plain_state)
|
||||
|
||||
def decrypt_block(self, ciphertext):
|
||||
assert len(ciphertext) == 16
|
||||
|
||||
cipher_state = bytes2matrix(ciphertext)
|
||||
|
||||
add_round_key(cipher_state, self._key_matrices[-1])
|
||||
inv_shift_rows(cipher_state)
|
||||
inv_sub_bytes(cipher_state)
|
||||
|
||||
for i in range(self.n_rounds - 1, 0, -1):
|
||||
add_round_key(cipher_state, self._key_matrices[i])
|
||||
inv_mix_columns(cipher_state)
|
||||
inv_shift_rows(cipher_state)
|
||||
inv_sub_bytes(cipher_state)
|
||||
|
||||
add_round_key(cipher_state, self._key_matrices[0])
|
||||
|
||||
return matrix2bytes(cipher_state)
|
||||
|
||||
def encrypt_cbc(self, plaintext, iv):
|
||||
if len(iv) != 16: raise ValueError(f"Invalid IV length: {len(iv)}")
|
||||
blocks = []
|
||||
previous = iv
|
||||
for plaintext_block in split_blocks(plaintext):
|
||||
block = self.encrypt_block(xor_bytes(plaintext_block, previous))
|
||||
blocks.append(block)
|
||||
previous = block
|
||||
|
||||
return b''.join(blocks)
|
||||
|
||||
def decrypt_cbc(self, ciphertext, iv):
|
||||
if len(iv) != 16: raise ValueError(f"Invalid IV length: {len(iv)}")
|
||||
blocks = []
|
||||
previous = iv
|
||||
for ciphertext_block in split_blocks(ciphertext):
|
||||
blocks.append(xor_bytes(previous, self.decrypt_block(ciphertext_block)))
|
||||
previous = ciphertext_block
|
||||
|
||||
return b''.join(blocks)
|
||||
|
||||
__all__ = ["AES256"]
|
||||
+34
-17
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -192,7 +200,7 @@ class Destination:
|
||||
"""
|
||||
:returns: A human-readable representation of the destination including addressable hash and full name.
|
||||
"""
|
||||
return "<"+self.name+"/"+self.hexhash+">"
|
||||
return "<"+self.name+":"+self.hexhash+">"
|
||||
|
||||
def _clean_ratchets(self):
|
||||
if self.ratchets != None:
|
||||
@@ -202,12 +210,16 @@ class Destination:
|
||||
def _persist_ratchets(self):
|
||||
try:
|
||||
with self.ratchet_file_lock:
|
||||
temp_write_path = self.ratchets_path+".tmp"
|
||||
packed_ratchets = umsgpack.packb(self.ratchets)
|
||||
persisted_data = {"signature": self.sign(packed_ratchets), "ratchets": packed_ratchets}
|
||||
ratchets_file = open(self.ratchets_path, "wb")
|
||||
ratchets_file = open(temp_write_path, "wb")
|
||||
ratchets_file.write(umsgpack.packb(persisted_data))
|
||||
ratchets_file.close()
|
||||
if os.path.isfile(self.ratchets_path): os.unlink(self.ratchets_path)
|
||||
os.rename(temp_write_path, self.ratchets_path)
|
||||
except Exception as e:
|
||||
RNS.trace_exception(e)
|
||||
self.ratchets = None
|
||||
self.ratchets_path = None
|
||||
raise OSError("Could not write ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -365,7 +377,7 @@ class Destination:
|
||||
else:
|
||||
self.proof_strategy = proof_strategy
|
||||
|
||||
def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None):
|
||||
def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None, auto_compress = True):
|
||||
"""
|
||||
Registers a request handler.
|
||||
|
||||
@@ -373,17 +385,15 @@ class Destination:
|
||||
:param response_generator: A function or method with the signature *response_generator(path, data, request_id, link_id, remote_identity, requested_at)* to be called. Whatever this funcion returns will be sent as a response to the requester. If the function returns ``None``, no response will be sent.
|
||||
:param allow: One of ``RNS.Destination.ALLOW_NONE``, ``RNS.Destination.ALLOW_ALL`` or ``RNS.Destination.ALLOW_LIST``. If ``RNS.Destination.ALLOW_LIST`` is set, the request handler will only respond to requests for identified peers in the supplied list.
|
||||
:param allowed_list: A list of *bytes-like* :ref:`RNS.Identity<api-identity>` hashes.
|
||||
:param auto_compress: If ``True`` or ``False``, determines whether automatic compression of responses should be carried out. If set to an integer value, responses will only be auto-compressed if under this size in bytes. If omitted, the default compression settings will be followed.
|
||||
:raises: ``ValueError`` if any of the supplied arguments are invalid.
|
||||
"""
|
||||
if path == None or path == "":
|
||||
raise ValueError("Invalid path specified")
|
||||
elif not callable(response_generator):
|
||||
raise ValueError("Invalid response generator specified")
|
||||
elif not allow in Destination.request_policies:
|
||||
raise ValueError("Invalid request policy")
|
||||
if path == None or path == "": raise ValueError("Invalid path specified")
|
||||
elif not callable(response_generator): raise ValueError("Invalid response generator specified")
|
||||
elif not allow in Destination.request_policies: raise ValueError("Invalid request policy")
|
||||
else:
|
||||
path_hash = RNS.Identity.truncated_hash(path.encode("utf-8"))
|
||||
request_handler = [path, response_generator, allow, allowed_list]
|
||||
request_handler = [path, response_generator, allow, allowed_list, auto_compress]
|
||||
self.request_handlers[path_hash] = request_handler
|
||||
|
||||
def deregister_request_handler(self, path):
|
||||
@@ -407,7 +417,8 @@ class Destination:
|
||||
else:
|
||||
plaintext = self.decrypt(packet.data)
|
||||
packet.ratchet_id = self.latest_ratchet_id
|
||||
if plaintext != None:
|
||||
if plaintext == None: return False
|
||||
else:
|
||||
if packet.packet_type == RNS.Packet.DATA:
|
||||
if self.callbacks.packet != None:
|
||||
try:
|
||||
@@ -415,6 +426,8 @@ class Destination:
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing receive callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
return True
|
||||
|
||||
def incoming_link_request(self, data, packet):
|
||||
if self.accept_link_requests:
|
||||
link = RNS.Link.validate_request(self, data, packet)
|
||||
@@ -449,7 +462,12 @@ class Destination:
|
||||
self.ratchets = None
|
||||
self.ratchets_path = None
|
||||
RNS.trace_exception(e)
|
||||
RNS.log(f"The ratchet file located at {ratchets_path} could not be loaded. This could indicate that the ratchet file has become corrupt.", RNS.LOG_CRITICAL)
|
||||
RNS.log(f"You can attempt to manually recover the ratchet file, or simply remove it to have Reticulum recreate it on the next use.", RNS.LOG_CRITICAL)
|
||||
RNS.log(f"If re-initialize this ratchet file, make sure to send an announce for the relevant destination as soon as possible,", RNS.LOG_CRITICAL)
|
||||
RNS.log(f"so that the new ratchet information is synchronized to the network.", RNS.LOG_CRITICAL)
|
||||
raise OSError("Could not read ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
else:
|
||||
RNS.log("No existing ratchet data found, initialising new ratchet file for "+str(self), RNS.LOG_DEBUG)
|
||||
self.ratchets = []
|
||||
@@ -475,7 +493,6 @@ class Destination:
|
||||
self.latest_ratchet_time = 0
|
||||
self._reload_ratchets(ratchets_path)
|
||||
|
||||
# TODO: Remove at some point
|
||||
RNS.log("Ratchets enabled on "+str(self), RNS.LOG_DEBUG)
|
||||
return True
|
||||
|
||||
@@ -629,7 +646,7 @@ class Destination:
|
||||
RNS.log(f"Decryption still failing after ratchet reload. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
raise e
|
||||
|
||||
RNS.log("Decryption succeeded after ratchet reload", RNS.LOG_NOTICE)
|
||||
if decrypted: RNS.log("Decryption succeeded after ratchet reload", RNS.LOG_NOTICE)
|
||||
|
||||
return decrypted
|
||||
|
||||
|
||||
@@ -0,0 +1,672 @@
|
||||
import os
|
||||
import RNS
|
||||
import time
|
||||
import threading
|
||||
import subprocess
|
||||
from .vendor import umsgpack as msgpack
|
||||
|
||||
NAME = 0xFF
|
||||
TRANSPORT_ID = 0xFE
|
||||
INTERFACE_TYPE = 0x00
|
||||
TRANSPORT = 0x01
|
||||
REACHABLE_ON = 0x02
|
||||
LATITUDE = 0x03
|
||||
LONGITUDE = 0x04
|
||||
HEIGHT = 0x05
|
||||
PORT = 0x06
|
||||
IFAC_NETNAME = 0x07
|
||||
IFAC_NETKEY = 0x08
|
||||
FREQUENCY = 0x09
|
||||
BANDWIDTH = 0x0A
|
||||
SPREADINGFACTOR = 0x0B
|
||||
CODINGRATE = 0x0C
|
||||
MODULATION = 0x0D
|
||||
CHANNEL = 0x0E
|
||||
|
||||
APP_NAME = "rnstransport"
|
||||
|
||||
class InterfaceAnnouncer():
|
||||
JOB_INTERVAL = 60
|
||||
DEFAULT_STAMP_VALUE = 14
|
||||
WORKBLOCK_EXPAND_ROUNDS = 20
|
||||
|
||||
DISCOVERABLE_INTERFACE_TYPES = ["BackboneInterface", "TCPServerInterface", "TCPClientInterface",
|
||||
"RNodeInterface", "WeaveInterface", "I2PInterface", "KISSInterface"]
|
||||
|
||||
def __init__(self, owner):
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('LXMF') != None: from LXMF import LXStamper
|
||||
else:
|
||||
RNS.log("Using on-network interface discovery requires the LXMF module to be installed.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install it with the command: pip install lxmf", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.owner = owner
|
||||
self.should_run = False
|
||||
self.job_interval = self.JOB_INTERVAL
|
||||
self.stamper = LXStamper
|
||||
self.stamp_cache = {}
|
||||
|
||||
if self.owner.has_network_identity(): identity = self.owner.network_identity
|
||||
else: identity = self.owner.identity
|
||||
|
||||
self.discovery_destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE,
|
||||
APP_NAME, "discovery", "interface")
|
||||
|
||||
def start(self):
|
||||
if not self.should_run:
|
||||
self.should_run = True
|
||||
threading.Thread(target=self.job, daemon=True).start()
|
||||
|
||||
def stop(self): self.should_run = False
|
||||
|
||||
def job(self):
|
||||
while self.should_run:
|
||||
time.sleep(self.job_interval)
|
||||
try:
|
||||
now = time.time()
|
||||
due_interfaces = [i for i in self.owner.interfaces if i.supports_discovery and i.discoverable and now > (i.last_discovery_announce+i.discovery_announce_interval)]
|
||||
due_interfaces.sort(key=lambda i: now-i.last_discovery_announce, reverse=True)
|
||||
|
||||
if len(due_interfaces) > 0:
|
||||
selected_interface = due_interfaces[0]
|
||||
selected_interface.last_discovery_announce = time.time()
|
||||
RNS.log(f"Preparing interface discovery announce for {selected_interface.name}", RNS.LOG_DEBUG)
|
||||
app_data = self.get_interface_announce_data(selected_interface)
|
||||
if not app_data: RNS.log(f"Could not generate interface discovery announce data for {selected_interface.name}", RNS.LOG_ERROR)
|
||||
else:
|
||||
RNS.log(f"Sending interface discovery announce for {selected_interface.name} with {len(app_data)}B payload", RNS.LOG_DEBUG)
|
||||
self.discovery_destination.announce(app_data=app_data)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while preparing interface discovery announces: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
def sanitize(self, in_str):
|
||||
sanitized = in_str.replace("\n", "")
|
||||
sanitized = sanitized.replace("\r", "")
|
||||
sanitized = sanitized.strip()
|
||||
return sanitized
|
||||
|
||||
def get_interface_announce_data(self, interface):
|
||||
interface_type = type(interface).__name__
|
||||
stamp_value = interface.discovery_stamp_value if interface.discovery_stamp_value else self.DEFAULT_STAMP_VALUE
|
||||
|
||||
if not interface_type in self.DISCOVERABLE_INTERFACE_TYPES: return None
|
||||
else:
|
||||
flags = 0x00
|
||||
info = {INTERFACE_TYPE: interface_type,
|
||||
TRANSPORT: RNS.Reticulum.transport_enabled(),
|
||||
TRANSPORT_ID: RNS.Transport.identity.hash,
|
||||
NAME: self.sanitize(interface.discovery_name),
|
||||
LATITUDE: interface.discovery_latitude,
|
||||
LONGITUDE: interface.discovery_longitude,
|
||||
HEIGHT: interface.discovery_height}
|
||||
|
||||
reachable_on = self.sanitize(interface.reachable_on)
|
||||
if not RNS.vendor.platformutils.is_windows():
|
||||
try:
|
||||
exec_path = os.path.expanduser(reachable_on)
|
||||
if os.path.isfile(exec_path) and os.access(exec_path, os.X_OK):
|
||||
RNS.log(f"Evaluating reachable_on 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")
|
||||
reachable_on = self.sanitize(exec_stdout)
|
||||
|
||||
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
|
||||
|
||||
if interface_type in ["BackboneInterface", "TCPServerInterface"]:
|
||||
info[REACHABLE_ON] = reachable_on
|
||||
info[PORT] = interface.bind_port
|
||||
|
||||
if interface_type == "I2PInterface" and interface.connectable and interface.b32:
|
||||
info[REACHABLE_ON] = interface.b32
|
||||
|
||||
if interface_type == "RNodeInterface":
|
||||
info[FREQUENCY] = interface.frequency
|
||||
info[BANDWIDTH] = interface.bandwidth
|
||||
info[SPREADINGFACTOR] = interface.sf
|
||||
info[CODINGRATE] = interface.cr
|
||||
|
||||
if interface_type == "WeaveInterface":
|
||||
info[FREQUENCY] = interface.discovery_frequency
|
||||
info[BANDWIDTH] = interface.discovery_bandwidth
|
||||
info[CHANNEL] = interface.discovery_channel
|
||||
info[MODULATION] = interface.discovery_modulation
|
||||
|
||||
if interface_type == "KISSInterface" or (interface_type == "TCPClientInterface" and interface.kiss_framing):
|
||||
info[INTERFACE_TYPE] = "KISSInterface"
|
||||
info[FREQUENCY] = interface.discovery_frequency
|
||||
info[BANDWIDTH] = interface.discovery_bandwidth
|
||||
info[MODULATION] = self.sanitize(interface.discovery_modulation)
|
||||
|
||||
if interface.discovery_publish_ifac == True:
|
||||
info[IFAC_NETNAME] = self.sanitize(interface.ifac_netname)
|
||||
info[IFAC_NETKEY] = self.sanitize(interface.ifac_netkey)
|
||||
|
||||
packed = msgpack.packb(info)
|
||||
infohash = RNS.Identity.full_hash(packed)
|
||||
|
||||
if infohash in self.stamp_cache: stamp = self.stamp_cache[infohash]
|
||||
else: stamp, v = self.stamper.generate_stamp(infohash, stamp_cost=stamp_value, expand_rounds=self.WORKBLOCK_EXPAND_ROUNDS)
|
||||
if not stamp: return None
|
||||
else: self.stamp_cache[infohash] = stamp
|
||||
|
||||
if interface.discovery_encrypt:
|
||||
flags |= InterfaceAnnounceHandler.FLAG_ENCRYPTED
|
||||
if not self.owner.has_network_identity():
|
||||
RNS.log(f"Discovery encryption requested for {interface}, but no network identity configured. Aborting discovery announce.", RNS.LOG_ERROR)
|
||||
return None
|
||||
|
||||
else: payload = self.owner.network_identity.encrypt(packed+stamp)
|
||||
|
||||
else: payload = packed+stamp
|
||||
|
||||
return bytes([flags])+payload
|
||||
|
||||
class InterfaceAnnounceHandler:
|
||||
FLAG_SIGNED = 0b00000001
|
||||
FLAG_ENCRYPTED = 0b00000010
|
||||
|
||||
def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None):
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('LXMF') != None: from LXMF import LXStamper
|
||||
else:
|
||||
RNS.log("Using on-network interface discovery requires the LXMF module to be installed.", RNS.LOG_CRITICAL)
|
||||
RNS.log("You can install it with the command: pip install lxmf", RNS.LOG_CRITICAL)
|
||||
RNS.panic()
|
||||
|
||||
self.aspect_filter = APP_NAME+".discovery.interface"
|
||||
self.required_value = required_value
|
||||
self.callback = callback
|
||||
self.stamper = LXStamper
|
||||
|
||||
def received_announce(self, destination_hash, announced_identity, app_data):
|
||||
try:
|
||||
discovery_sources = RNS.Reticulum.interface_discovery_sources()
|
||||
if discovery_sources and not announced_identity.hash in discovery_sources:
|
||||
RNS.log(f"Interface discovered from non-authorized network identity {RNS.prettyhexrep(announced_identity.hash)}, ignoring", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
if app_data and len(app_data) > self.stamper.STAMP_SIZE+1:
|
||||
flags = app_data[0]
|
||||
app_data = app_data[1:]
|
||||
signed = flags & self.FLAG_SIGNED
|
||||
encrypted = flags & self.FLAG_ENCRYPTED
|
||||
|
||||
if encrypted:
|
||||
if not RNS.Transport.has_network_identity(): return
|
||||
app_data = RNS.Transport.network_identity.decrypt(app_data)
|
||||
if not app_data: return
|
||||
|
||||
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)
|
||||
|
||||
if not valid:
|
||||
RNS.log(f"Ignored discovered interface with invalid stamp", RNS.LOG_DEBUG)
|
||||
return
|
||||
|
||||
if value < self.required_value: RNS.log(f"Ignored discovered interface with stamp value {value}", RNS.LOG_DEBUG)
|
||||
else:
|
||||
info = None
|
||||
unpacked = msgpack.unpackb(packed)
|
||||
if INTERFACE_TYPE in unpacked:
|
||||
interface_type = unpacked[INTERFACE_TYPE]
|
||||
info = {"type": interface_type,
|
||||
"transport": unpacked[TRANSPORT],
|
||||
"name": unpacked[NAME] or f"Discovered {interface_type}",
|
||||
"received": time.time(),
|
||||
"stamp": stamp,
|
||||
"value": value,
|
||||
"transport_id": RNS.hexrep(unpacked[TRANSPORT_ID], delimit=False),
|
||||
"network_id": RNS.hexrep(announced_identity.hash, delimit=False),
|
||||
"hops": RNS.Transport.hops_to(destination_hash),
|
||||
"latitude": unpacked[LATITUDE],
|
||||
"longitude": unpacked[LONGITUDE],
|
||||
"height": unpacked[HEIGHT]}
|
||||
|
||||
if IFAC_NETNAME in unpacked: info["ifac_netname"] = unpacked[IFAC_NETNAME]
|
||||
if IFAC_NETKEY in unpacked: info["ifac_netkey"] = unpacked[IFAC_NETKEY]
|
||||
|
||||
if interface_type in ["BackboneInterface", "TCPServerInterface"]:
|
||||
backbone_support = not RNS.vendor.platformutils.is_windows()
|
||||
info["reachable_on"] = unpacked[REACHABLE_ON]
|
||||
info["port"] = unpacked[PORT]
|
||||
connection_interface = "BackboneInterface" if backbone_support else "TCPClientInterface"
|
||||
remote_str = "remote" if backbone_support else "target_host"
|
||||
cfg_name = info["name"]
|
||||
cfg_remote = info["reachable_on"]
|
||||
cfg_port = info["port"]
|
||||
cfg_identity = info["transport_id"]
|
||||
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
|
||||
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
|
||||
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
|
||||
info["config_entry"] = f"[[{cfg_name}]]\n type = {connection_interface}\n enabled = yes\n {remote_str} = {cfg_remote}\n target_port = {cfg_port}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}"
|
||||
|
||||
if interface_type == "I2PInterface":
|
||||
info["reachable_on"] = unpacked[REACHABLE_ON]
|
||||
cfg_name = info["name"]
|
||||
cfg_remote = info["reachable_on"]
|
||||
cfg_identity = info["transport_id"]
|
||||
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
|
||||
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
|
||||
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
|
||||
info["config_entry"] = f"[[{cfg_name}]]\n type = I2PInterface\n enabled = yes\n peers = {cfg_remote}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}"
|
||||
|
||||
if interface_type == "RNodeInterface":
|
||||
info["frequency"] = unpacked[FREQUENCY]
|
||||
info["bandwidth"] = unpacked[BANDWIDTH]
|
||||
info["sf"] = unpacked[SPREADINGFACTOR]
|
||||
info["cr"] = unpacked[CODINGRATE]
|
||||
cfg_name = info["name"]
|
||||
cfg_frequency = info["frequency"]
|
||||
cfg_bandwidth = info["bandwidth"]
|
||||
cfg_sf = info["sf"]
|
||||
cfg_cr = info["cr"]
|
||||
cfg_identity = info["transport_id"]
|
||||
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
|
||||
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
|
||||
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
|
||||
info["config_entry"] = f"[[{cfg_name}]]\n type = RNodeInterface\n enabled = yes\n port = \n frequency = {cfg_frequency}\n bandwidth = {cfg_bandwidth}\n spreadingfactor = {cfg_sf}\n codingrate = {cfg_cr}\n txpower = {cfg_netname_str}{cfg_netkey_str}"
|
||||
|
||||
if interface_type == "WeaveInterface":
|
||||
info["frequency"] = unpacked[FREQUENCY]
|
||||
info["bandwidth"] = unpacked[BANDWIDTH]
|
||||
info["channel"] = unpacked[CHANNEL]
|
||||
info["modulation"] = unpacked[MODULATION]
|
||||
cfg_name = info["name"]
|
||||
cfg_identity = info["transport_id"]
|
||||
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
|
||||
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
|
||||
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
|
||||
info["config_entry"] = f"[[{cfg_name}]]\n type = WeaveInterface\n enabled = yes\n port = {cfg_netname_str}{cfg_netkey_str}"
|
||||
|
||||
if interface_type == "KISSInterface":
|
||||
info["frequency"] = unpacked[FREQUENCY]
|
||||
info["bandwidth"] = unpacked[BANDWIDTH]
|
||||
info["modulation"] = unpacked[MODULATION]
|
||||
cfg_name = info["name"]
|
||||
cfg_frequency = info["frequency"]
|
||||
cfg_bandwidth = info["bandwidth"]
|
||||
cfg_modulation = info["modulation"]
|
||||
cfg_identity = info["transport_id"]
|
||||
cfg_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
cfg_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
cfg_netname_str = f"\n network_name = {cfg_netname}" if cfg_netname else ""
|
||||
cfg_netkey_str = f"\n passphrase = {cfg_netkey}" if cfg_netkey else ""
|
||||
cfg_identity_str = f"\n transport_identity = {cfg_identity}"
|
||||
info["config_entry"] = f"[[{cfg_name}]]\n type = KISSInterface\n enabled = yes\n port = \n # Frequency: {cfg_frequency}\n # Bandwidth: {cfg_bandwidth}\n # Modulation: {cfg_modulation}{cfg_identity_str}{cfg_netname_str}{cfg_netkey_str}"
|
||||
|
||||
discovery_hash_material = info["transport_id"]+info["name"]
|
||||
info["discovery_hash"] = RNS.Identity.full_hash(discovery_hash_material.encode("utf-8"))
|
||||
|
||||
if self.callback and callable(self.callback): self.callback(info)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while trying to decode discovered interface. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
class InterfaceDiscovery():
|
||||
THRESHOLD_UNKNOWN = 24*60*60
|
||||
THRESHOLD_STALE = 3*24*60*60
|
||||
THRESHOLD_REMOVE = 7*24*60*60
|
||||
|
||||
MONITOR_INTERVAL = 5
|
||||
DETACH_THRESHOLD = 12
|
||||
|
||||
STATUS_STALE = 0
|
||||
STATUS_UNKNOWN = 100
|
||||
STATUS_AVAILABLE = 1000
|
||||
STATUS_CODE_MAP = {"available": STATUS_AVAILABLE, "unknown": STATUS_UNKNOWN, "stale": STATUS_STALE}
|
||||
AUTOCONNECT_TYPES = ["BackboneInterface", "TCPServerInterface"]
|
||||
|
||||
def __init__(self, required_value=InterfaceAnnouncer.DEFAULT_STAMP_VALUE, callback=None, discover_interfaces=True):
|
||||
if not required_value: required_value = InterfaceAnnouncer.DEFAULT_STAMP_VALUE
|
||||
|
||||
self.required_value = required_value
|
||||
self.discovery_callback = callback
|
||||
self.rns_instance = RNS.Reticulum.get_instance()
|
||||
self.monitored_interfaces = []
|
||||
self.monitoring_autoconnects = False
|
||||
self.monitor_interval = self.MONITOR_INTERVAL
|
||||
self.detach_threshold = self.DETACH_THRESHOLD
|
||||
|
||||
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")
|
||||
if not os.path.isdir(self.storagepath): os.makedirs(self.storagepath)
|
||||
|
||||
if discover_interfaces:
|
||||
self.handler = InterfaceAnnounceHandler(callback=self.interface_discovered, required_value=self.required_value)
|
||||
RNS.Transport.register_announce_handler(self.handler)
|
||||
threading.Thread(target=self.connect_discovered, daemon=True).start()
|
||||
|
||||
def list_discovered_interfaces(self):
|
||||
now = time.time()
|
||||
discovered_interfaces = []
|
||||
discovery_sources = RNS.Reticulum.interface_discovery_sources()
|
||||
for filename in os.listdir(self.storagepath):
|
||||
try:
|
||||
filepath = os.path.join(self.storagepath, filename)
|
||||
with open(filepath, "rb") as f: info = msgpack.unpackb(f.read())
|
||||
should_remove = False
|
||||
heard_delta = now-info["last_heard"]
|
||||
|
||||
if heard_delta > self.THRESHOLD_REMOVE: 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
|
||||
|
||||
if should_remove:
|
||||
os.unlink(filepath)
|
||||
continue
|
||||
|
||||
else:
|
||||
if heard_delta > self.THRESHOLD_STALE: info["status"] = "stale"
|
||||
elif heard_delta > self.THRESHOLD_UNKNOWN: info["status"] = "unknown"
|
||||
else: info["status"] = "available"
|
||||
|
||||
info["status_code"] = self.STATUS_CODE_MAP[info["status"]]
|
||||
discovered_interfaces.append(info)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while loading discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.log(f"The interface data file {os.path.join(self.storagepath, filename)} may be corrupt", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
discovered_interfaces.sort(key=lambda info: (info["status_code"], info["value"], info["last_heard"]), reverse=True)
|
||||
return discovered_interfaces
|
||||
|
||||
def interface_discovered(self, info):
|
||||
try:
|
||||
name = info["name"]
|
||||
value = info["value"]
|
||||
interface_type = info["type"]
|
||||
discovery_hash = info["discovery_hash"]
|
||||
hops = info["hops"]; ms = "" if hops == 1 else "s"
|
||||
filename = RNS.hexrep(discovery_hash, delimit=False)
|
||||
filepath = os.path.join(self.storagepath, filename)
|
||||
RNS.log(f"Discovered {interface_type} {hops} hop{ms} away with stamp value {value}: {name}", RNS.LOG_DEBUG)
|
||||
if not os.path.isfile(filepath):
|
||||
try:
|
||||
with open(filepath, "wb") as f:
|
||||
info["discovered"] = info["received"]
|
||||
info["last_heard"] = info["received"]
|
||||
info["heard_count"] = 0
|
||||
f.write(msgpack.packb(info))
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
return
|
||||
|
||||
else:
|
||||
discovered = None
|
||||
heard_count = None
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
last_info = msgpack.unpackb(f.read())
|
||||
discovered = last_info["discovered"]
|
||||
heard_count = last_info["heard_count"]
|
||||
|
||||
if discovered == None: discovered = info["discovered"]
|
||||
if heard_count == None: heard_count = 0
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
info["discovered"] = discovered
|
||||
info["last_heard"] = info["received"]
|
||||
info["heard_count"] = heard_count+1
|
||||
f.write(msgpack.packb(info))
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while persisting discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error processing discovered interface data: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
return
|
||||
|
||||
self.autoconnect(info)
|
||||
|
||||
try:
|
||||
if self.discovery_callback and callable(self.discovery_callback): self.discovery_callback(info)
|
||||
except Exception as e: RNS.log(f"Error while processing external interface discovery callback: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def monitor_interface(self, interface):
|
||||
if not interface in self.monitored_interfaces:
|
||||
self.monitored_interfaces.append(interface)
|
||||
|
||||
if not self.monitoring_autoconnects:
|
||||
self.monitoring_autoconnects = True
|
||||
threading.Thread(target=self.__monitor_job, daemon=True).start()
|
||||
|
||||
def __monitor_job(self):
|
||||
while self.monitoring_autoconnects:
|
||||
time.sleep(self.monitor_interval)
|
||||
detached_interfaces = []
|
||||
online_interfaces = 0
|
||||
for interface in self.monitored_interfaces:
|
||||
try:
|
||||
if interface.online:
|
||||
online_interfaces += 1
|
||||
if hasattr(interface, "autoconnect_down") and interface.autoconnect_down != None:
|
||||
RNS.log(f"Auto-discovered interface {interface} reconnected")
|
||||
interface.autoconnect_down = None
|
||||
|
||||
else:
|
||||
if not hasattr(interface, "autoconnect_down") or interface.autoconnect_down == None:
|
||||
RNS.log(f"Auto-discovered interface {interface} disconnected", RNS.LOG_DEBUG)
|
||||
interface.autoconnect_down = time.time()
|
||||
|
||||
else:
|
||||
down_for = time.time()-interface.autoconnect_down
|
||||
if down_for >= self.detach_threshold:
|
||||
RNS.log(f"Auto-discovered interface {interface} has been down for {RNS.prettytime(down_for)}, detaching", RNS.LOG_DEBUG)
|
||||
detached_interfaces.append(interface)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while checking auto-connected interface state for {interface}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
if online_interfaces >= RNS.Reticulum.max_autoconnected_interfaces():
|
||||
for interface in RNS.Transport.interfaces:
|
||||
if hasattr(interface, "bootstrap_only") and interface.bootstrap_only == True:
|
||||
RNS.log(f"Tearing down bootstrap-only {interface} since target connected auto-discovered interface count has been reached", RNS.LOG_INFO)
|
||||
if not interface in detached_interfaces: detached_interfaces.append(interface)
|
||||
|
||||
if online_interfaces == 0:
|
||||
if self.bootstrap_interface_count() == 0:
|
||||
RNS.log(f"No auto-discovered interfaces connected, re-enabling bootstrap interfaces", RNS.LOG_NOTICE)
|
||||
for config in RNS.Reticulum.get_instance().bootstrap_configs:
|
||||
RNS.Reticulum.get_instance()._synthesize_interface(config, config["name"])
|
||||
|
||||
for interface in detached_interfaces:
|
||||
try: self.teardown_interface(interface)
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while de-registering auto-connected interface from transport: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def teardown_interface(self, interface):
|
||||
interface.detach()
|
||||
if interface in RNS.Transport.interfaces: RNS.Transport.interfaces.remove(interface)
|
||||
if interface in self.monitored_interfaces: self.monitored_interfaces.remove(interface)
|
||||
|
||||
def autoconnect_count(self):
|
||||
return len([i for i in RNS.Transport.interfaces if hasattr(i, "autoconnect_hash")])
|
||||
|
||||
def bootstrap_interface_count(self):
|
||||
return len([i for i in RNS.Transport.interfaces if hasattr(i, "bootstrap_only") and i.bootstrap_only == True])
|
||||
|
||||
def connect_discovered(self):
|
||||
if RNS.Reticulum.should_autoconnect_discovered_interfaces():
|
||||
try:
|
||||
discovered_interfaces = self.list_discovered_interfaces()
|
||||
for info in discovered_interfaces:
|
||||
if self.autoconnect_count() >= RNS.Reticulum.max_autoconnected_interfaces(): break
|
||||
self.autoconnect(info)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while reconnecting discovered interfaces: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def autoconnect(self, info):
|
||||
try:
|
||||
if RNS.Reticulum.should_autoconnect_discovered_interfaces():
|
||||
autoconnected_count = self.autoconnect_count()
|
||||
if autoconnected_count < RNS.Reticulum.max_autoconnected_interfaces():
|
||||
interface_type = info["type"]
|
||||
if interface_type in self.AUTOCONNECT_TYPES:
|
||||
endpoint_specifier = ""
|
||||
if "reachable_on" in info: endpoint_specifier += str(info["reachable_on"])
|
||||
if "port" in info: endpoint_specifier += ":"+str(info["port"])
|
||||
endpoint_hash = RNS.Identity.full_hash(endpoint_specifier.encode("utf-8"))
|
||||
exists = False
|
||||
for interface in RNS.Transport.interfaces:
|
||||
if hasattr(interface, "autoconnect_hash") and interface.autoconnect_hash == endpoint_hash:
|
||||
exists = True
|
||||
break
|
||||
|
||||
else:
|
||||
dest_match = "reachable_on" in info and hasattr(interface, "target_ip") and interface.target_ip == info["reachable_on"]
|
||||
port_match = not "port" in info or (hasattr(interface, "target_port") and "port" in info and interface.target_port == info["port"])
|
||||
b32d_match = "reachable_on" in info and hasattr(interface, "b32") and interface.b32 == info["reachable_on"]
|
||||
|
||||
if (dest_match and port_match) or b32d_match:
|
||||
exists = True
|
||||
break
|
||||
|
||||
if exists: RNS.log(f"Discovered {interface_type} already exists, not auto-connecting", RNS.LOG_DEBUG)
|
||||
else:
|
||||
if interface_type == "TCPClientInterface":
|
||||
RNS.log(f"Your operating system does not support the Backbone interface type, and must degrade to using TCPClientInterface instead", RNS.LOG_WARNING)
|
||||
RNS.log(f"Auto-connecting discovered TCPClient interfaces is not yet implemented, aborting auto-connect", RNS.LOG_WARNING)
|
||||
RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
if interface_type == "I2PInterface":
|
||||
RNS.log(f"Auto-connecting discovered I2P interfaces is not yet implemented, aborting auto-connect", RNS.LOG_WARNING)
|
||||
RNS.log(f"You can obtain the configuration entry and add this interface manually instead using rnstatus -D", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
interface_name = info["name"]
|
||||
RNS.log(f"Auto-connecting discovered {interface_type} {interface_name}")
|
||||
config_entry = info["config_entry"]
|
||||
interface_config = {}
|
||||
interface_config["name"] = f"{interface_name}"
|
||||
ifac_netname = info["ifac_netname"] if "ifac_netname" in info else None
|
||||
ifac_netkey = info["ifac_netkey"] if "ifac_netkey" in info else None
|
||||
interface = None
|
||||
|
||||
if interface_type == "BackboneInterface":
|
||||
from RNS.Interfaces import BackboneInterface
|
||||
interface_config["target_host"] = info["reachable_on"]
|
||||
interface_config["target_port"] = info["port"]
|
||||
interface = BackboneInterface.BackboneClientInterface(RNS.Transport, interface_config)
|
||||
|
||||
if interface:
|
||||
interface.autoconnect_hash = endpoint_hash
|
||||
interface.autoconnect_source = info["network_id"]
|
||||
RNS.Reticulum.get_instance()._add_interface(interface, ifac_netname=ifac_netname, ifac_netkey=ifac_netkey, configured_bitrate=5E6)
|
||||
self.monitor_interface(interface)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while auto-connecting discovered interface: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
class BlackholeUpdater():
|
||||
INITIAL_WAIT = 20
|
||||
JOB_INTERVAL = 60
|
||||
UPDATE_INTERVAL = 1*60*60
|
||||
SOURCE_TIMEOUT = 25
|
||||
|
||||
def __init__(self):
|
||||
self.last_updates = {}
|
||||
self.should_run = False
|
||||
self.job_interval = self.JOB_INTERVAL
|
||||
self.update_lock = threading.Lock()
|
||||
|
||||
def start(self):
|
||||
if not self.should_run:
|
||||
source_count = len(RNS.Reticulum.blackhole_sources())
|
||||
ms = "" if source_count == 1 else "s"
|
||||
RNS.log(f"Starting blackhole updater with {source_count} source{ms}", RNS.LOG_DEBUG)
|
||||
self.should_run = True
|
||||
threading.Thread(target=self.job, daemon=True).start()
|
||||
|
||||
def stop(self): self.should_run = False
|
||||
|
||||
def update_link_established(self, link):
|
||||
remote_identity = link.get_remote_identity()
|
||||
RNS.log(f"Link established for blackhole list update from {RNS.prettyhexrep(remote_identity.hash)}", RNS.LOG_DEBUG)
|
||||
receipt = link.request("/list")
|
||||
while not receipt.concluded(): time.sleep(0.2)
|
||||
response = receipt.get_response()
|
||||
link.teardown()
|
||||
|
||||
if type(response) == dict: blackhole_list = response
|
||||
else: blackhole_list = None
|
||||
|
||||
if blackhole_list:
|
||||
added = 0
|
||||
for identity_hash in blackhole_list:
|
||||
entry = blackhole_list[identity_hash]
|
||||
if not identity_hash in RNS.Transport.blackholed_identities:
|
||||
RNS.Transport.blackholed_identities[identity_hash] = entry
|
||||
added += 1
|
||||
|
||||
if added > 0:
|
||||
spec = "identity" if added == 1 else "identities"
|
||||
RNS.log(f"Added {added} blackholed {spec} from {RNS.prettyhexrep(remote_identity.hash)}", RNS.LOG_DEBUG)
|
||||
|
||||
try:
|
||||
sourcelistpath = os.path.join(RNS.Reticulum.blackholepath, RNS.hexrep(remote_identity.hash, delimit=False))
|
||||
tmppath = f"{sourcelistpath}.tmp"
|
||||
with open(tmppath, "wb") as f: f.write(msgpack.packb(blackhole_list))
|
||||
if os.path.isfile(sourcelistpath): os.unlink(sourcelistpath)
|
||||
os.rename(tmppath, sourcelistpath)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while persisting blackhole list from {RNS.prettyhexrep(remote_identity.hash)}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
RNS.log(f"Blackhole list update from {RNS.prettyhexrep(remote_identity.hash)} completed", RNS.LOG_DEBUG)
|
||||
|
||||
def job(self):
|
||||
time.sleep(self.INITIAL_WAIT)
|
||||
while self.should_run:
|
||||
try:
|
||||
now = time.time()
|
||||
for identity_hash in RNS.Reticulum.blackhole_sources():
|
||||
if identity_hash in self.last_updates: last_update = self.last_updates[identity_hash]
|
||||
else: last_update = 0
|
||||
|
||||
if now > last_update+self.UPDATE_INTERVAL:
|
||||
try:
|
||||
destination_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.info.blackhole", identity_hash)
|
||||
RNS.log(f"Attempting blackhole list update from {RNS.prettyhexrep(identity_hash)}...", RNS.LOG_DEBUG)
|
||||
if not RNS.Transport.await_path(destination_hash): RNS.log(f"No path available for blackhole list update from {RNS.prettyhexrep(identity_hash)}, retrying later", RNS.LOG_VERBOSE)
|
||||
else:
|
||||
remote_identity = RNS.Identity.recall(destination_hash)
|
||||
destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "info", "blackhole")
|
||||
RNS.Link(destination, established_callback=self.update_link_established)
|
||||
self.last_updates[identity_hash] = time.time()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while establishing link for blackhole list update from {RNS.prettyhexrep(identity_hash)}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Error in blackhole list updater job: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
time.sleep(self.job_interval)
|
||||
+81
-43
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -71,13 +79,16 @@ class Identity:
|
||||
HASHLENGTH = 256 # In bits
|
||||
SIGLENGTH = KEYSIZE # In bits
|
||||
|
||||
NAME_HASH_LENGTH = 80
|
||||
TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH
|
||||
NAME_HASH_LENGTH = 80
|
||||
TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH
|
||||
"""
|
||||
Constant specifying the truncated hash length (in bits) used by Reticulum
|
||||
for addressable hashes and other purposes. Non-configurable.
|
||||
"""
|
||||
|
||||
DERIVED_KEY_LENGTH = 512//8
|
||||
DERIVED_KEY_LENGTH_LEGACY = 256//8
|
||||
|
||||
# Storage
|
||||
known_destinations = {}
|
||||
known_ratchets = {}
|
||||
@@ -93,29 +104,47 @@ class Identity:
|
||||
|
||||
|
||||
@staticmethod
|
||||
def recall(destination_hash):
|
||||
def recall(target_hash, from_identity_hash=False):
|
||||
"""
|
||||
Recall identity for a destination hash.
|
||||
Recall identity for a destination or identity hash. By default, this function
|
||||
will return the identity associated with a given *destination* hash. As an
|
||||
example, if you know the ``lxmf.delivery`` destination hash of an endpoint,
|
||||
this function will return the associated underlying identity. You can also
|
||||
search for an identity from a known *identity hash*, by setting the
|
||||
``from_identity_hash`` argument.
|
||||
|
||||
:param destination_hash: Destination hash as *bytes*.
|
||||
:param target_hash: Destination or identity hash as *bytes*.
|
||||
:param from_identity_hash: Whether to search based on identity hash instead of destination hash as *bool*.
|
||||
:returns: An :ref:`RNS.Identity<api-identity>` instance that can be used to create an outgoing :ref:`RNS.Destination<api-destination>`, or *None* if the destination is unknown.
|
||||
"""
|
||||
if destination_hash in Identity.known_destinations:
|
||||
identity_data = Identity.known_destinations[destination_hash]
|
||||
identity = Identity(create_keys=False)
|
||||
identity.load_public_key(identity_data[2])
|
||||
identity.app_data = identity_data[3]
|
||||
return identity
|
||||
else:
|
||||
for registered_destination in RNS.Transport.destinations:
|
||||
if destination_hash == registered_destination.hash:
|
||||
if from_identity_hash:
|
||||
for destination_hash in Identity.known_destinations:
|
||||
if target_hash == Identity.truncated_hash(Identity.known_destinations[destination_hash][2]):
|
||||
identity_data = Identity.known_destinations[destination_hash]
|
||||
identity = Identity(create_keys=False)
|
||||
identity.load_public_key(registered_destination.identity.get_public_key())
|
||||
identity.app_data = None
|
||||
identity.load_public_key(identity_data[2])
|
||||
identity.app_data = identity_data[3]
|
||||
return identity
|
||||
|
||||
return None
|
||||
|
||||
else:
|
||||
if target_hash in Identity.known_destinations:
|
||||
identity_data = Identity.known_destinations[target_hash]
|
||||
identity = Identity(create_keys=False)
|
||||
identity.load_public_key(identity_data[2])
|
||||
identity.app_data = identity_data[3]
|
||||
return identity
|
||||
else:
|
||||
for registered_destination in RNS.Transport.destinations:
|
||||
if target_hash == registered_destination.hash:
|
||||
identity = Identity(create_keys=False)
|
||||
identity.load_public_key(registered_destination.identity.get_public_key())
|
||||
identity.app_data = None
|
||||
return identity
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def recall_app_data(destination_hash):
|
||||
"""
|
||||
@@ -310,12 +339,19 @@ class Identity:
|
||||
for filename in os.listdir(ratchetdir):
|
||||
try:
|
||||
expired = False
|
||||
corrupted = False
|
||||
with open(f"{ratchetdir}/{filename}", "rb") as rf:
|
||||
ratchet_data = umsgpack.unpackb(rf.read())
|
||||
if now > ratchet_data["received"]+Identity.RATCHET_EXPIRY:
|
||||
expired = True
|
||||
# TODO: Remove individual ratchet file if corrupt
|
||||
try:
|
||||
ratchet_data = umsgpack.unpackb(rf.read())
|
||||
if now > ratchet_data["received"]+Identity.RATCHET_EXPIRY:
|
||||
expired = True
|
||||
|
||||
if expired:
|
||||
except Exception as e:
|
||||
RNS.log(f"Corrupted ratchet data while reading {ratchetdir}/{filename}, removing file", RNS.LOG_ERROR)
|
||||
corrupted = True
|
||||
|
||||
if expired or corrupted:
|
||||
os.unlink(f"{ratchetdir}/{filename}")
|
||||
|
||||
except Exception as e:
|
||||
@@ -394,6 +430,11 @@ class Identity:
|
||||
announced_identity = Identity(create_keys=False)
|
||||
announced_identity.load_public_key(public_key)
|
||||
|
||||
if len(RNS.Transport.blackholed_identities) > 0:
|
||||
if announced_identity.hash in RNS.Transport.blackholed_identities:
|
||||
RNS.log(f"Invalidated and dropped announce from blackholed identity {RNS.prettyhexrep(announced_identity.hash)}", RNS.LOG_EXTREME)
|
||||
return False
|
||||
|
||||
if announced_identity.pub != None and announced_identity.validate(signature, signed_data):
|
||||
if only_validate_signature:
|
||||
del announced_identity
|
||||
@@ -644,7 +685,7 @@ class Identity:
|
||||
shared_key = ephemeral_key.exchange(target_public_key)
|
||||
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
length=Identity.DERIVED_KEY_LENGTH,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
@@ -658,6 +699,16 @@ class Identity:
|
||||
else:
|
||||
raise KeyError("Encryption failed because identity does not hold a public key")
|
||||
|
||||
def __decrypt(self, shared_key, ciphertext):
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=Identity.DERIVED_KEY_LENGTH,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context())
|
||||
|
||||
token = Token(derived_key)
|
||||
plaintext = token.decrypt(ciphertext)
|
||||
return plaintext
|
||||
|
||||
def decrypt(self, ciphertext_token, ratchets=None, enforce_ratchets=False, ratchet_id_receiver=None):
|
||||
"""
|
||||
@@ -667,6 +718,7 @@ class Identity:
|
||||
:returns: Plaintext as *bytes*, or *None* if decryption fails.
|
||||
:raises: *KeyError* if the instance does not hold a private key.
|
||||
"""
|
||||
|
||||
if self.prv != None:
|
||||
if len(ciphertext_token) > Identity.KEYSIZE//8//2:
|
||||
plaintext = None
|
||||
@@ -681,15 +733,7 @@ class Identity:
|
||||
ratchet_prv = X25519PrivateKey.from_private_bytes(ratchet)
|
||||
ratchet_id = Identity._get_ratchet_id(ratchet_prv.public_key().public_bytes())
|
||||
shared_key = ratchet_prv.exchange(peer_pub)
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
)
|
||||
|
||||
token = Token(derived_key)
|
||||
plaintext = token.decrypt(ciphertext)
|
||||
plaintext = self.__decrypt(shared_key, ciphertext)
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = ratchet_id
|
||||
|
||||
@@ -706,15 +750,8 @@ class Identity:
|
||||
|
||||
if plaintext == None:
|
||||
shared_key = self.prv.exchange(peer_pub)
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
)
|
||||
plaintext = self.__decrypt(shared_key, ciphertext)
|
||||
|
||||
token = Token(derived_key)
|
||||
plaintext = token.decrypt(ciphertext)
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = None
|
||||
|
||||
@@ -723,7 +760,8 @@ class Identity:
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = None
|
||||
|
||||
return plaintext;
|
||||
return plaintext
|
||||
|
||||
else:
|
||||
RNS.log("Decryption failed because the token size was invalid.", RNS.LOG_DEBUG)
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -70,7 +78,7 @@ class AX25KISSInterface(Interface):
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
import importlib
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -63,7 +71,7 @@ class KISSInterface(Interface):
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
import importlib
|
||||
import importlib.util
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.on_android = True
|
||||
if importlib.util.find_spec('usbserial4a') != None:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -24,6 +32,7 @@ from RNS.Interfaces.Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
import socket
|
||||
import time
|
||||
import math
|
||||
import RNS
|
||||
@@ -65,6 +74,7 @@ class KISS():
|
||||
CMD_STAT_PHYPRM = 0x26
|
||||
CMD_STAT_BAT = 0x27
|
||||
CMD_STAT_CSMA = 0x28
|
||||
CMD_STAT_TEMP = 0x29
|
||||
CMD_BLINK = 0x30
|
||||
CMD_RANDOM = 0x40
|
||||
CMD_FB_EXT = 0x41
|
||||
@@ -355,7 +365,9 @@ class RNodeInterface(Interface):
|
||||
target_device_address = c["target_device_address"] if "target_device_address" in c else None
|
||||
ble_name = c["ble_name"] if "ble_name" in c else None
|
||||
ble_addr = c["ble_addr"] if "ble_addr" in c else None
|
||||
tcp_host = c["tcp_host"] if "tcp_host" in c else None
|
||||
force_ble = c["force_ble"] if "force_ble" in c else False
|
||||
force_tcp = c["force_tcp"] if "force_tcp" in c else False
|
||||
frequency = int(c["frequency"]) if "frequency" in c else 0
|
||||
bandwidth = int(c["bandwidth"]) if "bandwidth" in c else 0
|
||||
txpower = int(c["txpower"]) if "txpower" in c else 0
|
||||
@@ -368,7 +380,7 @@ class RNodeInterface(Interface):
|
||||
lt_alock = float(c["airtime_limit_long"]) if "airtime_limit_long" in c and c["airtime_limit_long"] != None else None
|
||||
port = c["port"] if "port" in c else None
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.on_android = True
|
||||
if importlib.util.find_spec('usbserial4a') != None:
|
||||
@@ -427,6 +439,14 @@ class RNodeInterface(Interface):
|
||||
self.ble_rx_queue= b""
|
||||
self.ble_tx_queue= b""
|
||||
|
||||
self.tcp = None
|
||||
self.use_tcp = False
|
||||
self.tcp_host = tcp_host
|
||||
self.tcp_rx_queue= b""
|
||||
self.tcp_tx_queue= b""
|
||||
self.tcp_rx_lock = threading.Lock()
|
||||
self.tcp_tx_lock = threading.Lock()
|
||||
|
||||
self.frequency = frequency
|
||||
self.bandwidth = bandwidth
|
||||
self.txpower = txpower
|
||||
@@ -436,6 +456,7 @@ class RNodeInterface(Interface):
|
||||
self.bitrate = 0
|
||||
self.st_alock = st_alock
|
||||
self.lt_alock = lt_alock
|
||||
self.cpu_temp = None
|
||||
self.platform = None
|
||||
self.display = None
|
||||
self.mcu = None
|
||||
@@ -448,6 +469,7 @@ class RNodeInterface(Interface):
|
||||
self.first_tx = None
|
||||
self.reconnect_w = RNodeInterface.RECONNECT_WAIT
|
||||
self.reconnect_lock = threading.Lock()
|
||||
self.awaiting_ble_reset = False
|
||||
|
||||
self.r_frequency = None
|
||||
self.r_bandwidth = None
|
||||
@@ -478,6 +500,9 @@ class RNodeInterface(Interface):
|
||||
self.r_csma_cw_max = None
|
||||
self.r_current_rssi = None
|
||||
self.r_noise_floor = None
|
||||
self.r_interference = None
|
||||
self.r_interference_l = None
|
||||
self.r_temperature = None
|
||||
|
||||
self.r_battery_state = RNodeInterface.BATTERY_STATE_UNKNOWN
|
||||
self.r_battery_percent = 0
|
||||
@@ -499,15 +524,15 @@ class RNodeInterface(Interface):
|
||||
self.port_io_timeout = RNodeInterface.PORT_IO_TIMEOUT
|
||||
self.last_imagedata = None
|
||||
|
||||
if force_ble or self.ble_addr != None or self.ble_name != None:
|
||||
self.use_ble = True
|
||||
if force_ble or self.ble_addr != None or self.ble_name != None: self.use_ble = True
|
||||
if force_tcp or self.tcp_host != None: self.use_tcp = True
|
||||
|
||||
self.validcfg = True
|
||||
if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
|
||||
RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
if (self.txpower < 0 or self.txpower > 22):
|
||||
if (self.txpower < 0 or self.txpower > 37):
|
||||
RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
@@ -550,10 +575,8 @@ class RNodeInterface(Interface):
|
||||
self.open_port()
|
||||
|
||||
if self.serial != None:
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
else:
|
||||
raise IOError("Could not open serial port")
|
||||
if self.serial.is_open: self.configure_device()
|
||||
else: raise IOError("Could not open serial port")
|
||||
elif self.bt_manager != None:
|
||||
if self.bt_manager.connected:
|
||||
self.configure_device()
|
||||
@@ -571,12 +594,9 @@ class RNodeInterface(Interface):
|
||||
|
||||
|
||||
def read_mux(self, len=None):
|
||||
if self.serial != None:
|
||||
return self.serial.read()
|
||||
elif self.bt_manager != None:
|
||||
return self.bt_manager.read()
|
||||
else:
|
||||
raise IOError("No ports available for reading")
|
||||
if self.serial != None: return self.serial.read()
|
||||
elif self.bt_manager != None: return self.bt_manager.read()
|
||||
else: raise IOError("No ports available for reading")
|
||||
|
||||
def write_mux(self, data):
|
||||
if self.serial != None:
|
||||
@@ -591,17 +611,19 @@ class RNodeInterface(Interface):
|
||||
else:
|
||||
raise IOError("No ports available for writing")
|
||||
|
||||
# def reset_ble(self):
|
||||
# RNS.log(f"Clearing previous connection instance: "+str(self.ble))
|
||||
# del self.ble
|
||||
# self.ble = None
|
||||
# self.serial = None
|
||||
# self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
|
||||
# self.serial = self.ble
|
||||
# RNS.log(f"New connection instance: "+str(self.ble))
|
||||
def reset_ble(self):
|
||||
if not self.awaiting_ble_reset: return
|
||||
else:
|
||||
RNS.log(f"Clearing previous connection instance: "+str(self.ble), RNS.LOG_DEBUG)
|
||||
self.ble = None
|
||||
self.serial = None
|
||||
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
|
||||
self.serial = self.ble
|
||||
self.awaiting_ble_reset = False
|
||||
RNS.log(f"New connection instance: "+str(self.ble), RNS.LOG_DEBUG)
|
||||
|
||||
def open_port(self):
|
||||
if not self.use_ble:
|
||||
if not self.use_ble and not self.use_tcp:
|
||||
if self.port != None:
|
||||
RNS.log("Opening serial port "+self.port+"...")
|
||||
# Get device parameters
|
||||
@@ -669,7 +691,7 @@ class RNodeInterface(Interface):
|
||||
if self.bt_manager != None:
|
||||
self.bt_manager.connect_any_device()
|
||||
|
||||
else:
|
||||
elif self.use_ble:
|
||||
if self.ble == None:
|
||||
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
|
||||
self.serial = self.ble
|
||||
@@ -678,6 +700,24 @@ class RNodeInterface(Interface):
|
||||
while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
|
||||
time.sleep(1)
|
||||
|
||||
elif self.use_tcp:
|
||||
RNS.log(f"Opening TCP connection for {self}...")
|
||||
if self.tcp != None and self.tcp.running == False:
|
||||
self.tcp.close()
|
||||
self.tcp.cleanup()
|
||||
self.tcp = None
|
||||
|
||||
if self.tcp == None:
|
||||
self.tcp = TCPConnection(owner=self, target_host=self.tcp_host)
|
||||
self.serial = self.tcp
|
||||
|
||||
open_time = time.time()
|
||||
while not self.tcp.connected and time.time() < open_time + self.tcp.CONNECT_TIMEOUT:
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
raise TypeError("No valid device connection type defined for RNode interface")
|
||||
|
||||
|
||||
def configure_device(self):
|
||||
self.resetRadioState()
|
||||
@@ -685,17 +725,19 @@ class RNodeInterface(Interface):
|
||||
thread = threading.Thread(target=self.readLoop, daemon=True).start()
|
||||
|
||||
self.detect()
|
||||
if not self.use_ble:
|
||||
sleep(0.5)
|
||||
else:
|
||||
if self.use_tcp:
|
||||
tcp_detect_timeout = 5.0
|
||||
detect_time = time.time()
|
||||
while not self.detected and time.time() < detect_time + tcp_detect_timeout: time.sleep(0.1)
|
||||
if not self.detected: RNS.log(f"RNode detect timed out over TCP", RNS.LOG_ERROR)
|
||||
elif self.use_ble:
|
||||
ble_detect_timeout = 5
|
||||
detect_time = time.time()
|
||||
while not self.detected and time.time() < detect_time + ble_detect_timeout:
|
||||
time.sleep(0.1)
|
||||
if self.detected:
|
||||
detect_time = RNS.prettytime(time.time()-detect_time)
|
||||
else:
|
||||
RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
|
||||
while not self.detected and time.time() < detect_time + ble_detect_timeout: time.sleep(0.1)
|
||||
if self.detected: detect_time = RNS.prettytime(time.time()-detect_time)
|
||||
else: RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
|
||||
else:
|
||||
sleep(0.2)
|
||||
|
||||
if not self.detected:
|
||||
raise IOError("Could not detect device")
|
||||
@@ -708,11 +750,19 @@ class RNodeInterface(Interface):
|
||||
|
||||
if self.serial != None and self.port != None:
|
||||
self.timeout = 200
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
RNS.log(f"Serial port {self.port} is now open")
|
||||
|
||||
if self.bt_manager != None and self.bt_manager.connected:
|
||||
self.timeout = 1500
|
||||
RNS.log("Bluetooth connection to RNode now open")
|
||||
RNS.log(f"Bluetooth connection to RNode now open")
|
||||
|
||||
if self.ble != None and self.ble.connected:
|
||||
self.timeout = 1500
|
||||
RNS.log(f"BLE connection {self.port} to RNode now open")
|
||||
|
||||
if self.tcp != None and self.tcp.connected:
|
||||
self.timeout = 1500
|
||||
RNS.log(f"TCP connection tcp://{self.tcp_host} to RNode now open")
|
||||
|
||||
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
|
||||
self.initRadio()
|
||||
@@ -964,10 +1014,8 @@ class RNodeInterface(Interface):
|
||||
|
||||
def validateRadioState(self):
|
||||
RNS.log("Waiting for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
if not self.platform == KISS.PLATFORM_ESP32:
|
||||
sleep(1.00);
|
||||
else:
|
||||
sleep(2.00);
|
||||
if not self.platform == KISS.PLATFORM_ESP32: sleep(1.00);
|
||||
else: sleep(2.00);
|
||||
|
||||
self.validcfg = True
|
||||
if (self.r_frequency != None and abs(self.frequency - int(self.r_frequency)) > 100):
|
||||
@@ -1264,10 +1312,12 @@ class RNodeInterface(Interface):
|
||||
self.r_channel_load_long = cul/100.0
|
||||
self.r_current_rssi = crs-RNodeInterface.RSSI_OFFSET
|
||||
self.r_noise_floor = nfl-RNodeInterface.RSSI_OFFSET
|
||||
|
||||
if ntf == 0xFF:
|
||||
self.r_interference = None
|
||||
else:
|
||||
self.r_interference = ntf-RNodeInterface.RSSI_OFFSET
|
||||
self.r_interference_l = [time.time(), self.r_interference]
|
||||
|
||||
if self.r_interference != None:
|
||||
RNS.log(f"{self} Radio detected interference at {self.r_interference} dBm", RNS.LOG_DEBUG)
|
||||
@@ -1347,6 +1397,22 @@ class RNodeInterface(Interface):
|
||||
bat_percent = 0
|
||||
self.r_battery_state = command_buffer[0]
|
||||
self.r_battery_percent = bat_percent
|
||||
elif (command == KISS.CMD_STAT_TEMP):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
command_buffer = command_buffer+bytes([byte])
|
||||
if (len(command_buffer) == 1):
|
||||
temp = command_buffer[0]-120
|
||||
if temp >= -30 and temp <= 90: self.r_temperature = temp
|
||||
else: self.r_temperature = None
|
||||
self.cpu_temp = self.r_temperature
|
||||
elif (command == KISS.CMD_RANDOM):
|
||||
self.r_random = byte
|
||||
elif (command == KISS.CMD_PLATFORM):
|
||||
@@ -1417,7 +1483,7 @@ class RNodeInterface(Interface):
|
||||
if got == 0:
|
||||
time_since_last = int(time.time()*1000) - last_read_ms
|
||||
if len(data_buffer) > 0 and time_since_last > self.timeout:
|
||||
RNS.log(str(self)+" serial read timeout in command "+str(command), RNS.LOG_WARNING)
|
||||
RNS.log(f"{self} device read timeout in command {command} after {RNS.prettytime(self.timeout/1000.0)}", RNS.LOG_WARNING)
|
||||
data_buffer = b""
|
||||
in_frame = False
|
||||
command = KISS.CMD_UNKNOWN
|
||||
@@ -1428,19 +1494,19 @@ class RNodeInterface(Interface):
|
||||
if time.time() > self.first_tx + self.id_interval:
|
||||
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.id_callsign.decode("utf-8")), RNS.LOG_DEBUG)
|
||||
self.process_outgoing(self.id_callsign)
|
||||
|
||||
if (time.time() - self.last_port_io > self.port_io_timeout):
|
||||
self.detect()
|
||||
|
||||
if (time.time() - self.last_port_io > self.port_io_timeout*3):
|
||||
raise IOError("Connected port for "+str(self)+" became unresponsive")
|
||||
|
||||
if self.bt_manager != None:
|
||||
sleep(0.08)
|
||||
if self.use_tcp:
|
||||
if self.tcp and self.tcp.connected:
|
||||
if time.time() > self.tcp.last_write + self.tcp.ACTIVITY_KEEPALIVE:
|
||||
self.detect()
|
||||
|
||||
if (time.time() - self.last_port_io > self.port_io_timeout): self.detect()
|
||||
if (time.time() - self.last_port_io > self.port_io_timeout*3): raise IOError("Connected port for "+str(self)+" became unresponsive")
|
||||
if self.bt_manager != None or self.ble != None: sleep(0.08)
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
RNS.log("A serial port occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
|
||||
|
||||
if RNS.Reticulum.panic_on_interface_error:
|
||||
@@ -1498,12 +1564,18 @@ class RNodeInterface(Interface):
|
||||
|
||||
def detach(self):
|
||||
self.detached = True
|
||||
self.disable_external_framebuffer()
|
||||
self.setRadioState(KISS.RADIO_STATE_OFF)
|
||||
self.leave()
|
||||
try:
|
||||
self.disable_external_framebuffer()
|
||||
self.setRadioState(KISS.RADIO_STATE_OFF)
|
||||
self.leave()
|
||||
|
||||
if self.use_ble:
|
||||
self.ble.close()
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while detaching {self}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
if self.use_ble: self.ble.close()
|
||||
if self.use_tcp:
|
||||
time.sleep(0.5)
|
||||
self.tcp.close()
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
@@ -1524,6 +1596,17 @@ class RNodeInterface(Interface):
|
||||
def get_battery_percent(self):
|
||||
return self.r_battery_percent
|
||||
|
||||
def tcp_receive(self, data):
|
||||
with self.tcp_rx_lock: self.tcp_rx_queue += data
|
||||
|
||||
def tcp_waiting(self): return len(self.tcp_tx_queue) > 0
|
||||
|
||||
def get_tcp_waiting(self, n):
|
||||
with self.tcp_tx_lock:
|
||||
data = self.tcp_tx_queue[:n]
|
||||
self.tcp_tx_queue = self.tcp_tx_queue[n:]
|
||||
return data
|
||||
|
||||
def ble_receive(self, data):
|
||||
with self.ble_rx_lock:
|
||||
self.ble_rx_queue += data
|
||||
@@ -1538,7 +1621,7 @@ class RNodeInterface(Interface):
|
||||
return data
|
||||
|
||||
def __str__(self):
|
||||
return "RNodeInterface["+str(self.name)+"]"
|
||||
return f"RNodeInterface[{self.name}]"
|
||||
|
||||
class BLEConnection(BluetoothDispatcher):
|
||||
UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
|
||||
@@ -1550,7 +1633,7 @@ class BLEConnection(BluetoothDispatcher):
|
||||
|
||||
MTU_TIMEOUT = 4.0
|
||||
CONNECT_TIMEOUT = 7.0
|
||||
RECONNECT_WAIT = 1.0
|
||||
RECONNECT_WAIT = 2.5
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
@@ -1651,13 +1734,17 @@ class BLEConnection(BluetoothDispatcher):
|
||||
self.write_thread = None
|
||||
|
||||
def connection_job(self):
|
||||
ble_devices = []
|
||||
while self.should_run:
|
||||
if self.bt_manager.bt_enabled():
|
||||
if self.ble_device == None:
|
||||
self.ble_device = self.find_target_device()
|
||||
if not self.connected:
|
||||
if len(ble_devices) == 0:
|
||||
ble_devices = self.find_target_devices()
|
||||
|
||||
if len(ble_devices) > 0: self.ble_device = ble_devices.pop()
|
||||
else: self.ble_device == None
|
||||
|
||||
if self.ble_device != None:
|
||||
if not self.connected:
|
||||
if self.ble_device != None:
|
||||
if self.was_connected:
|
||||
RNS.log(f"Throttling BLE reconnect for {BLEConnection.RECONNECT_WAIT} seconds", RNS.LOG_DEBUG)
|
||||
time.sleep(BLEConnection.RECONNECT_WAIT)
|
||||
@@ -1669,7 +1756,7 @@ class BLEConnection(BluetoothDispatcher):
|
||||
RNS.log("Bluetooth was disabled, closing active BLE device connection", RNS.LOG_ERROR)
|
||||
self.close()
|
||||
|
||||
time.sleep(2)
|
||||
time.sleep(1)
|
||||
|
||||
def connect_device(self):
|
||||
if self.ble_device != None and self.bt_manager.bt_enabled():
|
||||
@@ -1687,12 +1774,15 @@ class BLEConnection(BluetoothDispatcher):
|
||||
else:
|
||||
RNS.log(f"BLE device connection timed out for {self.owner}", RNS.LOG_DEBUG)
|
||||
if self.mtu_requested_time:
|
||||
RNS.log("MTU update timeout, tearing down connection")
|
||||
RNS.log("MTU update timeout, tearing down connection and resetting BLE dispatcher")
|
||||
self.owner.hw_errors.append({"error": KISS.ERROR_INVALID_BLE_MTU, "description": "The Bluetooth Low Energy transfer MTU could not be configured for the connected device, and communication has failed. Restart Reticulum and any connected applications to retry connecting."})
|
||||
self.close()
|
||||
self.close_gatt()
|
||||
self.should_run = False
|
||||
|
||||
self.close_gatt()
|
||||
self.owner.awaiting_ble_reset = True
|
||||
|
||||
else:
|
||||
self.close_gatt()
|
||||
|
||||
self.connect_job_running = False
|
||||
|
||||
@@ -1702,15 +1792,17 @@ class BLEConnection(BluetoothDispatcher):
|
||||
self.ble_device = None
|
||||
self.close_gatt()
|
||||
|
||||
def find_target_device(self):
|
||||
def find_target_devices(self):
|
||||
found_device = None
|
||||
potential_devices = self.bt_manager.get_paired_devices()
|
||||
suitable_devices = []
|
||||
|
||||
if self.target_bt_addr != None:
|
||||
for device in potential_devices:
|
||||
if (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_LE) or (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_DUAL):
|
||||
if str(device.getAddress()).replace(":", "").lower() == str(self.target_bt_addr).replace(":", "").lower():
|
||||
found_device = device
|
||||
suitable_devices.append(device)
|
||||
break
|
||||
|
||||
if not found_device and self.target_name != None:
|
||||
@@ -1718,6 +1810,7 @@ class BLEConnection(BluetoothDispatcher):
|
||||
if (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_LE) or (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_DUAL):
|
||||
if device.getName().lower() == self.target_name.lower():
|
||||
found_device = device
|
||||
suitable_devices.append(device)
|
||||
break
|
||||
|
||||
if not found_device:
|
||||
@@ -1725,9 +1818,9 @@ class BLEConnection(BluetoothDispatcher):
|
||||
if (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_LE) or (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_DUAL):
|
||||
if device.getName().startswith("RNode "):
|
||||
found_device = device
|
||||
break
|
||||
suitable_devices.append(device)
|
||||
|
||||
return found_device
|
||||
return suitable_devices
|
||||
|
||||
def on_connection_state_change(self, status, state):
|
||||
if status == GATT_SUCCESS and state:
|
||||
@@ -1771,4 +1864,139 @@ class BLEConnection(BluetoothDispatcher):
|
||||
def on_characteristic_changed(self, characteristic):
|
||||
if characteristic.getUuid().toString() == BLEConnection.UART_TX_CHAR_UUID:
|
||||
recvd = bytes(characteristic.getValue())
|
||||
self.owner.ble_receive(recvd)
|
||||
self.owner.ble_receive(recvd)
|
||||
|
||||
class TCPConnection():
|
||||
TARGET_PORT = 7633
|
||||
CONNECT_TIMEOUT = 5.0
|
||||
INITIAL_CONNECT_TIMEOUT = 5.0
|
||||
RECONNECT_WAIT = 4.0
|
||||
ACTIVITY_TIMEOUT = 6.0
|
||||
ACTIVITY_KEEPALIVE = ACTIVITY_TIMEOUT-2.5
|
||||
|
||||
TCP_USER_TIMEOUT = 24
|
||||
TCP_PROBE_AFTER = 5
|
||||
TCP_PROBE_INTERVAL = 2
|
||||
TCP_PROBES = 12
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
return self.connected
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
buflen = len(self.owner.tcp_rx_queue)
|
||||
return buflen > 0
|
||||
|
||||
def write(self, data_bytes):
|
||||
if self.connected and self.socket:
|
||||
with self.owner.tcp_tx_lock:
|
||||
if len(self.owner.tcp_tx_queue) > 0:
|
||||
self.socket.sendall(self.owner.tcp_tx_queue)
|
||||
self.owner.tcp_tx_queue = b""
|
||||
|
||||
self.socket.sendall(data_bytes)
|
||||
self.last_write = time.time()
|
||||
|
||||
else:
|
||||
with self.owner.tcp_tx_lock: self.owner.tcp_tx_queue += data_bytes
|
||||
|
||||
return len(data_bytes)
|
||||
|
||||
def read(self, n=4096):
|
||||
with self.owner.tcp_rx_lock:
|
||||
data = self.owner.tcp_rx_queue[:n]
|
||||
self.owner.tcp_rx_queue = self.owner.tcp_rx_queue[n:]
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
if self.connected:
|
||||
RNS.log(f"Disconnecting TCP socket for {self.owner}", RNS.LOG_DEBUG)
|
||||
self.must_disconnect = True
|
||||
if self.socket: self.socket.close()
|
||||
|
||||
def __init__(self, owner=None, target_host=None):
|
||||
self.owner = owner
|
||||
self.target_host = target_host
|
||||
self.connected = False
|
||||
self.reconnecting = False
|
||||
self.running = False
|
||||
self.should_run = False
|
||||
self.must_disconnect = False
|
||||
self.connect_job_running = False
|
||||
self.last_write = time.time()
|
||||
|
||||
self.should_run = True
|
||||
self.connection_thread = threading.Thread(target=self.initial_connect, daemon=True).start()
|
||||
|
||||
def set_timeouts_linux(self):
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(self.TCP_USER_TIMEOUT * 1000))
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(self.TCP_PROBE_INTERVAL))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(self.TCP_PROBES))
|
||||
|
||||
def set_timeouts_osx(self):
|
||||
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(self.TCP_PROBE_AFTER))
|
||||
|
||||
def cleanup(self):
|
||||
try:
|
||||
if self.socket: self.socket.close()
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while disconnecting TCP socket on cleanup for {self.owner}", RNS.LOG_ERROR)
|
||||
|
||||
self.should_run = False
|
||||
|
||||
def initial_connect(self):
|
||||
if self.connect(initial=True): threading.Thread(target=self.read_loop, daemon=True).start()
|
||||
|
||||
def connect(self, initial=False):
|
||||
try:
|
||||
if initial:
|
||||
RNS.log(f"Establishing TCP connection to device for {self.owner}...", RNS.LOG_DEBUG)
|
||||
|
||||
address_info = socket.getaddrinfo(self.target_host, self.TARGET_PORT, proto=socket.IPPROTO_TCP)[0]
|
||||
address_family = address_info[0]
|
||||
target_address = address_info[4]
|
||||
|
||||
self.socket = socket.socket(address_family, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(self.INITIAL_CONNECT_TIMEOUT)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.socket.connect(target_address)
|
||||
self.socket.settimeout(None)
|
||||
self.connected = True
|
||||
self.last_write = time.time()
|
||||
|
||||
RNS.log(f"TCP connection to device for {self.owner} established", RNS.LOG_DEBUG)
|
||||
|
||||
if RNS.vendor.platformutils.is_linux(): self.set_timeouts_linux()
|
||||
elif RNS.vendor.platformutils.is_darwin(): self.set_timeouts_osx()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
if initial:
|
||||
RNS.log(f"TCP connection to device for {self.owner} could not be established: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
else: raise e
|
||||
|
||||
def read_loop(self):
|
||||
try:
|
||||
data_in = b""
|
||||
while not self.must_disconnect:
|
||||
if self.socket: data_in = self.socket.recv(4096)
|
||||
else: data_in = b""
|
||||
|
||||
if len(data_in) > 0: self.owner.tcp_receive(data_in)
|
||||
else:
|
||||
self.connected = False
|
||||
RNS.log(f"The TCP socket for {self} was closed", RNS.LOG_WARNING)
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
self.connected = False
|
||||
RNS.log(f"A TCP read error occurred for {self}, the contained exception was: {e}", RNS.LOG_WARNING)
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -53,7 +61,7 @@ class SerialInterface(Interface):
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
import importlib
|
||||
import importlib.util
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.on_android = True
|
||||
if importlib.util.find_spec('usbserial4a') != None:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
+254
-100
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -33,6 +41,9 @@ import RNS
|
||||
|
||||
|
||||
class AutoInterface(Interface):
|
||||
HW_MTU = 1196
|
||||
FIXED_MTU = True
|
||||
|
||||
DEFAULT_DISCOVERY_PORT = 29716
|
||||
DEFAULT_DATA_PORT = 42671
|
||||
DEFAULT_GROUP_ID = "reticulum".encode("utf-8")
|
||||
@@ -47,7 +58,10 @@ class AutoInterface(Interface):
|
||||
MULTICAST_PERMANENT_ADDRESS_TYPE = "0"
|
||||
MULTICAST_TEMPORARY_ADDRESS_TYPE = "1"
|
||||
|
||||
PEERING_TIMEOUT = 7.5
|
||||
PEERING_TIMEOUT = 22.0
|
||||
ANNOUNCE_INTERVAL = 1.6
|
||||
PEER_JOB_INTERVAL = 4.0
|
||||
MCAST_ECHO_TIMEOUT = 6.5
|
||||
|
||||
ALL_IGNORE_IFS = ["lo0"]
|
||||
DARWIN_IGNORE_IFS = ["awdl0", "llw0", "lo0", "en5"]
|
||||
@@ -79,7 +93,6 @@ class AutoInterface(Interface):
|
||||
return ifas
|
||||
|
||||
def interface_name_to_index(self, ifname):
|
||||
|
||||
# socket.if_nametoindex doesn't work with uuid interface names on windows, it wants the ethernet_0 style
|
||||
# we will just get the index from netinfo instead as it seems to work
|
||||
if RNS.vendor.platformutils.is_windows():
|
||||
@@ -99,38 +112,43 @@ class AutoInterface(Interface):
|
||||
ignored_interfaces = c.as_list("ignored_devices") if "ignored_devices" in c else None
|
||||
configured_bitrate = c["configured_bitrate"] if "configured_bitrate" in c else None
|
||||
|
||||
from RNS.vendor.ifaddr import niwrapper
|
||||
from RNS.Interfaces import netinfo
|
||||
super().__init__()
|
||||
self.netinfo = niwrapper
|
||||
|
||||
self.HW_MTU = 1064
|
||||
self.netinfo = netinfo
|
||||
|
||||
self.HW_MTU = AutoInterface.HW_MTU
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.name = name
|
||||
self.owner = owner
|
||||
self.online = False
|
||||
self.final_init_done = False
|
||||
self.peers = {}
|
||||
self.link_local_addresses = []
|
||||
self.adopted_interfaces = {}
|
||||
self.interface_servers = {}
|
||||
self.multicast_echoes = {}
|
||||
self.initial_echoes = {}
|
||||
self.timed_out_interfaces = {}
|
||||
self.spawned_interfaces = {}
|
||||
self.write_lock = threading.Lock()
|
||||
self.mif_deque = deque(maxlen=AutoInterface.MULTI_IF_DEQUE_LEN)
|
||||
self.mif_deque_times = deque(maxlen=AutoInterface.MULTI_IF_DEQUE_LEN)
|
||||
self.carrier_changed = False
|
||||
|
||||
self.outbound_udp_socket = None
|
||||
|
||||
self.announce_rate_target = None
|
||||
self.announce_interval = AutoInterface.PEERING_TIMEOUT/6.0
|
||||
self.peer_job_interval = AutoInterface.PEERING_TIMEOUT*1.1
|
||||
self.peering_timeout = AutoInterface.PEERING_TIMEOUT
|
||||
self.multicast_echo_timeout = AutoInterface.PEERING_TIMEOUT/2
|
||||
self.announce_rate_target = None
|
||||
self.announce_interval = AutoInterface.ANNOUNCE_INTERVAL
|
||||
self.peer_job_interval = AutoInterface.PEER_JOB_INTERVAL
|
||||
self.peering_timeout = AutoInterface.PEERING_TIMEOUT
|
||||
self.multicast_echo_timeout = AutoInterface.MCAST_ECHO_TIMEOUT
|
||||
self.reverse_peering_interval = self.announce_interval*3.25
|
||||
|
||||
# Increase peering timeout on Android, due to potential
|
||||
# low-power modes implemented on many chipsets.
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
self.peering_timeout *= 3
|
||||
self.peering_timeout *= 1.25
|
||||
|
||||
if allowed_interfaces == None:
|
||||
self.allowed_interfaces = []
|
||||
@@ -152,6 +170,8 @@ class AutoInterface(Interface):
|
||||
else:
|
||||
self.discovery_port = discovery_port
|
||||
|
||||
self.unicast_discovery_port = self.discovery_port+1
|
||||
|
||||
if multicast_address_type == None:
|
||||
self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
|
||||
elif str(multicast_address_type).lower() == "temporary":
|
||||
@@ -227,33 +247,48 @@ class AutoInterface(Interface):
|
||||
if link_local_addr == None:
|
||||
RNS.log(str(self)+" No link-local IPv6 address configured for "+str(ifname)+", skipping interface", RNS.LOG_EXTREME)
|
||||
else:
|
||||
mcast_addr = self.mcast_discovery_address
|
||||
RNS.log(str(self)+" Creating multicast discovery listener on "+str(ifname)+" with address "+str(mcast_addr), RNS.LOG_EXTREME)
|
||||
RNS.log(str(self)+" Creating unicast discovery listener on "+str(ifname)+" with address "+str(link_local_addr), RNS.LOG_EXTREME)
|
||||
|
||||
# Struct with interface index
|
||||
if_struct = struct.pack("I", self.interface_name_to_index(ifname))
|
||||
|
||||
# Set up multicast socket
|
||||
# Set up unicast discovery socket
|
||||
unicast_discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
unicast_discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
if hasattr(socket, "SO_REUSEPORT"): unicast_discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
|
||||
# Bind unicast discovery socket
|
||||
if RNS.vendor.platformutils.is_windows():
|
||||
# Windows throws "[WinError 10049] The requested address is not valid in its context"
|
||||
# when trying to use the multicast address as host, or when providing interface index
|
||||
# passing an empty host appears to work, but probably not exactly how we want it to...
|
||||
unicast_discovery_socket.bind(('', self.unicast_discovery_port))
|
||||
|
||||
else:
|
||||
addr_info = socket.getaddrinfo(link_local_addr+"%"+ifname, self.unicast_discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
unicast_discovery_socket.bind(addr_info[0][4])
|
||||
|
||||
mcast_addr = self.mcast_discovery_address
|
||||
RNS.log(str(self)+" Creating multicast discovery listener on "+str(ifname)+" with address "+str(mcast_addr), RNS.LOG_EXTREME)
|
||||
|
||||
# Set up multicast discovery socket
|
||||
discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
if hasattr(socket, "SO_REUSEPORT"):
|
||||
discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
if hasattr(socket, "SO_REUSEPORT"): discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, if_struct)
|
||||
|
||||
# Join multicast group
|
||||
mcast_group = socket.inet_pton(socket.AF_INET6, mcast_addr) + if_struct
|
||||
discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mcast_group)
|
||||
|
||||
# Bind socket
|
||||
# Bind multicast socket
|
||||
if RNS.vendor.platformutils.is_windows():
|
||||
|
||||
# window throws "[WinError 10049] The requested address is not valid in its context"
|
||||
# Windows throws "[WinError 10049] The requested address is not valid in its context"
|
||||
# when trying to use the multicast address as host, or when providing interface index
|
||||
# passing an empty host appears to work, but probably not exactly how we want it to...
|
||||
discovery_socket.bind(('', self.discovery_port))
|
||||
|
||||
else:
|
||||
|
||||
if self.discovery_scope == AutoInterface.SCOPE_LINK:
|
||||
addr_info = socket.getaddrinfo(mcast_addr+"%"+ifname, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
else:
|
||||
@@ -261,13 +296,13 @@ class AutoInterface(Interface):
|
||||
|
||||
discovery_socket.bind(addr_info[0][4])
|
||||
|
||||
# Set up thread for discovery packets
|
||||
def discovery_loop():
|
||||
self.discovery_handler(discovery_socket, ifname)
|
||||
|
||||
thread = threading.Thread(target=discovery_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
# Set up thread for multicast discovery packets
|
||||
def discovery_loop(): self.discovery_handler(discovery_socket, ifname)
|
||||
thread = threading.Thread(target=discovery_loop, daemon=True).start()
|
||||
|
||||
# Set up thread for unicast discovery packets
|
||||
def unicast_discovery_loop(): self.discovery_handler(unicast_discovery_socket, ifname, announce=False)
|
||||
thread = threading.Thread(target=unicast_discovery_loop, daemon=True).start()
|
||||
|
||||
suitable_interfaces += 1
|
||||
|
||||
@@ -288,48 +323,50 @@ class AutoInterface(Interface):
|
||||
else:
|
||||
self.bitrate = AutoInterface.BITRATE_GUESS
|
||||
|
||||
peering_wait = self.announce_interval*1.2
|
||||
RNS.log(str(self)+" discovering peers for "+str(round(peering_wait, 2))+" seconds...", RNS.LOG_VERBOSE)
|
||||
def final_init(self):
|
||||
peering_wait = self.announce_interval*1.2
|
||||
RNS.log(str(self)+" discovering peers for "+str(round(peering_wait, 2))+" seconds...", RNS.LOG_VERBOSE)
|
||||
|
||||
self.owner = owner
|
||||
socketserver.UDPServer.address_family = socket.AF_INET6
|
||||
socketserver.UDPServer.address_family = socket.AF_INET6
|
||||
|
||||
for ifname in self.adopted_interfaces:
|
||||
local_addr = self.adopted_interfaces[ifname]+"%"+str(self.interface_name_to_index(ifname))
|
||||
addr_info = socket.getaddrinfo(local_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
address = addr_info[0][4]
|
||||
for ifname in self.adopted_interfaces:
|
||||
local_addr = self.adopted_interfaces[ifname]+"%"+str(self.interface_name_to_index(ifname))
|
||||
addr_info = socket.getaddrinfo(local_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
address = addr_info[0][4]
|
||||
|
||||
udp_server = socketserver.UDPServer(address, self.handler_factory(self.process_incoming))
|
||||
self.interface_servers[ifname] = udp_server
|
||||
|
||||
thread = threading.Thread(target=udp_server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
job_thread = threading.Thread(target=self.peer_jobs)
|
||||
job_thread.daemon = True
|
||||
job_thread.start()
|
||||
|
||||
time.sleep(peering_wait)
|
||||
|
||||
self.online = True
|
||||
|
||||
|
||||
def discovery_handler(self, socket, ifname):
|
||||
def announce_loop():
|
||||
self.announce_handler(ifname)
|
||||
udp_server = socketserver.UDPServer(address, self.handler_factory(self.process_incoming))
|
||||
self.interface_servers[ifname] = udp_server
|
||||
|
||||
thread = threading.Thread(target=announce_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
thread = threading.Thread(target=udp_server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
job_thread = threading.Thread(target=self.peer_jobs)
|
||||
job_thread.daemon = True
|
||||
job_thread.start()
|
||||
|
||||
time.sleep(peering_wait)
|
||||
|
||||
self.online = True
|
||||
self.final_init_done = True
|
||||
|
||||
def discovery_handler(self, socket, ifname, announce=True):
|
||||
def announce_loop(): self.announce_handler(ifname)
|
||||
|
||||
if announce:
|
||||
thread = threading.Thread(target=announce_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
while True:
|
||||
data, ipv6_src = socket.recvfrom(1024)
|
||||
expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
|
||||
if data == expected_hash:
|
||||
self.add_peer(ipv6_src[0], ifname)
|
||||
else:
|
||||
RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG)
|
||||
if self.final_init_done:
|
||||
peering_hash = data[:RNS.Identity.HASHLENGTH//8]
|
||||
expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
|
||||
if peering_hash == expected_hash:
|
||||
self.add_peer(ipv6_src[0], ifname)
|
||||
else:
|
||||
RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG)
|
||||
|
||||
def peer_jobs(self):
|
||||
while True:
|
||||
@@ -347,8 +384,24 @@ class AutoInterface(Interface):
|
||||
# Remove any timed out peers
|
||||
for peer_addr in timed_out_peers:
|
||||
removed_peer = self.peers.pop(peer_addr)
|
||||
if peer_addr in self.spawned_interfaces:
|
||||
spawned_interface = self.spawned_interfaces[peer_addr]
|
||||
spawned_interface.detach()
|
||||
spawned_interface.teardown()
|
||||
RNS.log(str(self)+" removed peer "+str(peer_addr)+" on "+str(removed_peer[0]), RNS.LOG_DEBUG)
|
||||
|
||||
# Send reverse peering packets
|
||||
for peer_addr in self.peers:
|
||||
try:
|
||||
peer = self.peers[peer_addr]
|
||||
ifname = peer[0]
|
||||
last_outbound = peer[2]
|
||||
if now > last_outbound+self.reverse_peering_interval:
|
||||
self.reverse_announce(ifname, peer_addr)
|
||||
peer[2] = time.time()
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while sending reverse peering packet to {peer_addr}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
for ifname in self.adopted_interfaces:
|
||||
# Check that the link-local address has not changed
|
||||
try:
|
||||
@@ -394,9 +447,10 @@ class AutoInterface(Interface):
|
||||
RNS.log("Could not get device information while updating link-local addresses for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
# Check multicast echo timeouts
|
||||
last_multicast_echo = 0
|
||||
if ifname in self.multicast_echoes:
|
||||
last_multicast_echo = self.multicast_echoes[ifname]
|
||||
last_multicast_echo = 0
|
||||
multicast_echo_received = False
|
||||
if ifname in self.multicast_echoes: last_multicast_echo = self.multicast_echoes[ifname]
|
||||
if ifname in self.initial_echoes: multicast_echo_received = True
|
||||
|
||||
if now - last_multicast_echo > self.multicast_echo_timeout:
|
||||
if ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == False:
|
||||
@@ -408,6 +462,11 @@ class AutoInterface(Interface):
|
||||
self.carrier_changed = True
|
||||
RNS.log(str(self)+" Carrier recovered on "+str(ifname), RNS.LOG_WARNING)
|
||||
self.timed_out_interfaces[ifname] = False
|
||||
|
||||
if not multicast_echo_received:
|
||||
RNS.log(f"{self} No multicast echoes received on {ifname}. The networking hardware or a firewall may be blocking multicast traffic.", RNS.LOG_ERROR)
|
||||
# else:
|
||||
# RNS.log(f"{self} Initial multicast echo on {ifname} received {RNS.prettytime(time.time()-self.initial_echoes[ifname])} ago.", RNS.LOG_DEBUG)
|
||||
|
||||
|
||||
def announce_handler(self, ifname):
|
||||
@@ -415,6 +474,20 @@ class AutoInterface(Interface):
|
||||
self.peer_announce(ifname)
|
||||
time.sleep(self.announce_interval)
|
||||
|
||||
def reverse_announce(self, ifname, peer_addr):
|
||||
try:
|
||||
link_local_address = self.adopted_interfaces[ifname]
|
||||
discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
|
||||
announce_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
addr_info = socket.getaddrinfo(f"{peer_addr}%{ifname}", self.unicast_discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
|
||||
ifis = struct.pack("I", self.interface_name_to_index(ifname))
|
||||
announce_socket.sendto(discovery_token, addr_info[0][4])
|
||||
announce_socket.close()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not send reverse peering packet to {peer_addr} on {ifname}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def peer_announce(self, ifname):
|
||||
try:
|
||||
link_local_address = self.adopted_interfaces[ifname]
|
||||
@@ -433,6 +506,10 @@ class AutoInterface(Interface):
|
||||
else:
|
||||
pass
|
||||
|
||||
@property
|
||||
def peer_count(self):
|
||||
return len(self.spawned_interfaces)
|
||||
|
||||
def add_peer(self, addr, ifname):
|
||||
if addr in self.link_local_addresses:
|
||||
ifname = None
|
||||
@@ -442,63 +519,139 @@ class AutoInterface(Interface):
|
||||
|
||||
if ifname != None:
|
||||
self.multicast_echoes[ifname] = time.time()
|
||||
if not ifname in self.initial_echoes: self.initial_echoes[ifname] = time.time()
|
||||
else:
|
||||
RNS.log(str(self)+" received multicast echo on unexpected interface "+str(ifname), RNS.LOG_WARNING)
|
||||
|
||||
else:
|
||||
if not addr in self.peers:
|
||||
self.peers[addr] = [ifname, time.time()]
|
||||
self.peers[addr] = [ifname, time.time(), time.time()]
|
||||
|
||||
spawned_interface = AutoInterfacePeer(self, addr, ifname)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
|
||||
spawned_interface.ifac_size = self.ifac_size
|
||||
spawned_interface.ifac_netname = self.ifac_netname
|
||||
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"))
|
||||
|
||||
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_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))
|
||||
|
||||
spawned_interface.announce_rate_target = self.announce_rate_target
|
||||
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.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
if addr in self.spawned_interfaces:
|
||||
self.spawned_interfaces[addr].detach()
|
||||
self.spawned_interfaces[addr].teardown()
|
||||
if addr in self.spawned_interfaces: self.spawned_interfaces.pop(addr)
|
||||
self.spawned_interfaces[addr] = spawned_interface
|
||||
|
||||
RNS.log(str(self)+" added peer "+str(addr)+" on "+str(ifname), RNS.LOG_DEBUG)
|
||||
else:
|
||||
self.refresh_peer(addr)
|
||||
|
||||
def refresh_peer(self, addr):
|
||||
self.peers[addr][1] = time.time()
|
||||
try: self.peers[addr][1] = time.time()
|
||||
except Exception as e: RNS.log(f"An error occurred while refreshing peer {addr} on {self}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
def process_incoming(self, data):
|
||||
if self.online:
|
||||
def process_incoming(self, data, addr=None):
|
||||
if self.online and addr in self.spawned_interfaces:
|
||||
self.spawned_interfaces[addr].process_incoming(data, addr)
|
||||
|
||||
def process_outgoing(self, data): pass
|
||||
|
||||
def detach(self): self.online = False
|
||||
|
||||
def __str__(self): return f"AutoInterface[{self.name}]"
|
||||
|
||||
class AutoInterfacePeer(Interface):
|
||||
|
||||
def __init__(self, owner, addr, ifname):
|
||||
super().__init__()
|
||||
self.owner = owner
|
||||
self.parent_interface = owner
|
||||
self.addr = addr
|
||||
self.ifname = ifname
|
||||
self.peer_addr = None
|
||||
self.addr_info = None
|
||||
self.HW_MTU = self.owner.HW_MTU
|
||||
self.FIXED_MTU = self.owner.FIXED_MTU
|
||||
|
||||
def __str__(self):
|
||||
return f"AutoInterfacePeer[{self.ifname}/{self.addr}]"
|
||||
|
||||
def process_incoming(self, data, addr=None):
|
||||
if self.online and self.owner.online:
|
||||
data_hash = RNS.Identity.full_hash(data)
|
||||
deque_hit = False
|
||||
if data_hash in self.mif_deque:
|
||||
for te in self.mif_deque_times:
|
||||
if data_hash in self.owner.mif_deque:
|
||||
for te in self.owner.mif_deque_times:
|
||||
if te[0] == data_hash and time.time() < te[1]+AutoInterface.MULTI_IF_DEQUE_TTL:
|
||||
deque_hit = True
|
||||
break
|
||||
|
||||
if not deque_hit:
|
||||
self.mif_deque.append(data_hash)
|
||||
self.mif_deque_times.append([data_hash, time.time()])
|
||||
self.owner.refresh_peer(self.addr)
|
||||
self.owner.mif_deque.append(data_hash)
|
||||
self.owner.mif_deque_times.append([data_hash, time.time()])
|
||||
self.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
self.owner.rxb += len(data)
|
||||
self.owner.owner.inbound(data, self)
|
||||
|
||||
def process_outgoing(self,data):
|
||||
def process_outgoing(self, data):
|
||||
if self.online:
|
||||
for peer in self.peers:
|
||||
with self.owner.write_lock:
|
||||
try:
|
||||
if self.outbound_udp_socket == None:
|
||||
self.outbound_udp_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
|
||||
peer_addr = str(peer)+"%"+str(self.interface_name_to_index(self.peers[peer][0]))
|
||||
addr_info = socket.getaddrinfo(peer_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
self.outbound_udp_socket.sendto(data, addr_info[0][4])
|
||||
|
||||
if self.owner.outbound_udp_socket == None: self.owner.outbound_udp_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
if self.peer_addr == None: self.peer_addr = str(self.addr)+"%"+str(self.owner.interface_name_to_index(self.ifname))
|
||||
if self.addr_info == None: self.addr_info = socket.getaddrinfo(self.peer_addr, self.owner.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
|
||||
self.owner.outbound_udp_socket.sendto(data, self.addr_info[0][4])
|
||||
self.txb += len(data)
|
||||
self.owner.txb += len(data)
|
||||
except Exception as e:
|
||||
RNS.log("Could not transmit on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
self.txb += len(data)
|
||||
|
||||
|
||||
# Until per-device sub-interfacing is implemented,
|
||||
# ingress limiting should be disabled on AutoInterface
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
def detach(self):
|
||||
self.online = False
|
||||
self.detached = True
|
||||
|
||||
def teardown(self):
|
||||
if not self.detached:
|
||||
RNS.log(f"The interface {self} experienced an unrecoverable error and is being torn down.", RNS.LOG_ERROR)
|
||||
if RNS.Reticulum.panic_on_interface_error: RNS.panic()
|
||||
|
||||
def __str__(self):
|
||||
return "AutoInterface["+self.name+"]"
|
||||
else: RNS.log(f"The interface {self} is being torn down.", RNS.LOG_VERBOSE)
|
||||
|
||||
self.online = False
|
||||
self.OUT = False
|
||||
self.IN = False
|
||||
|
||||
if self.addr in self.owner.spawned_interfaces:
|
||||
try: self.owner.spawned_interfaces.pop(self.addr)
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not remove {self} from parent interface on detach. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
if self in RNS.Transport.interfaces: RNS.Transport.interfaces.remove(self)
|
||||
|
||||
class AutoInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, callback, *args, **keys):
|
||||
@@ -507,4 +660,5 @@ class AutoInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
|
||||
def handle(self):
|
||||
data = self.request[0]
|
||||
self.callback(data)
|
||||
addr = self.client_address[0]
|
||||
self.callback(data, addr)
|
||||
@@ -0,0 +1,698 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
import threading
|
||||
import socket
|
||||
import select
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import RNS
|
||||
|
||||
class HDLC():
|
||||
FLAG = 0x7E
|
||||
ESC = 0x7D
|
||||
ESC_MASK = 0x20
|
||||
|
||||
@staticmethod
|
||||
def escape(data):
|
||||
data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
|
||||
data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
|
||||
return data
|
||||
|
||||
class BackboneInterface(Interface):
|
||||
HW_MTU = 1048576
|
||||
BITRATE_GUESS = 1_000_000_000
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
AUTOCONFIGURE_MTU = True
|
||||
|
||||
epoll = None
|
||||
listener_filenos = {}
|
||||
spawned_interface_filenos = {}
|
||||
epoll = None
|
||||
_job_active = False
|
||||
_job_lock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def get_address_for_if(name, bind_port, prefer_ipv6=False):
|
||||
from RNS.Interfaces import netinfo
|
||||
ifaddr = netinfo.ifaddresses(name)
|
||||
if len(ifaddr) < 1:
|
||||
raise SystemError(f"No addresses available on specified kernel interface \"{name}\" for BackboneInterface to bind to")
|
||||
|
||||
if (prefer_ipv6 or not netinfo.AF_INET in ifaddr) and netinfo.AF_INET6 in ifaddr:
|
||||
bind_ip = ifaddr[netinfo.AF_INET6][0]["addr"]
|
||||
if bind_ip.lower().startswith("fe80::"):
|
||||
# We'll need to add the interface as scope for link-local addresses
|
||||
return BackboneInterface.get_address_for_host(f"{bind_ip}%{name}", bind_port, prefer_ipv6)
|
||||
else:
|
||||
return BackboneInterface.get_address_for_host(bind_ip, bind_port, prefer_ipv6)
|
||||
elif netinfo.AF_INET in ifaddr:
|
||||
bind_ip = ifaddr[netinfo.AF_INET][0]["addr"]
|
||||
return (bind_ip, bind_port)
|
||||
else:
|
||||
raise SystemError(f"No addresses available on specified kernel interface \"{name}\" for BackboneInterface to bind to")
|
||||
|
||||
@staticmethod
|
||||
def get_address_for_host(name, bind_port, prefer_ipv6=False):
|
||||
address_infos = socket.getaddrinfo(name, bind_port, proto=socket.IPPROTO_TCP)
|
||||
address_info = address_infos[0]
|
||||
for entry in address_infos:
|
||||
if prefer_ipv6 and entry[0] == socket.AF_INET6:
|
||||
address_info = entry; break
|
||||
elif not prefer_ipv6 and entry[0] == socket.AF_INET:
|
||||
address_info = entry; break
|
||||
|
||||
if address_info[0] == socket.AF_INET6:
|
||||
return (name, bind_port, address_info[4][2], address_info[4][3])
|
||||
elif address_info[0] == socket.AF_INET:
|
||||
return (name, bind_port)
|
||||
else:
|
||||
raise SystemError(f"No suitable kernel interface available for address \"{name}\" for BackboneInterface to bind to")
|
||||
|
||||
|
||||
@property
|
||||
def clients(self):
|
||||
return len(self.spawned_interfaces)
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
if not RNS.vendor.platformutils.is_linux() and not RNS.vendor.platformutils.is_android():
|
||||
raise OSError("BackboneInterface is only supported on Linux-based operating systems")
|
||||
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
device = c["device"] if "device" in c else None
|
||||
port = int(c["port"]) if "port" in c else None
|
||||
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
|
||||
|
||||
if port != None: bindport = port
|
||||
|
||||
self.HW_MTU = BackboneInterface.HW_MTU
|
||||
self.online = False
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.name = name
|
||||
self.detached = False
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
self.spawned_interfaces = []
|
||||
self.supports_discovery = True
|
||||
|
||||
if bindport == None:
|
||||
raise SystemError(f"No TCP port configured for interface \"{name}\"")
|
||||
else:
|
||||
self.bind_port = bindport
|
||||
|
||||
bind_address = None
|
||||
if device != None:
|
||||
bind_address = self.get_address_for_if(device, self.bind_port, prefer_ipv6)
|
||||
else:
|
||||
if bindip == None:
|
||||
raise SystemError(f"No TCP bind IP configured for interface \"{name}\"")
|
||||
bind_address = self.get_address_for_host(bindip, self.bind_port, prefer_ipv6)
|
||||
|
||||
if bind_address != None:
|
||||
self.receives = True
|
||||
self.bind_ip = bind_address[0]
|
||||
self.owner = owner
|
||||
|
||||
if len(bind_address) == 2 : BackboneInterface.add_listener(self, bind_address, socket_type=socket.AF_INET)
|
||||
elif len(bind_address) == 4: BackboneInterface.add_listener(self, bind_address, socket_type=socket.AF_INET6)
|
||||
|
||||
self.bitrate = self.BITRATE_GUESS
|
||||
self.online = True
|
||||
|
||||
else:
|
||||
raise SystemError("Insufficient parameters to create listener")
|
||||
|
||||
@staticmethod
|
||||
def start():
|
||||
if not BackboneInterface._job_active: threading.Thread(target=BackboneInterface.__job, daemon=True).start()
|
||||
|
||||
@staticmethod
|
||||
def ensure_epoll():
|
||||
if not BackboneInterface.epoll: BackboneInterface.epoll = select.epoll()
|
||||
|
||||
@staticmethod
|
||||
def add_listener(interface, bind_address, socket_type=socket.AF_INET):
|
||||
BackboneInterface.ensure_epoll()
|
||||
if socket_type == socket.AF_INET:
|
||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server_socket.bind(bind_address)
|
||||
elif socket_type == socket.AF_INET6:
|
||||
server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server_socket.bind(bind_address)
|
||||
elif socket_type == socket.AF_UNIX:
|
||||
server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server_socket.bind(bind_address)
|
||||
else: raise TypeError(f"Invalid socket type {socket_type} for {interface}")
|
||||
|
||||
server_socket.listen(1)
|
||||
server_socket.setblocking(0)
|
||||
BackboneInterface.listener_filenos[server_socket.fileno()] = (interface, server_socket)
|
||||
BackboneInterface.epoll.register(server_socket.fileno(), select.EPOLLIN)
|
||||
BackboneInterface.start()
|
||||
|
||||
@staticmethod
|
||||
def add_client_socket(client_socket, interface):
|
||||
BackboneInterface.ensure_epoll()
|
||||
BackboneInterface.spawned_interface_filenos[client_socket.fileno()] = interface
|
||||
BackboneInterface.register_in(client_socket.fileno())
|
||||
BackboneInterface.start()
|
||||
|
||||
@staticmethod
|
||||
def register_in(fileno):
|
||||
if fileno < 0:
|
||||
RNS.log(f"Attempt to register invalid file descriptor {fileno}", RNS.LOG_ERROR)
|
||||
return
|
||||
|
||||
try: BackboneInterface.epoll.register(fileno, select.EPOLLIN)
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while registering EPOLL_IN for file descriptor {fileno}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
@staticmethod
|
||||
def deregister_fileno(fileno):
|
||||
if fileno < 0:
|
||||
RNS.log(f"Attempt to deregister invalid file descriptor {fileno}", RNS.LOG_ERROR)
|
||||
return
|
||||
|
||||
try: BackboneInterface.epoll.unregister(fileno)
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while deregistering file descriptor {fileno}: {e}", RNS.LOG_DEBUG)
|
||||
|
||||
@staticmethod
|
||||
def deregister_listeners():
|
||||
for fileno in BackboneInterface.listener_filenos:
|
||||
owner_interface, server_socket = BackboneInterface.listener_filenos[fileno]
|
||||
fileno = server_socket.fileno()
|
||||
BackboneInterface.deregister_fileno(fileno)
|
||||
server_socket.close()
|
||||
|
||||
BackboneInterface.listener_filenos.clear()
|
||||
|
||||
@staticmethod
|
||||
def tx_ready(interface):
|
||||
if interface.socket:
|
||||
fileno = interface.socket.fileno()
|
||||
if fileno in BackboneInterface.spawned_interface_filenos:
|
||||
try:
|
||||
BackboneInterface.epoll.modify(interface.socket.fileno(), select.EPOLLOUT)
|
||||
except Exception as e:
|
||||
RNS.trace_exception(e)
|
||||
|
||||
@staticmethod
|
||||
def __job():
|
||||
with BackboneInterface._job_lock:
|
||||
if BackboneInterface._job_active: return
|
||||
else:
|
||||
BackboneInterface._job_active = True
|
||||
BackboneInterface.ensure_epoll()
|
||||
try:
|
||||
while True:
|
||||
events = BackboneInterface.epoll.poll(1)
|
||||
for fileno, event in BackboneInterface.epoll.poll(1):
|
||||
if fileno in BackboneInterface.spawned_interface_filenos:
|
||||
spawned_interface = BackboneInterface.spawned_interface_filenos[fileno]
|
||||
client_socket = spawned_interface.socket
|
||||
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)
|
||||
received_bytes = b""
|
||||
|
||||
if len(received_bytes): spawned_interface.receive(received_bytes)
|
||||
else:
|
||||
BackboneInterface.deregister_fileno(fileno); client_socket.close()
|
||||
try:
|
||||
if fileno in BackboneInterface.spawned_interface_filenos: BackboneInterface.spawned_interface_filenos.pop(fileno)
|
||||
except Exception as e: RNS.log(f"Error while removing spawned interface file descriptor from BackboneInterface I/O handler: {e}", RNS.LOG_ERROR)
|
||||
|
||||
try:
|
||||
if spawned_interface.parent_interface:
|
||||
pif = spawned_interface.parent_interface
|
||||
if pif.spawned_interfaces != None:
|
||||
while spawned_interface in pif.spawned_interfaces: pif.spawned_interfaces.remove(spawned_interface)
|
||||
except Exception as e: RNS.log(f"Error while removing spawned interface from {pif}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
spawned_interface.receive(received_bytes)
|
||||
|
||||
elif client_socket and fileno == client_socket.fileno() and (event & select.EPOLLOUT):
|
||||
try:
|
||||
written = client_socket.send(spawned_interface.transmit_buffer)
|
||||
except Exception as e:
|
||||
written = 0
|
||||
if not spawned_interface.detached: RNS.log(f"Error while writing to {spawned_interface}: {e}", RNS.LOG_DEBUG)
|
||||
BackboneInterface.deregister_fileno(fileno)
|
||||
|
||||
try:
|
||||
if fileno in BackboneInterface.spawned_interface_filenos: BackboneInterface.spawned_interface_filenos.pop(fileno)
|
||||
except Exception as e: RNS.log(f"Error while removing spawned interface file descriptor from BackboneInterface I/O handler: {e}", RNS.LOG_ERROR)
|
||||
|
||||
try:
|
||||
if spawned_interface.parent_interface:
|
||||
pif = spawned_interface.parent_interface
|
||||
if pif.spawned_interfaces != None:
|
||||
while spawned_interface in pif.spawned_interfaces: pif.spawned_interfaces.remove(spawned_interface)
|
||||
except Exception as e: RNS.log(f"Error while removing spawned interface from {pif}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
try: client_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_ERROR)
|
||||
spawned_interface.receive(b"")
|
||||
|
||||
spawned_interface.transmit_buffer = spawned_interface.transmit_buffer[written:]
|
||||
if len(spawned_interface.transmit_buffer) == 0: BackboneInterface.epoll.modify(fileno, select.EPOLLIN)
|
||||
spawned_interface.txb += written
|
||||
if spawned_interface.parent_interface: spawned_interface.parent_interface.txb += written
|
||||
|
||||
elif client_socket and fileno == client_socket.fileno() and event & (select.EPOLLHUP):
|
||||
BackboneInterface.deregister_fileno(fileno)
|
||||
try:
|
||||
if fileno in BackboneInterface.spawned_interface_filenos: BackboneInterface.spawned_interface_filenos.pop(fileno)
|
||||
except Exception as e: RNS.log(f"Error while removing spawned interface file descriptor from BackboneInterface I/O handler: {e}", RNS.LOG_ERROR)
|
||||
|
||||
try:
|
||||
if spawned_interface.parent_interface:
|
||||
pif = spawned_interface.parent_interface
|
||||
if pif.spawned_interfaces != None:
|
||||
while spawned_interface in pif.spawned_interfaces: pif.spawned_interfaces.remove(spawned_interface)
|
||||
except Exception as e: RNS.log(f"Error while removing spawned interface from {pif}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
try: client_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing socket for {spawned_interface}: {e}", RNS.LOG_ERROR)
|
||||
spawned_interface.receive(b"")
|
||||
|
||||
elif fileno in BackboneInterface.listener_filenos:
|
||||
owner_interface, server_socket = BackboneInterface.listener_filenos[fileno]
|
||||
if fileno == server_socket.fileno() and (event & select.EPOLLIN):
|
||||
client_socket, address = server_socket.accept()
|
||||
client_socket.setblocking(0)
|
||||
if not owner_interface.incoming_connection(client_socket):
|
||||
try: client_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_ERROR)
|
||||
|
||||
elif fileno == server_socket.fileno() and (event & select.EPOLLHUP):
|
||||
try: BackboneInterface.deregister_fileno(fileno)
|
||||
except Exception as e: RNS.log(f"Error while deregistering listener file descriptor {fileno}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
try: server_socket.close()
|
||||
except Exception as e: RNS.log(f"Error while closing listener socket for {server_socket}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"BackboneInterface error: {e}", RNS.LOG_ERROR)
|
||||
RNS.trace_exception(e)
|
||||
|
||||
finally:
|
||||
BackboneInterface.deregister_listeners()
|
||||
|
||||
def incoming_connection(self, socket):
|
||||
RNS.log("Accepting incoming connection", RNS.LOG_VERBOSE)
|
||||
try:
|
||||
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
|
||||
spawned_interface.IN = self.IN
|
||||
spawned_interface.socket = socket
|
||||
spawned_interface.target_ip = socket.getpeername()[0]
|
||||
spawned_interface.target_port = str(socket.getpeername()[1])
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
spawned_interface.optimise_mtu()
|
||||
|
||||
spawned_interface.ifac_size = self.ifac_size
|
||||
spawned_interface.ifac_netname = self.ifac_netname
|
||||
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"))
|
||||
|
||||
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_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))
|
||||
|
||||
spawned_interface.announce_rate_target = self.announce_rate_target
|
||||
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.HW_MTU = self.HW_MTU
|
||||
spawned_interface.online = True
|
||||
RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE)
|
||||
RNS.Transport.interfaces.append(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)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while accepting incoming connection on {self}: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def received_announce(self, from_spawned=False):
|
||||
if from_spawned: self.ia_freq_deque.append(time.time())
|
||||
|
||||
def sent_announce(self, from_spawned=False):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def process_outgoing(self, data):
|
||||
pass
|
||||
|
||||
def detach(self):
|
||||
self.detached = True
|
||||
self.online = False
|
||||
detached = []
|
||||
for fileno in BackboneInterface.listener_filenos:
|
||||
owner_interface, listener_socket = BackboneInterface.listener_filenos[fileno]
|
||||
if owner_interface == self:
|
||||
if hasattr(listener_socket, "shutdown"):
|
||||
if callable(listener_socket.shutdown):
|
||||
try: listener_socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
if str(e).endswith("Transport endpoint is not connected"): 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}"
|
||||
|
||||
return "BackboneInterface["+self.name+"/"+ip_str+":"+str(self.bind_port)+"]"
|
||||
|
||||
|
||||
class BackboneClientInterface(Interface):
|
||||
BITRATE_GUESS = 100_000_000
|
||||
DEFAULT_IFAC_SIZE = 16
|
||||
AUTOCONFIGURE_MTU = True
|
||||
|
||||
RECONNECT_WAIT = 5
|
||||
RECONNECT_MAX_TRIES = None
|
||||
|
||||
# TCP socket options
|
||||
TCP_USER_TIMEOUT = 24
|
||||
TCP_PROBE_AFTER = 5
|
||||
TCP_PROBE_INTERVAL = 2
|
||||
TCP_PROBES = 12
|
||||
|
||||
INITIAL_CONNECT_TIMEOUT = 5
|
||||
SYNCHRONOUS_START = True
|
||||
|
||||
def __init__(self, owner, configuration, connected_socket=None):
|
||||
super().__init__()
|
||||
|
||||
c = Interface.get_config_obj(configuration)
|
||||
name = c["name"]
|
||||
target_ip = c["target_host"] if "target_host" in c and c["target_host"] != None else None
|
||||
target_port = int(c["target_port"]) if "target_port" in c and c["target_host"] != None else None
|
||||
i2p_tunneled = c.as_bool("i2p_tunneled") if "i2p_tunneled" in c else False
|
||||
connect_timeout = c.as_int("connect_timeout") if "connect_timeout" in c else None
|
||||
max_reconnect_tries = c.as_int("max_reconnect_tries") if "max_reconnect_tries" in c else None
|
||||
prefer_ipv6 = c.as_bool("prefer_ipv6") if "prefer_ipv6" in c else False
|
||||
|
||||
self.HW_MTU = BackboneInterface.HW_MTU
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.socket = None
|
||||
self.parent_interface = None
|
||||
self.name = name
|
||||
self.initiator = False
|
||||
self.reconnecting = False
|
||||
self.never_connected = True
|
||||
self.owner = owner
|
||||
self.online = False
|
||||
self.detached = False
|
||||
self.prefer_ipv6 = prefer_ipv6
|
||||
self.i2p_tunneled = i2p_tunneled
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
self.bitrate = BackboneClientInterface.BITRATE_GUESS
|
||||
self.frame_buffer = b""
|
||||
self.transmit_buffer = b""
|
||||
|
||||
if max_reconnect_tries == None:
|
||||
self.max_reconnect_tries = BackboneClientInterface.RECONNECT_MAX_TRIES
|
||||
else:
|
||||
self.max_reconnect_tries = max_reconnect_tries
|
||||
|
||||
if connected_socket != None:
|
||||
self.receives = True
|
||||
self.target_ip = None
|
||||
self.target_port = None
|
||||
self.socket = connected_socket
|
||||
|
||||
self.set_timeouts_linux()
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
elif target_ip != None and target_port != None:
|
||||
self.receives = True
|
||||
self.target_ip = target_ip
|
||||
self.target_port = target_port
|
||||
self.initiator = True
|
||||
|
||||
if connect_timeout != None:
|
||||
self.connect_timeout = connect_timeout
|
||||
else:
|
||||
self.connect_timeout = BackboneClientInterface.INITIAL_CONNECT_TIMEOUT
|
||||
|
||||
if BackboneClientInterface.SYNCHRONOUS_START:
|
||||
self.initial_connect()
|
||||
else:
|
||||
thread = threading.Thread(target=self.initial_connect)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def initial_connect(self):
|
||||
if not self.connect(initial=True):
|
||||
thread = threading.Thread(target=self.reconnect)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
else:
|
||||
self.wants_tunnel = True
|
||||
|
||||
def set_timeouts_linux(self):
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(BackboneClientInterface.TCP_USER_TIMEOUT * 1000))
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(BackboneClientInterface.TCP_PROBE_AFTER))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(BackboneClientInterface.TCP_PROBE_INTERVAL))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(BackboneClientInterface.TCP_PROBES))
|
||||
|
||||
def detach(self):
|
||||
self.online = False
|
||||
if self.socket != None:
|
||||
if hasattr(self.socket, "close"):
|
||||
if callable(self.socket.close):
|
||||
self.detached = True
|
||||
|
||||
try:
|
||||
if self.socket != None: self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except Exception as e:
|
||||
if str(e).endswith("Transport endpoint is not connected"): pass
|
||||
else: RNS.log("Error while shutting down socket for "+str(self)+": "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
try:
|
||||
if self.socket != None: self.socket.close()
|
||||
except Exception as e: RNS.log("Error while closing socket for "+str(self)+": "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
self.socket = None
|
||||
|
||||
def connect(self, initial=False):
|
||||
try:
|
||||
if initial:
|
||||
RNS.log("Establishing TCP connection for "+str(self)+"...", RNS.LOG_DEBUG)
|
||||
|
||||
address_infos = socket.getaddrinfo(self.target_ip, self.target_port, proto=socket.IPPROTO_TCP)
|
||||
address_info = address_infos[0]
|
||||
for entry in address_infos:
|
||||
if self.prefer_ipv6 and entry[0] == socket.AF_INET6:
|
||||
address_info = entry; break
|
||||
elif not self.prefer_ipv6 and entry[0] == socket.AF_INET:
|
||||
address_info = entry; break
|
||||
|
||||
address_family = address_info[0]
|
||||
target_address = address_info[4]
|
||||
|
||||
self.socket = socket.socket(address_family, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(BackboneClientInterface.INITIAL_CONNECT_TIMEOUT)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.socket.connect(target_address)
|
||||
self.socket.settimeout(None)
|
||||
|
||||
BackboneInterface.add_client_socket(self.socket, self)
|
||||
self.online = True
|
||||
|
||||
if initial:
|
||||
RNS.log("TCP connection for "+str(self)+" established", RNS.LOG_DEBUG)
|
||||
|
||||
except Exception as e:
|
||||
if initial:
|
||||
RNS.log("Initial connection for "+str(self)+" could not be established: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("Leaving unconnected and retrying connection in "+str(BackboneClientInterface.RECONNECT_WAIT)+" seconds.", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
else:
|
||||
raise e
|
||||
|
||||
self.set_timeouts_linux()
|
||||
|
||||
self.online = True
|
||||
self.never_connected = False
|
||||
|
||||
return True
|
||||
|
||||
def reconnect(self):
|
||||
if self.initiator:
|
||||
if not self.reconnecting:
|
||||
self.reconnecting = True
|
||||
attempts = 0
|
||||
while not self.online and not self.detached:
|
||||
time.sleep(BackboneClientInterface.RECONNECT_WAIT)
|
||||
attempts += 1
|
||||
|
||||
if self.max_reconnect_tries != None and attempts > self.max_reconnect_tries:
|
||||
RNS.log("Max reconnection attempts reached for "+str(self), RNS.LOG_ERROR)
|
||||
self.teardown()
|
||||
break
|
||||
|
||||
try: self.connect()
|
||||
except Exception as e:
|
||||
RNS.log("Connection attempt for "+str(self)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
|
||||
if not self.online: return
|
||||
|
||||
if not self.never_connected:
|
||||
RNS.log("Reconnected socket for "+str(self)+".", RNS.LOG_INFO)
|
||||
|
||||
self.reconnecting = False
|
||||
RNS.Transport.synthesize_tunnel(self)
|
||||
|
||||
else:
|
||||
RNS.log("Attempt to reconnect on a non-initiator TCP interface. This should not happen.", RNS.LOG_ERROR)
|
||||
raise IOError("Attempt to reconnect on a non-initiator TCP interface")
|
||||
|
||||
def process_incoming(self, data):
|
||||
if self.online and not self.detached:
|
||||
self.rxb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.rxb += len(data)
|
||||
|
||||
self.owner.inbound(data, self)
|
||||
|
||||
def process_outgoing(self, data):
|
||||
if self.online and not self.detached:
|
||||
try:
|
||||
self.transmit_buffer += bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
BackboneInterface.tx_ready(self)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Exception occurred while transmitting via "+str(self)+", tearing down interface", RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
self.teardown()
|
||||
|
||||
def receive(self, data_in):
|
||||
try:
|
||||
if len(data_in) > 0:
|
||||
self.frame_buffer += data_in
|
||||
flags_remaining = True
|
||||
while flags_remaining:
|
||||
frame_start = self.frame_buffer.find(HDLC.FLAG)
|
||||
if frame_start != -1:
|
||||
frame_end = self.frame_buffer.find(HDLC.FLAG, frame_start+1)
|
||||
if frame_end != -1:
|
||||
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)
|
||||
self.frame_buffer = self.frame_buffer[frame_end:]
|
||||
else:
|
||||
flags_remaining = False
|
||||
else:
|
||||
flags_remaining = False
|
||||
|
||||
else:
|
||||
self.online = False
|
||||
if self.initiator and not self.detached:
|
||||
RNS.log("The socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
|
||||
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_VERBOSE)
|
||||
self.teardown()
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
RNS.log("An interface error occurred for "+str(self)+", the contained exception was: "+str(e), RNS.LOG_WARNING)
|
||||
|
||||
if self.initiator:
|
||||
RNS.log("Attempting to reconnect...", RNS.LOG_WARNING)
|
||||
def job(): self.reconnect()
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
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()
|
||||
|
||||
else:
|
||||
RNS.log("The interface "+str(self)+" is being torn down.", RNS.LOG_VERBOSE)
|
||||
|
||||
self.online = False
|
||||
self.OUT = False
|
||||
self.IN = False
|
||||
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
while self in self.parent_interface.spawned_interfaces:
|
||||
self.parent_interface.spawned_interfaces.remove(self)
|
||||
|
||||
if self in RNS.Transport.interfaces:
|
||||
if not self.initiator:
|
||||
RNS.Transport.interfaces.remove(self)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
if ":" in self.target_ip: ip_str = f"[{self.target_ip}]"
|
||||
else: ip_str = f"{self.target_ip}"
|
||||
return "BackboneInterface["+str(self.name)+"/"+ip_str+":"+str(self.target_port)+"]"
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -872,6 +880,7 @@ class I2PInterface(Interface):
|
||||
self.ifac_size = ifac_size
|
||||
self.ifac_netname = ifac_netname
|
||||
self.ifac_netkey = ifac_netkey
|
||||
self.supports_discovery = True
|
||||
|
||||
self.online = False
|
||||
|
||||
|
||||
+35
-14
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -65,6 +73,7 @@ class Interface:
|
||||
IC_HELD_RELEASE_INTERVAL = 30
|
||||
|
||||
AUTOCONFIGURE_MTU = False
|
||||
FIXED_MTU = False
|
||||
|
||||
def __init__(self):
|
||||
self.rxb = 0
|
||||
@@ -75,6 +84,13 @@ class Interface:
|
||||
self.bitrate = 62500
|
||||
self.HW_MTU = None
|
||||
|
||||
self.supports_discovery = False
|
||||
self.discoverable = False
|
||||
self.last_discovery_announce = 0
|
||||
self.bootstrap_only = False
|
||||
self.parent_interface = None
|
||||
self.spawned_interfaces = None
|
||||
self.tunnel_id = None
|
||||
self.ingress_control = True
|
||||
self.ic_max_held_announces = Interface.MAX_HELD_ANNOUNCES
|
||||
self.ic_burst_hold = Interface.IC_BURST_HOLD
|
||||
@@ -123,21 +139,23 @@ class Interface:
|
||||
|
||||
def optimise_mtu(self):
|
||||
if self.AUTOCONFIGURE_MTU:
|
||||
if self.bitrate > 16_000_000:
|
||||
if self.bitrate >= 1_000_000_000:
|
||||
self.HW_MTU = 524288
|
||||
elif self.bitrate > 750_000_000:
|
||||
self.HW_MTU = 262144
|
||||
elif self.bitrate > 8_000_000:
|
||||
elif self.bitrate > 400_000_000:
|
||||
self.HW_MTU = 131072
|
||||
elif self.bitrate > 4_000_000:
|
||||
elif self.bitrate > 200_000_000:
|
||||
self.HW_MTU = 65536
|
||||
elif self.bitrate > 2_000_000:
|
||||
elif self.bitrate > 100_000_000:
|
||||
self.HW_MTU = 32768
|
||||
elif self.bitrate > 1_000_000:
|
||||
elif self.bitrate > 10_000_000:
|
||||
self.HW_MTU = 16384
|
||||
elif self.bitrate > 500_000:
|
||||
elif self.bitrate > 5_000_000:
|
||||
self.HW_MTU = 8192
|
||||
elif self.bitrate > 250_000:
|
||||
elif self.bitrate > 2_000_000:
|
||||
self.HW_MTU = 4096
|
||||
elif self.bitrate > 125_000:
|
||||
elif self.bitrate > 1_000_000:
|
||||
self.HW_MTU = 2048
|
||||
elif self.bitrate > 62_500:
|
||||
self.HW_MTU = 1024
|
||||
@@ -181,12 +199,12 @@ class Interface:
|
||||
RNS.log("An error occurred while processing held announces for "+str(self), RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def received_announce(self):
|
||||
def received_announce(self, from_spawned=False):
|
||||
self.ia_freq_deque.append(time.time())
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.received_announce(from_spawned=True)
|
||||
|
||||
def sent_announce(self):
|
||||
def sent_announce(self, from_spawned=False):
|
||||
self.oa_freq_deque.append(time.time())
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.sent_announce(from_spawned=True)
|
||||
@@ -267,6 +285,9 @@ class Interface:
|
||||
RNS.log("Error while processing announce queue on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("The announce queue for this interface has been cleared.", RNS.LOG_ERROR)
|
||||
|
||||
def final_init(self):
|
||||
pass
|
||||
|
||||
def detach(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -63,7 +71,7 @@ class KISSInterface(Interface):
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
import importlib
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -21,6 +29,7 @@
|
||||
# SOFTWARE.
|
||||
|
||||
from RNS.Interfaces.Interface import Interface
|
||||
from RNS.Interfaces.BackboneInterface import BackboneInterface
|
||||
import socketserver
|
||||
import threading
|
||||
import socket
|
||||
@@ -54,12 +63,15 @@ class LocalClientInterface(Interface):
|
||||
RECONNECT_WAIT = 8
|
||||
AUTOCONFIGURE_MTU = True
|
||||
|
||||
def __init__(self, owner, name, target_port = None, connected_socket=None):
|
||||
def __init__(self, owner, name, target_port = None, connected_socket=None, socket_path=None):
|
||||
super().__init__()
|
||||
|
||||
self.HW_MTU = 262144
|
||||
|
||||
self.online = False
|
||||
self.epoll_backend = False
|
||||
self.HW_MTU = 262144
|
||||
self.online = False
|
||||
|
||||
if socket_path != None and RNS.Reticulum.get_instance().use_af_unix: self.socket_path = f"\0rns/{socket_path}"
|
||||
else: self.socket_path = None
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
@@ -70,16 +82,29 @@ class LocalClientInterface(Interface):
|
||||
self.detached = False
|
||||
self.name = name
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
self.frame_buffer = b""
|
||||
self.transmit_buffer = b""
|
||||
|
||||
if RNS.vendor.platformutils.use_epoll():
|
||||
self.epoll_backend = True
|
||||
|
||||
if connected_socket != None:
|
||||
self.receives = True
|
||||
self.target_ip = None
|
||||
self.target_port = None
|
||||
self.socket = connected_socket
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
if self.socket.family == socket.AF_INET:
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
self.is_connected_to_shared_instance = False
|
||||
|
||||
elif self.socket_path != None:
|
||||
self.receives = True
|
||||
self.target_ip = None
|
||||
self.target_port = None
|
||||
self.connect()
|
||||
|
||||
elif target_port != None:
|
||||
self.receives = True
|
||||
self.target_ip = "127.0.0.1"
|
||||
@@ -98,22 +123,30 @@ class LocalClientInterface(Interface):
|
||||
self.announce_rate_penalty = None
|
||||
|
||||
if connected_socket == None:
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
if not self.epoll_backend:
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
def connect(self):
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.socket.connect((self.target_ip, self.target_port))
|
||||
if self.socket_path != None:
|
||||
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.socket.connect(self.socket_path)
|
||||
|
||||
else:
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.socket.connect((self.target_ip, self.target_port))
|
||||
|
||||
self.online = True
|
||||
self.is_connected_to_shared_instance = True
|
||||
self.never_connected = False
|
||||
|
||||
if self.epoll_backend: BackboneInterface.add_client_socket(self.socket, self)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -137,9 +170,11 @@ class LocalClientInterface(Interface):
|
||||
RNS.log("Reconnected socket for "+str(self)+".", RNS.LOG_INFO)
|
||||
|
||||
self.reconnecting = False
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
if not self.epoll_backend:
|
||||
thread = threading.Thread(target=self.read_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def job():
|
||||
time.sleep(LocalClientInterface.RECONNECT_WAIT+2)
|
||||
RNS.Transport.shared_connection_reappeared()
|
||||
@@ -152,8 +187,7 @@ class LocalClientInterface(Interface):
|
||||
|
||||
def process_incoming(self, data):
|
||||
self.rxb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.rxb += len(data)
|
||||
if self.parent_interface != None: self.parent_interface.rxb += len(data)
|
||||
|
||||
try:
|
||||
self.owner.inbound(data, self)
|
||||
@@ -164,23 +198,28 @@ class LocalClientInterface(Interface):
|
||||
def process_outgoing(self, data):
|
||||
if self.online:
|
||||
try:
|
||||
self.writing = True
|
||||
if self.epoll_backend:
|
||||
self.transmit_buffer += bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
BackboneInterface.tx_ready(self)
|
||||
|
||||
if self._force_bitrate:
|
||||
if not hasattr(self, "send_lock"):
|
||||
self.send_lock = Lock()
|
||||
else:
|
||||
self.writing = True
|
||||
|
||||
with self.send_lock:
|
||||
# RNS.log(f"Simulating latency of {RNS.prettytime(s)} for {len(data)} bytes", RNS.LOG_EXTREME)
|
||||
s = len(data) / self.bitrate * 8
|
||||
time.sleep(s)
|
||||
if self._force_bitrate:
|
||||
if not hasattr(self, "send_lock"):
|
||||
self.send_lock = Lock()
|
||||
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
self.socket.sendall(data)
|
||||
self.writing = False
|
||||
self.txb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
self.parent_interface.txb += len(data)
|
||||
with self.send_lock:
|
||||
# RNS.log(f"Simulating latency of {RNS.prettytime(s)} for {len(data)} bytes", RNS.LOG_EXTREME)
|
||||
s = len(data) / self.bitrate * 8
|
||||
time.sleep(s)
|
||||
|
||||
data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
|
||||
self.socket.sendall(data)
|
||||
self.writing = False
|
||||
self.txb += len(data)
|
||||
if hasattr(self, "parent_interface") and self.parent_interface != None:
|
||||
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)
|
||||
@@ -188,48 +227,67 @@ class LocalClientInterface(Interface):
|
||||
RNS.trace_exception(e)
|
||||
self.teardown()
|
||||
|
||||
def handle_hdlc(self, data_in):
|
||||
self.frame_buffer += data_in
|
||||
flags_remaining = True
|
||||
while flags_remaining:
|
||||
frame_start = self.frame_buffer.find(HDLC.FLAG)
|
||||
if frame_start != -1:
|
||||
frame_end = self.frame_buffer.find(HDLC.FLAG, frame_start+1)
|
||||
if frame_end != -1:
|
||||
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)
|
||||
self.frame_buffer = self.frame_buffer[frame_end:]
|
||||
else:
|
||||
flags_remaining = False
|
||||
else:
|
||||
flags_remaining = False
|
||||
|
||||
def receive(self, data_in):
|
||||
try:
|
||||
if len(data_in) > 0: self.handle_hdlc(data_in)
|
||||
else:
|
||||
self.online = False
|
||||
if self.is_connected_to_shared_instance and not self.detached:
|
||||
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
|
||||
RNS.Transport.shared_connection_disappeared()
|
||||
# TODO: Potentially run this in a thread, but since if we get here,
|
||||
# there's no other connectivity left to block anyway, it might be
|
||||
# unnecessary.
|
||||
self.reconnect()
|
||||
else:
|
||||
self.teardown(nowarning=True)
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
RNS.log("An interface error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
RNS.log("Tearing down "+str(self), RNS.LOG_ERROR)
|
||||
self.teardown()
|
||||
|
||||
def read_loop(self):
|
||||
try:
|
||||
in_frame = False
|
||||
escape = False
|
||||
frame_buffer = b""
|
||||
self.frame_buffer = b""
|
||||
data_in = b""
|
||||
data_buffer = b""
|
||||
|
||||
while True:
|
||||
data_in = self.socket.recv(4096)
|
||||
if len(data_in) > 0:
|
||||
frame_buffer += data_in
|
||||
flags_remaining = True
|
||||
while flags_remaining:
|
||||
frame_start = frame_buffer.find(HDLC.FLAG)
|
||||
if frame_start != -1:
|
||||
frame_end = frame_buffer.find(HDLC.FLAG, frame_start+1)
|
||||
if frame_end != -1:
|
||||
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_buffer = frame_buffer[frame_end:]
|
||||
else:
|
||||
flags_remaining = False
|
||||
else:
|
||||
flags_remaining = False
|
||||
|
||||
if len(data_in) > 0: self.handle_hdlc(data_in)
|
||||
else:
|
||||
self.online = False
|
||||
if self.is_connected_to_shared_instance and not self.detached:
|
||||
RNS.log("Socket for "+str(self)+" was closed, attempting to reconnect...", RNS.LOG_WARNING)
|
||||
RNS.Transport.shared_connection_disappeared()
|
||||
# TODO: Potentially run this in a thread, but since if we get here,
|
||||
# there's no other connectivity left to block anyway, it might be
|
||||
# unnecessary.
|
||||
self.reconnect()
|
||||
else:
|
||||
self.teardown(nowarning=True)
|
||||
|
||||
break
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.online = False
|
||||
RNS.log("An interface error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
@@ -285,69 +343,113 @@ class LocalClientInterface(Interface):
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "LocalInterface["+str(self.target_port)+"]"
|
||||
if self.socket_path: return "LocalInterface["+str(self.socket_path.replace("\0", ""))+"]"
|
||||
else: return "LocalInterface["+str(self.target_port)+"]"
|
||||
|
||||
|
||||
class LocalServerInterface(Interface):
|
||||
AUTOCONFIGURE_MTU = True
|
||||
|
||||
def __init__(self, owner, bindport=None):
|
||||
def __init__(self, owner, bindport=None, socket_path=None):
|
||||
super().__init__()
|
||||
self.epoll_backend = False
|
||||
self.online = False
|
||||
self.clients = 0
|
||||
|
||||
if socket_path != None and RNS.Reticulum.get_instance().use_af_unix: self.socket_path = f"\0rns/{socket_path}"
|
||||
else: self.socket_path = None
|
||||
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.name = "Reticulum"
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
|
||||
if (bindport != None):
|
||||
if RNS.vendor.platformutils.use_epoll():
|
||||
self.epoll_backend = True
|
||||
|
||||
if socket_path != None and self.epoll_backend:
|
||||
self.receives = True
|
||||
self.bind_ip = None
|
||||
self.bind_port = None
|
||||
|
||||
self.owner = owner
|
||||
self.is_local_shared_instance = True
|
||||
BackboneInterface.add_listener(self, self.socket_path, socket_type=socket.AF_UNIX)
|
||||
|
||||
elif bindport != None:
|
||||
self.receives = True
|
||||
self.bind_ip = "127.0.0.1"
|
||||
self.bind_port = bindport
|
||||
|
||||
def handlerFactory(callback):
|
||||
def createHandler(*args, **keys):
|
||||
return LocalInterfaceHandler(callback, *args, **keys)
|
||||
return createHandler
|
||||
|
||||
self.owner = owner
|
||||
self.is_local_shared_instance = True
|
||||
|
||||
address = (self.bind_ip, self.bind_port)
|
||||
if self.epoll_backend: BackboneInterface.add_listener(self, address)
|
||||
else:
|
||||
def handlerFactory(callback):
|
||||
def createHandler(*args, **keys):
|
||||
return LocalInterfaceHandler(callback, *args, **keys)
|
||||
return createHandler
|
||||
|
||||
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
|
||||
self.server.daemon_threads = True
|
||||
|
||||
thread = threading.Thread(target=self.server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
self.announce_rate_target = None
|
||||
self.announce_rate_grace = None
|
||||
self.announce_rate_penalty = None
|
||||
|
||||
self.bitrate = 1000*1000*1000
|
||||
self.online = True
|
||||
self.server = ThreadingTCPServer(address, handlerFactory(self.incoming_connection))
|
||||
self.server.daemon_threads = True
|
||||
thread = threading.Thread(target=self.server.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
self.announce_rate_target = None
|
||||
self.announce_rate_grace = None
|
||||
self.announce_rate_penalty = None
|
||||
|
||||
self.bitrate = 1000*1000*1000
|
||||
self.online = True
|
||||
|
||||
def incoming_connection(self, handler):
|
||||
interface_name = str(str(handler.client_address[1]))
|
||||
spawned_interface = LocalClientInterface(self.owner, name=interface_name, connected_socket=handler.request)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
spawned_interface.target_ip = handler.client_address[0]
|
||||
spawned_interface.target_port = str(handler.client_address[1])
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
if hasattr(self, "_force_bitrate"):
|
||||
spawned_interface._force_bitrate = self._force_bitrate
|
||||
# RNS.log("Accepting new connection to shared instance: "+str(spawned_interface), RNS.LOG_EXTREME)
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.local_client_interfaces.append(spawned_interface)
|
||||
self.clients += 1
|
||||
spawned_interface.read_loop()
|
||||
if self.epoll_backend:
|
||||
client_socket = handler
|
||||
if client_socket.family == socket.AF_INET:
|
||||
interface_name = str(str(client_socket.getpeername()[1]))
|
||||
elif client_socket.family == socket.AF_UNIX:
|
||||
interface_name = f"{self.clients}@{self.socket_path}"
|
||||
|
||||
spawned_interface = LocalClientInterface(self.owner, name=interface_name, connected_socket=client_socket)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
spawned_interface.socket = client_socket
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
|
||||
if client_socket.family == socket.AF_INET:
|
||||
spawned_interface.target_ip = client_socket.getpeername()[0]
|
||||
spawned_interface.target_port = str(client_socket.getpeername()[1])
|
||||
|
||||
elif client_socket.family == socket.AF_UNIX:
|
||||
spawned_interface.target_ip = None
|
||||
spawned_interface.target_port = interface_name
|
||||
spawned_interface.socket_path = self.socket_path
|
||||
|
||||
if hasattr(self, "_force_bitrate"): spawned_interface._force_bitrate = self._force_bitrate
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.local_client_interfaces.append(spawned_interface)
|
||||
BackboneInterface.add_client_socket(client_socket, spawned_interface)
|
||||
self.clients += 1
|
||||
return True
|
||||
|
||||
else:
|
||||
interface_name = str(str(handler.client_address[1]))
|
||||
spawned_interface = LocalClientInterface(self.owner, name=interface_name, connected_socket=handler.request)
|
||||
spawned_interface.OUT = self.OUT
|
||||
spawned_interface.IN = self.IN
|
||||
spawned_interface.target_ip = handler.client_address[0]
|
||||
spawned_interface.target_port = str(handler.client_address[1])
|
||||
spawned_interface.parent_interface = self
|
||||
spawned_interface.bitrate = self.bitrate
|
||||
if hasattr(self, "_force_bitrate"): spawned_interface._force_bitrate = self._force_bitrate
|
||||
RNS.Transport.interfaces.append(spawned_interface)
|
||||
RNS.Transport.local_client_interfaces.append(spawned_interface)
|
||||
self.clients += 1
|
||||
spawned_interface.read_loop()
|
||||
|
||||
def process_outgoing(self, data):
|
||||
pass
|
||||
@@ -359,7 +461,8 @@ class LocalServerInterface(Interface):
|
||||
if from_spawned: self.oa_freq_deque.append(time.time())
|
||||
|
||||
def __str__(self):
|
||||
return "Shared Instance["+str(self.bind_port)+"]"
|
||||
if self.socket_path: return "Shared Instance["+str(self.socket_path.replace("\0", ""))+"]"
|
||||
else: return "Shared Instance["+str(self.bind_port)+"]"
|
||||
|
||||
class LocalInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, callback, *args, **keys):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -24,6 +32,7 @@ from RNS.Interfaces.Interface import Interface
|
||||
from time import sleep
|
||||
import sys
|
||||
import threading
|
||||
import socket
|
||||
import time
|
||||
import math
|
||||
import RNS
|
||||
@@ -56,6 +65,7 @@ class KISS():
|
||||
CMD_STAT_PHYPRM = 0x26
|
||||
CMD_STAT_BAT = 0x27
|
||||
CMD_STAT_CSMA = 0x28
|
||||
CMD_STAT_TEMP = 0x29
|
||||
CMD_BLINK = 0x30
|
||||
CMD_RANDOM = 0x40
|
||||
CMD_FB_EXT = 0x41
|
||||
@@ -126,7 +136,7 @@ class RNodeInterface(Interface):
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
raise SystemError("Invalid interface type. The Android-specific RNode interface must be used on Android")
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
@@ -150,8 +160,11 @@ class RNodeInterface(Interface):
|
||||
lt_alock = float(c["airtime_limit_long"]) if "airtime_limit_long" in c else None
|
||||
|
||||
force_ble = False
|
||||
ble_name = None
|
||||
ble_addr = None
|
||||
ble_name = None
|
||||
ble_addr = None
|
||||
|
||||
force_tcp = False
|
||||
tcp_host = None
|
||||
|
||||
port = c["port"] if "port" in c else None
|
||||
|
||||
@@ -159,6 +172,7 @@ class RNodeInterface(Interface):
|
||||
raise ValueError("No port specified for RNode interface")
|
||||
|
||||
if port != None:
|
||||
tcp_uri_scheme = "tcp://"
|
||||
ble_uri_scheme = "ble://"
|
||||
if port.lower().startswith(ble_uri_scheme):
|
||||
force_ble = True
|
||||
@@ -171,6 +185,13 @@ class RNodeInterface(Interface):
|
||||
else:
|
||||
ble_name = ble_string
|
||||
|
||||
elif port.lower().startswith(tcp_uri_scheme):
|
||||
force_tcp = True
|
||||
tcp_string = port[len(tcp_uri_scheme):]
|
||||
port = None
|
||||
if len(tcp_string) == 0: pass
|
||||
else: tcp_host = tcp_string
|
||||
|
||||
self.HW_MTU = 508
|
||||
|
||||
self.pyserial = serial
|
||||
@@ -187,6 +208,14 @@ class RNodeInterface(Interface):
|
||||
self.reconnecting= False
|
||||
self.hw_errors = []
|
||||
|
||||
self.use_tcp = False
|
||||
self.tcp = None
|
||||
self.tcp_host = tcp_host
|
||||
self.tcp_rx_queue= b""
|
||||
self.tcp_tx_queue= b""
|
||||
self.tcp_rx_lock = threading.Lock()
|
||||
self.tcp_tx_lock = threading.Lock()
|
||||
|
||||
self.use_ble = False
|
||||
self.ble_name = ble_name
|
||||
self.ble_addr = ble_addr
|
||||
@@ -205,6 +234,7 @@ class RNodeInterface(Interface):
|
||||
self.bitrate = 0
|
||||
self.st_alock = st_alock
|
||||
self.lt_alock = lt_alock
|
||||
self.cpu_temp = None
|
||||
self.platform = None
|
||||
self.display = None
|
||||
self.mcu = None
|
||||
@@ -246,9 +276,12 @@ class RNodeInterface(Interface):
|
||||
self.r_csma_cw_max = None
|
||||
self.r_current_rssi = None
|
||||
self.r_noise_floor = None
|
||||
self.r_interference = None
|
||||
self.r_interference_l = None
|
||||
|
||||
self.r_battery_state = RNodeInterface.BATTERY_STATE_UNKNOWN
|
||||
self.r_battery_percent = 0
|
||||
self.r_temperature = None
|
||||
self.r_framebuffer = b""
|
||||
self.r_framebuffer_readtime = 0
|
||||
self.r_framebuffer_latency = 0
|
||||
@@ -263,16 +296,17 @@ class RNodeInterface(Interface):
|
||||
self.flow_control = flow_control
|
||||
self.interface_ready = False
|
||||
self.announce_rate_target = None
|
||||
self.supports_discovery = True
|
||||
|
||||
if force_ble or self.ble_addr != None or self.ble_name != None:
|
||||
self.use_ble = True
|
||||
if force_ble or self.ble_addr != None or self.ble_name != None: self.use_ble = True
|
||||
if force_tcp or self.tcp_host != None: self.use_tcp = True
|
||||
|
||||
self.validcfg = True
|
||||
if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
|
||||
RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
if (self.txpower < 0 or self.txpower > 22):
|
||||
if (self.txpower < 0 or self.txpower > 37):
|
||||
RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
@@ -314,10 +348,8 @@ class RNodeInterface(Interface):
|
||||
try:
|
||||
self.open_port()
|
||||
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
else:
|
||||
raise IOError("Could not open serial port")
|
||||
if self.serial.is_open: self.configure_device()
|
||||
else: raise IOError("Could not open serial port")
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
|
||||
@@ -330,8 +362,8 @@ class RNodeInterface(Interface):
|
||||
|
||||
|
||||
def open_port(self):
|
||||
if not self.use_ble:
|
||||
RNS.log("Opening serial port "+self.port+"...")
|
||||
if not self.use_ble and not self.use_tcp:
|
||||
RNS.log(f"Opening serial port {self.port}...")
|
||||
self.serial = self.pyserial.Serial(
|
||||
port = self.port,
|
||||
baudrate = self.speed,
|
||||
@@ -347,19 +379,37 @@ class RNodeInterface(Interface):
|
||||
)
|
||||
|
||||
else:
|
||||
RNS.log(f"Opening BLE connection for {self}...")
|
||||
if self.ble != None and self.ble.running == False:
|
||||
self.ble.close()
|
||||
self.ble.cleanup()
|
||||
self.ble = None
|
||||
if self.use_ble:
|
||||
RNS.log(f"Opening BLE connection for {self}...")
|
||||
self.timeout = 1250
|
||||
if self.ble != None and self.ble.running == False:
|
||||
self.ble.close()
|
||||
self.ble.cleanup()
|
||||
self.ble = None
|
||||
|
||||
if self.ble == None:
|
||||
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
|
||||
self.serial = self.ble
|
||||
if self.ble == None:
|
||||
self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
|
||||
self.serial = self.ble
|
||||
|
||||
open_time = time.time()
|
||||
while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
|
||||
time.sleep(1)
|
||||
open_time = time.time()
|
||||
while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
|
||||
time.sleep(1)
|
||||
|
||||
if self.use_tcp:
|
||||
self.timeout = 1500
|
||||
RNS.log(f"Opening TCP connection for {self}...")
|
||||
if self.tcp != None and self.tcp.running == False:
|
||||
self.tcp.close()
|
||||
self.tcp.cleanup()
|
||||
self.tcp = None
|
||||
|
||||
if self.tcp == None:
|
||||
self.tcp = TCPConnection(owner=self, target_host=self.tcp_host)
|
||||
self.serial = self.tcp
|
||||
|
||||
open_time = time.time()
|
||||
while not self.tcp.connected and time.time() < open_time + self.tcp.CONNECT_TIMEOUT:
|
||||
time.sleep(1)
|
||||
|
||||
def reset_radio_state(self):
|
||||
self.r_frequency = None
|
||||
@@ -380,38 +430,41 @@ class RNodeInterface(Interface):
|
||||
thread.start()
|
||||
|
||||
self.detect()
|
||||
if not self.use_ble:
|
||||
sleep(0.2)
|
||||
else:
|
||||
ble_detect_timeout = 5
|
||||
if self.use_tcp:
|
||||
tcp_detect_timeout = 5.0
|
||||
detect_time = time.time()
|
||||
while not self.detected and time.time() < detect_time + ble_detect_timeout:
|
||||
time.sleep(0.1)
|
||||
if self.detected:
|
||||
detect_time = RNS.prettytime(time.time()-detect_time)
|
||||
else:
|
||||
RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
|
||||
while not self.detected and time.time() < detect_time + tcp_detect_timeout: time.sleep(0.1)
|
||||
if not self.detected: RNS.log(f"RNode detect timed out over TCP", RNS.LOG_ERROR)
|
||||
elif self.use_ble:
|
||||
ble_detect_timeout = 5.0
|
||||
detect_time = time.time()
|
||||
while not self.detected and time.time() < detect_time + ble_detect_timeout: time.sleep(0.1)
|
||||
if not self.detected: RNS.log(f"RNode detect timed out over BLE", RNS.LOG_ERROR)
|
||||
else:
|
||||
sleep(0.2)
|
||||
|
||||
if not self.detected:
|
||||
RNS.log("Could not detect device for "+str(self), RNS.LOG_ERROR)
|
||||
RNS.log(f"Could not detect device for {self}", RNS.LOG_ERROR)
|
||||
self.serial.close()
|
||||
|
||||
else:
|
||||
if self.platform == KISS.PLATFORM_ESP32 or self.platform == KISS.PLATFORM_NRF52:
|
||||
self.display = True
|
||||
if self.platform == KISS.PLATFORM_ESP32 or self.platform == KISS.PLATFORM_NRF52: self.display = True
|
||||
|
||||
RNS.log("Serial port "+self.port+" is now open")
|
||||
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
|
||||
self.initRadio()
|
||||
if (self.validateRadioState()):
|
||||
self.interface_ready = True
|
||||
RNS.log(str(self)+" is configured and powered up")
|
||||
sleep(0.3)
|
||||
self.online = True
|
||||
else:
|
||||
RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
|
||||
RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
|
||||
RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
|
||||
self.serial.close()
|
||||
if self.use_tcp: RNS.log(f"TCP connection to {self.tcp_host} is now open", RNS.LOG_VERBOSE)
|
||||
elif self.use_ble: RNS.log(f"BLE connection to {self} is now open", RNS.LOG_VERBOSE)
|
||||
else: RNS.log(f"Serial port {self.port} is now open", RNS.LOG_VERBOSE)
|
||||
RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
|
||||
self.initRadio()
|
||||
if (self.validateRadioState()):
|
||||
self.interface_ready = True
|
||||
RNS.log(str(self)+" is configured and powered up")
|
||||
sleep(0.3)
|
||||
self.online = True
|
||||
else:
|
||||
RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
|
||||
RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
|
||||
RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
|
||||
self.serial.close()
|
||||
|
||||
|
||||
def initRadio(self):
|
||||
@@ -606,10 +659,9 @@ class RNodeInterface(Interface):
|
||||
|
||||
def validateRadioState(self):
|
||||
RNS.log("Waiting for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
if self.use_ble:
|
||||
sleep(1.00)
|
||||
else:
|
||||
sleep(0.25)
|
||||
if self.use_ble: sleep(1.00)
|
||||
elif self.use_tcp: sleep(1.5)
|
||||
else: sleep(0.25)
|
||||
|
||||
if self.use_ble and self.ble != None and self.ble.device_disappeared:
|
||||
RNS.log(f"Device disappeared during radio state validation for {self}", RNS.LOG_ERROR)
|
||||
@@ -894,16 +946,35 @@ class RNodeInterface(Interface):
|
||||
self.r_channel_load_long = cul/100.0
|
||||
self.r_current_rssi = crs-RNodeInterface.RSSI_OFFSET
|
||||
self.r_noise_floor = nfl-RNodeInterface.RSSI_OFFSET
|
||||
|
||||
# TODO: Remove debug
|
||||
# interference_log_threshold = 10
|
||||
# if ntf == 0xFF:
|
||||
# self.r_interference = None
|
||||
# if self.r_noise_floor != None:
|
||||
# # Filter potential false interference events due to LNA recalibration
|
||||
# if self.r_interference_l != None:
|
||||
# if self.r_interference_l[1] < self.r_noise_floor+interference_log_threshold:
|
||||
# self.r_interference_l = None
|
||||
# else:
|
||||
# if self.r_noise_floor != None:
|
||||
# interference = ntf-RNodeInterface.RSSI_OFFSET
|
||||
# # Filter potential false interference events due to LNA recalibration
|
||||
# if interference > self.r_noise_floor+interference_log_threshold:
|
||||
# self.r_interference = ntf-RNodeInterface.RSSI_OFFSET
|
||||
# self.r_interference_l = [time.time(), self.r_interference]
|
||||
|
||||
if ntf == 0xFF:
|
||||
self.r_interference = None
|
||||
else:
|
||||
self.r_interference = ntf-RNodeInterface.RSSI_OFFSET
|
||||
self.r_interference_l = [time.time(), self.r_interference]
|
||||
|
||||
if self.r_interference != None:
|
||||
RNS.log(f"{self} Radio detected interference at {self.r_interference} dBm", RNS.LOG_DEBUG)
|
||||
|
||||
# TODO: Remove debug
|
||||
# RNS.log(f"RSSI: {self.r_current_rssi}, Noise floor: {self.r_noise_floor}, Interference: {self.r_interference}", RNS.LOG_EXTREME)
|
||||
# RNS.log(f"RSSI: {self.r_current_rssi}, Noise floor: {self.r_noise_floor}, Interference: {self.r_interference}", RNS.LOG_DEBUG)
|
||||
elif (command == KISS.CMD_STAT_PHYPRM):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
@@ -977,6 +1048,22 @@ class RNodeInterface(Interface):
|
||||
bat_percent = 0
|
||||
self.r_battery_state = command_buffer[0]
|
||||
self.r_battery_percent = bat_percent
|
||||
elif (command == KISS.CMD_STAT_TEMP):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
command_buffer = command_buffer+bytes([byte])
|
||||
if (len(command_buffer) == 1):
|
||||
temp = command_buffer[0]-120
|
||||
if temp >= -30 and temp <= 90: self.r_temperature = temp
|
||||
else: self.r_temperature = None
|
||||
self.cpu_temp = self.r_temperature
|
||||
elif (command == KISS.CMD_RANDOM):
|
||||
self.r_random = byte
|
||||
elif (command == KISS.CMD_PLATFORM):
|
||||
@@ -1046,7 +1133,7 @@ class RNodeInterface(Interface):
|
||||
else:
|
||||
time_since_last = int(time.time()*1000) - last_read_ms
|
||||
if len(data_buffer) > 0 and time_since_last > self.timeout:
|
||||
RNS.log(str(self)+" serial read timeout in command "+str(command), RNS.LOG_WARNING)
|
||||
RNS.log(f"{self} device read timeout in command {command} after {RNS.prettytime(self.timeout/1000.0)}", RNS.LOG_WARNING)
|
||||
data_buffer = b""
|
||||
in_frame = False
|
||||
command = KISS.CMD_UNKNOWN
|
||||
@@ -1058,6 +1145,11 @@ class RNodeInterface(Interface):
|
||||
RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.id_callsign.decode("utf-8")), RNS.LOG_DEBUG)
|
||||
self.process_outgoing(self.id_callsign)
|
||||
|
||||
if self.use_tcp:
|
||||
if self.tcp and self.tcp.connected:
|
||||
if time.time() > self.tcp.last_write + self.tcp.ACTIVITY_KEEPALIVE:
|
||||
self.detect()
|
||||
|
||||
sleep(0.08)
|
||||
|
||||
except Exception as e:
|
||||
@@ -1086,23 +1178,28 @@ class RNodeInterface(Interface):
|
||||
time.sleep(5)
|
||||
RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
|
||||
self.open_port()
|
||||
if self.serial.is_open:
|
||||
self.configure_device()
|
||||
if self.serial.is_open: self.configure_device()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
self.reconnecting = False
|
||||
if self.online:
|
||||
RNS.log("Reconnected serial port for "+str(self))
|
||||
if self.online: RNS.log(f"Reconnected port for {self}")
|
||||
|
||||
def detach(self):
|
||||
self.detached = True
|
||||
self.disable_external_framebuffer()
|
||||
self.setRadioState(KISS.RADIO_STATE_OFF)
|
||||
self.leave()
|
||||
try:
|
||||
self.disable_external_framebuffer()
|
||||
self.setRadioState(KISS.RADIO_STATE_OFF)
|
||||
self.leave()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while detaching {self}: {e}", RNS.LOG_ERROR)
|
||||
|
||||
if self.use_ble:
|
||||
self.ble.close()
|
||||
if self.use_ble: self.ble.close()
|
||||
if self.use_tcp:
|
||||
time.sleep(0.5)
|
||||
self.tcp.close()
|
||||
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
@@ -1123,6 +1220,17 @@ class RNodeInterface(Interface):
|
||||
def get_battery_percent(self):
|
||||
return self.r_battery_percent
|
||||
|
||||
def tcp_receive(self, data):
|
||||
with self.tcp_rx_lock: self.tcp_rx_queue += data
|
||||
|
||||
def tcp_waiting(self): return len(self.tcp_tx_queue) > 0
|
||||
|
||||
def get_tcp_waiting(self, n):
|
||||
with self.tcp_tx_lock:
|
||||
data = self.tcp_tx_queue[:n]
|
||||
self.tcp_tx_queue = self.tcp_tx_queue[n:]
|
||||
return data
|
||||
|
||||
def ble_receive(self, data):
|
||||
with self.ble_rx_lock:
|
||||
self.ble_rx_queue += data
|
||||
@@ -1137,7 +1245,7 @@ class RNodeInterface(Interface):
|
||||
return data
|
||||
|
||||
def __str__(self):
|
||||
return "RNodeInterface["+str(self.name)+"]"
|
||||
return f"RNodeInterface[{self.name}]"
|
||||
|
||||
class BLEConnection():
|
||||
UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
@@ -1190,7 +1298,7 @@ class BLEConnection():
|
||||
self.connect_job_running = False
|
||||
self.device_disappeared = False
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
if BLEConnection.bleak == None:
|
||||
if importlib.util.find_spec("bleak") != None:
|
||||
import bleak
|
||||
@@ -1316,3 +1424,136 @@ class BLEConnection():
|
||||
RNS.log(f"Error while determining device bond status for {device}, the contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
return False
|
||||
|
||||
class TCPConnection():
|
||||
TARGET_PORT = 7633
|
||||
CONNECT_TIMEOUT = 5.0
|
||||
INITIAL_CONNECT_TIMEOUT = 5.0
|
||||
RECONNECT_WAIT = 4.0
|
||||
ACTIVITY_TIMEOUT = 6.0
|
||||
ACTIVITY_KEEPALIVE = ACTIVITY_TIMEOUT-2.5
|
||||
|
||||
TCP_USER_TIMEOUT = 24
|
||||
TCP_PROBE_AFTER = 5
|
||||
TCP_PROBE_INTERVAL = 2
|
||||
TCP_PROBES = 12
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
return self.connected
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
buflen = len(self.owner.tcp_rx_queue)
|
||||
return buflen > 0
|
||||
|
||||
def write(self, data_bytes):
|
||||
if self.connected and self.socket:
|
||||
with self.owner.tcp_tx_lock:
|
||||
if len(self.owner.tcp_tx_queue) > 0:
|
||||
self.socket.sendall(self.owner.tcp_tx_queue)
|
||||
self.owner.tcp_tx_queue = b""
|
||||
|
||||
self.socket.sendall(data_bytes)
|
||||
self.last_write = time.time()
|
||||
|
||||
else:
|
||||
with self.owner.tcp_tx_lock: self.owner.tcp_tx_queue += data_bytes
|
||||
|
||||
return len(data_bytes)
|
||||
|
||||
def read(self, n):
|
||||
with self.owner.tcp_rx_lock:
|
||||
data = self.owner.tcp_rx_queue[:n]
|
||||
self.owner.tcp_rx_queue = self.owner.tcp_rx_queue[n:]
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
if self.connected:
|
||||
RNS.log(f"Disconnecting TCP socket for {self.owner}", RNS.LOG_DEBUG)
|
||||
self.must_disconnect = True
|
||||
if self.socket: self.socket.close()
|
||||
|
||||
def __init__(self, owner=None, target_host=None):
|
||||
self.owner = owner
|
||||
self.target_host = target_host
|
||||
self.connected = False
|
||||
self.reconnecting = False
|
||||
self.running = False
|
||||
self.should_run = False
|
||||
self.must_disconnect = False
|
||||
self.connect_job_running = False
|
||||
self.last_write = time.time()
|
||||
|
||||
self.should_run = True
|
||||
self.connection_thread = threading.Thread(target=self.initial_connect, daemon=True).start()
|
||||
|
||||
def set_timeouts_linux(self):
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(self.TCP_USER_TIMEOUT * 1000))
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(self.TCP_PROBE_INTERVAL))
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(self.TCP_PROBES))
|
||||
|
||||
def set_timeouts_osx(self):
|
||||
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(self.TCP_PROBE_AFTER))
|
||||
|
||||
def cleanup(self):
|
||||
try:
|
||||
if self.socket: self.socket.close()
|
||||
except Exception as e: RNS.log(f"Error while disconnecting TCP socket on cleanup for {self.owner}", RNS.LOG_ERROR)
|
||||
self.should_run = False
|
||||
|
||||
def initial_connect(self):
|
||||
if self.connect(initial=True): threading.Thread(target=self.read_loop, daemon=True).start()
|
||||
|
||||
def connect(self, initial=False):
|
||||
try:
|
||||
if initial:
|
||||
RNS.log(f"Establishing TCP connection to device for {self.owner}...", RNS.LOG_DEBUG)
|
||||
|
||||
address_info = socket.getaddrinfo(self.target_host, self.TARGET_PORT, proto=socket.IPPROTO_TCP)[0]
|
||||
address_family = address_info[0]
|
||||
target_address = address_info[4]
|
||||
|
||||
self.socket = socket.socket(address_family, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(self.INITIAL_CONNECT_TIMEOUT)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.socket.connect(target_address)
|
||||
self.socket.settimeout(None)
|
||||
self.connected = True
|
||||
self.last_write = time.time()
|
||||
|
||||
RNS.log(f"TCP connection to device for {self.owner} established", RNS.LOG_DEBUG)
|
||||
|
||||
if RNS.vendor.platformutils.is_linux(): self.set_timeouts_linux()
|
||||
elif RNS.vendor.platformutils.is_darwin(): self.set_timeouts_osx()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
if initial:
|
||||
RNS.log(f"TCP connection to device for {self.owner} could not be established: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
else: raise e
|
||||
|
||||
def read_loop(self):
|
||||
try:
|
||||
data_in = b""
|
||||
while not self.must_disconnect:
|
||||
if self.socket: data_in = self.socket.recv(4096)
|
||||
else: data_in = b""
|
||||
|
||||
if len(data_in) > 0: self.owner.tcp_receive(data_in)
|
||||
else:
|
||||
self.connected = False
|
||||
RNS.log(f"The TCP socket for {self} was closed", RNS.LOG_WARNING)
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
self.connected = False
|
||||
RNS.log(f"A TCP read error occurred for {self}, the contained exception was: {e}", RNS.LOG_WARNING)
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2024 Jacob Eva. Adapted from the RNodeInterface by Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -33,6 +41,8 @@ class KISS():
|
||||
FESC = 0xDB
|
||||
TFEND = 0xDC
|
||||
TFESC = 0xDD
|
||||
|
||||
CMD_DATA = 0x00
|
||||
|
||||
CMD_UNKNOWN = 0xFE
|
||||
CMD_FREQUENCY = 0x01
|
||||
@@ -64,7 +74,7 @@ class KISS():
|
||||
CMD_FW_VERSION = 0x50
|
||||
CMD_ROM_READ = 0x51
|
||||
CMD_RESET = 0x55
|
||||
CMD_INTERFACES = 0x64
|
||||
CMD_INTERFACES = 0x71
|
||||
|
||||
CMD_INT0_DATA = 0x00
|
||||
CMD_INT1_DATA = 0x10
|
||||
@@ -79,19 +89,6 @@ class KISS():
|
||||
CMD_INT10_DATA = 0xE0
|
||||
CMD_INT11_DATA = 0xF0
|
||||
|
||||
CMD_SEL_INT0 = 0x1E
|
||||
CMD_SEL_INT1 = 0x1F
|
||||
CMD_SEL_INT2 = 0x2F
|
||||
CMD_SEL_INT3 = 0x74
|
||||
CMD_SEL_INT4 = 0x7F
|
||||
CMD_SEL_INT5 = 0x9F
|
||||
CMD_SEL_INT6 = 0xAF
|
||||
CMD_SEL_INT7 = 0xBF
|
||||
CMD_SEL_INT8 = 0xCF
|
||||
CMD_SEL_INT9 = 0xDF
|
||||
CMD_SEL_INT10 = 0xEF
|
||||
CMD_SEL_INT11 = 0xFF
|
||||
|
||||
DETECT_REQ = 0x73
|
||||
DETECT_RESP = 0x46
|
||||
|
||||
@@ -116,33 +113,7 @@ class KISS():
|
||||
SX128X = 0x20
|
||||
SX1280 = 0x21
|
||||
|
||||
def int_data_cmd_to_index(int_data_cmd):
|
||||
if int_data_cmd == KISS.CMD_INT0_DATA:
|
||||
return 0
|
||||
elif int_data_cmd == KISS.CMD_INT1_DATA:
|
||||
return 1
|
||||
elif int_data_cmd == KISS.CMD_INT2_DATA:
|
||||
return 2
|
||||
elif int_data_cmd == KISS.CMD_INT3_DATA:
|
||||
return 3
|
||||
elif int_data_cmd == KISS.CMD_INT4_DATA:
|
||||
return 4
|
||||
elif int_data_cmd == KISS.CMD_INT5_DATA:
|
||||
return 5
|
||||
elif int_data_cmd == KISS.CMD_INT6_DATA:
|
||||
return 6
|
||||
elif int_data_cmd == KISS.CMD_INT7_DATA:
|
||||
return 7
|
||||
elif int_data_cmd == KISS.CMD_INT8_DATA:
|
||||
return 8
|
||||
elif int_data_cmd == KISS.CMD_INT9_DATA:
|
||||
return 9
|
||||
elif int_data_cmd == KISS.CMD_INT10_DATA:
|
||||
return 10
|
||||
elif int_data_cmd == KISS.CMD_INT11_DATA:
|
||||
return 11
|
||||
else:
|
||||
return 0
|
||||
CMD_SEL_INT = 0x1F
|
||||
|
||||
def interface_type_to_str(interface_type):
|
||||
if interface_type == KISS.SX126X or interface_type == KISS.SX1262:
|
||||
@@ -178,7 +149,7 @@ class RNodeMultiInterface(Interface):
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
raise SystemError("Invalid interface type. The Android-specific RNode interface must be used on Android")
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
@@ -208,16 +179,17 @@ class RNodeMultiInterface(Interface):
|
||||
enabled_count += 1
|
||||
|
||||
# Create an array with a row for each subinterface
|
||||
subint_config = [[0 for x in range(11)] for y in range(enabled_count)]
|
||||
subint_config = [[None for x in range(11)] for y in range(enabled_count)]
|
||||
subint_index = 0
|
||||
|
||||
for subinterface in c:
|
||||
if isinstance(c[subinterface], dict):
|
||||
subinterface_config = c[subinterface]
|
||||
if (("interface_enabled" in subinterface_config) and subinterface_config.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True):
|
||||
subint_vport = subinterface_config["vport"] if "vport" in subinterface_config else None
|
||||
|
||||
subint_config[subint_index][0] = subinterface
|
||||
|
||||
subint_vport = subinterface_config["vport"] if "vport" in subinterface_config else None
|
||||
subint_config[subint_index][1] = subint_vport
|
||||
|
||||
frequency = int(subinterface_config["frequency"]) if "frequency" in subinterface_config else None
|
||||
@@ -241,6 +213,7 @@ class RNodeMultiInterface(Interface):
|
||||
subint_config[subint_index][10] = False
|
||||
else:
|
||||
subint_config[subint_index][10] = True
|
||||
|
||||
subint_index += 1
|
||||
|
||||
# if no subinterfaces are defined
|
||||
@@ -474,11 +447,10 @@ class RNodeMultiInterface(Interface):
|
||||
c4 = frequency & 0xFF
|
||||
data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4]))
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([interface.sel_cmd])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SEL_INT])+bytes([interface.index])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring frequency for "+str(self))
|
||||
self.selected_index = interface.index
|
||||
|
||||
def setBandwidth(self, bandwidth, interface):
|
||||
c1 = bandwidth >> 24
|
||||
@@ -487,35 +459,31 @@ class RNodeMultiInterface(Interface):
|
||||
c4 = bandwidth & 0xFF
|
||||
data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4]))
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([interface.sel_cmd])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SEL_INT])+bytes([interface.index])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring bandwidth for "+str(self))
|
||||
self.selected_index = interface.index
|
||||
|
||||
def setTXPower(self, txpower, interface):
|
||||
txp = txpower.to_bytes(1, byteorder="big", signed=True)
|
||||
kiss_command = bytes([KISS.FEND])+bytes([interface.sel_cmd])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SEL_INT])+bytes([interface.index])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring TX power for "+str(self))
|
||||
self.selected_index = interface.index
|
||||
|
||||
def setSpreadingFactor(self, sf, interface):
|
||||
sf = bytes([sf])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([interface.sel_cmd])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SEL_INT])+bytes([interface.index])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring spreading factor for "+str(self))
|
||||
self.selected_index = interface.index
|
||||
|
||||
def setCodingRate(self, cr, interface):
|
||||
cr = bytes([cr])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([interface.sel_cmd])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SEL_INT])+bytes([interface.index])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring coding rate for "+str(self))
|
||||
self.selected_index = interface.index
|
||||
|
||||
def setSTALock(self, st_alock, interface):
|
||||
if st_alock != None:
|
||||
@@ -524,11 +492,10 @@ class RNodeMultiInterface(Interface):
|
||||
c2 = at & 0xFF
|
||||
data = KISS.escape(bytes([c1])+bytes([c2]))
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([interface.sel_cmd])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_ST_ALOCK])+data+bytes([KISS.FEND])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SEL_INT])+bytes([interface.index])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_ST_ALOCK])+data+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring short-term airtime limit for "+str(self))
|
||||
self.selected_index = interface.index
|
||||
|
||||
def setLTALock(self, lt_alock, interface):
|
||||
if lt_alock != None:
|
||||
@@ -537,19 +504,17 @@ class RNodeMultiInterface(Interface):
|
||||
c2 = at & 0xFF
|
||||
data = KISS.escape(bytes([c1])+bytes([c2]))
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([interface.sel_cmd])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_LT_ALOCK])+data+bytes([KISS.FEND])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SEL_INT])+bytes([interface.index])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_LT_ALOCK])+data+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring long-term airtime limit for "+str(self))
|
||||
self.selected_index = interface.index
|
||||
|
||||
def setRadioState(self, state, interface):
|
||||
#self.state = state
|
||||
kiss_command = bytes([KISS.FEND])+bytes([interface.sel_cmd])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SEL_INT])+bytes([interface.index])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring radio state for "+str(self))
|
||||
self.selected_index = interface.index
|
||||
|
||||
def validate_firmware(self):
|
||||
if (self.maj_version >= RNodeMultiInterface.REQUIRED_FW_VER_MAJ):
|
||||
@@ -570,7 +535,7 @@ class RNodeMultiInterface(Interface):
|
||||
pass
|
||||
else:
|
||||
data = KISS.escape(data)
|
||||
frame = bytes([0xc0])+bytes([interface.data_cmd])+data+bytes([0xc0])
|
||||
frame = bytes([KISS.FEND])+bytes([KISS.CMD_SEL_INT])+bytes([interface.index])+bytes([KISS.FEND])+bytes([KISS.FEND])+bytes([KISS.CMD_DATA])+data+bytes([KISS.FEND])
|
||||
|
||||
written = self.serial.write(frame)
|
||||
self.txb += len(data)
|
||||
@@ -599,21 +564,9 @@ class RNodeMultiInterface(Interface):
|
||||
last_read_ms = int(time.time()*1000)
|
||||
|
||||
if (in_frame and byte == KISS.FEND and
|
||||
(command == KISS.CMD_INT0_DATA or
|
||||
command == KISS.CMD_INT1_DATA or
|
||||
command == KISS.CMD_INT2_DATA or
|
||||
command == KISS.CMD_INT3_DATA or
|
||||
command == KISS.CMD_INT4_DATA or
|
||||
command == KISS.CMD_INT5_DATA or
|
||||
command == KISS.CMD_INT6_DATA or
|
||||
command == KISS.CMD_INT7_DATA or
|
||||
command == KISS.CMD_INT8_DATA or
|
||||
command == KISS.CMD_INT9_DATA or
|
||||
command == KISS.CMD_INT10_DATA or
|
||||
command == KISS.CMD_INT11_DATA)):
|
||||
(command == KISS.CMD_DATA)):
|
||||
in_frame = False
|
||||
self.subinterfaces[KISS.int_data_cmd_to_index(command)].process_incoming(data_buffer)
|
||||
self.selected_index = KISS.int_data_cmd_to_index(command)
|
||||
self.subinterfaces[self.selected_index].process_incoming(data_buffer)
|
||||
data_buffer = b""
|
||||
command_buffer = b""
|
||||
elif (byte == KISS.FEND):
|
||||
@@ -678,6 +631,9 @@ class RNodeMultiInterface(Interface):
|
||||
RNS.log(str(self.subinterfaces[self.selected_index])+" Radio reporting bandwidth is "+str(self.subinterfaces[self.selected_index].r_bandwidth/1000.0)+" KHz", RNS.LOG_DEBUG)
|
||||
self.subinterfaces[self.selected_index].updateBitrate()
|
||||
|
||||
elif (command == KISS.CMD_SEL_INT):
|
||||
self.selected_index = byte
|
||||
|
||||
elif (command == KISS.CMD_TXPOWER):
|
||||
txp = byte - 256 if byte > 127 else byte
|
||||
self.subinterfaces[self.selected_index].r_txpower = txp
|
||||
@@ -979,7 +935,7 @@ class RNodeSubInterface(Interface):
|
||||
if RNS.vendor.platformutils.is_android():
|
||||
raise SystemError("Invalid interface type. The Android-specific RNode interface must be used on Android")
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
@@ -989,51 +945,9 @@ class RNodeSubInterface(Interface):
|
||||
|
||||
super().__init__()
|
||||
|
||||
if index == 0:
|
||||
sel_cmd = KISS.CMD_SEL_INT0
|
||||
data_cmd= KISS.CMD_INT0_DATA
|
||||
elif index == 1:
|
||||
sel_cmd = KISS.CMD_SEL_INT1
|
||||
data_cmd= KISS.CMD_INT1_DATA
|
||||
elif index == 2:
|
||||
sel_cmd = KISS.CMD_SEL_INT2
|
||||
data_cmd= KISS.CMD_INT2_DATA
|
||||
elif index == 3:
|
||||
sel_cmd = KISS.CMD_SEL_INT3
|
||||
data_cmd= KISS.CMD_INT3_DATA
|
||||
elif index == 4:
|
||||
sel_cmd = KISS.CMD_SEL_INT4
|
||||
data_cmd= KISS.CMD_INT4_DATA
|
||||
elif index == 5:
|
||||
sel_cmd = KISS.CMD_SEL_INT5
|
||||
data_cmd= KISS.CMD_INT5_DATA
|
||||
elif index == 6:
|
||||
sel_cmd = KISS.CMD_SEL_INT6
|
||||
data_cmd= KISS.CMD_INT6_DATA
|
||||
elif index == 7:
|
||||
sel_cmd = KISS.CMD_SEL_INT7
|
||||
data_cmd= KISS.CMD_INT7_DATA
|
||||
elif index == 8:
|
||||
sel_cmd = KISS.CMD_SEL_INT8
|
||||
data_cmd= KISS.CMD_INT8_DATA
|
||||
elif index == 9:
|
||||
sel_cmd = KISS.CMD_SEL_INT9
|
||||
data_cmd= KISS.CMD_INT9_DATA
|
||||
elif index == 10:
|
||||
sel_cmd = KISS.CMD_SEL_INT10
|
||||
data_cmd= KISS.CMD_INT10_DATA
|
||||
elif index == 11:
|
||||
sel_cmd = KISS.CMD_SEL_INT11
|
||||
data_cmd= KISS.CMD_INT11_DATA
|
||||
else:
|
||||
sel_cmd = KISS.CMD_SEL_INT0
|
||||
data_cmd= KISS.CMD_INT0_DATA
|
||||
|
||||
self.owner = owner
|
||||
self.name = name
|
||||
self.index = index
|
||||
self.sel_cmd = sel_cmd
|
||||
self.data_cmd = data_cmd
|
||||
self.interface_type= interface_type
|
||||
self.flow_control= flow_control
|
||||
self.online = False
|
||||
@@ -1079,7 +993,6 @@ class RNodeSubInterface(Interface):
|
||||
self.announce_rate_target = None
|
||||
|
||||
self.mode = None
|
||||
self.announce_cap = None
|
||||
self.bitrate = None
|
||||
self.ifac_size = None
|
||||
|
||||
@@ -1099,7 +1012,7 @@ class RNodeSubInterface(Interface):
|
||||
RNS.log("Invalid interface type configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
if (self.txpower < -9 or self.txpower > 27):
|
||||
if (self.txpower < -9 or self.txpower > 37):
|
||||
RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
|
||||
self.validcfg = False
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -53,7 +61,7 @@ class SerialInterface(Interface):
|
||||
serial = None
|
||||
|
||||
def __init__(self, owner, configuration):
|
||||
import importlib
|
||||
import importlib.util
|
||||
if importlib.util.find_spec('serial') != None:
|
||||
import serial
|
||||
else:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -99,8 +107,13 @@ class TCPClientInterface(Interface):
|
||||
i2p_tunneled = c.as_bool("i2p_tunneled") if "i2p_tunneled" in c else False
|
||||
connect_timeout = c.as_int("connect_timeout") if "connect_timeout" in c else None
|
||||
max_reconnect_tries = c.as_int("max_reconnect_tries") if "max_reconnect_tries" in c else None
|
||||
fixed_mtu = c.as_int("fixed_mtu") if "fixed_mtu" in c else None
|
||||
if fixed_mtu:
|
||||
if fixed_mtu < RNS.Reticulum.MTU: raise ValueError(f"Configured MTU of {fixed_mtu} bytes is too small")
|
||||
self.AUTOCONFIGURE_MTU = False
|
||||
self.FIXED_MTU = True
|
||||
|
||||
self.HW_MTU = TCPInterface.HW_MTU
|
||||
self.HW_MTU = TCPInterface.HW_MTU if not fixed_mtu else fixed_mtu
|
||||
self.IN = True
|
||||
self.OUT = False
|
||||
self.socket = None
|
||||
@@ -118,10 +131,9 @@ class TCPClientInterface(Interface):
|
||||
self.mode = RNS.Interfaces.Interface.Interface.MODE_FULL
|
||||
self.bitrate = TCPClientInterface.BITRATE_GUESS
|
||||
|
||||
if max_reconnect_tries == None:
|
||||
self.max_reconnect_tries = TCPClientInterface.RECONNECT_MAX_TRIES
|
||||
else:
|
||||
self.max_reconnect_tries = max_reconnect_tries
|
||||
self.supports_discovery = True
|
||||
if max_reconnect_tries == None: self.max_reconnect_tries = TCPClientInterface.RECONNECT_MAX_TRIES
|
||||
else: self.max_reconnect_tries = max_reconnect_tries
|
||||
|
||||
if connected_socket != None:
|
||||
self.receives = True
|
||||
@@ -331,7 +343,8 @@ class TCPClientInterface(Interface):
|
||||
data_buffer = b""
|
||||
|
||||
while True:
|
||||
data_in = self.socket.recv(4096)
|
||||
if self.socket: data_in = self.socket.recv(4096)
|
||||
else: data_in = b""
|
||||
if len(data_in) > 0:
|
||||
if self.kiss_framing:
|
||||
# Read loop for KISS framing
|
||||
@@ -444,7 +457,7 @@ class TCPServerInterface(Interface):
|
||||
|
||||
@staticmethod
|
||||
def get_address_for_if(name, bind_port, prefer_ipv6=False):
|
||||
import RNS.vendor.ifaddr.niwrapper as netinfo
|
||||
from RNS.Interfaces import netinfo
|
||||
ifaddr = netinfo.ifaddresses(name)
|
||||
if len(ifaddr) < 1:
|
||||
raise SystemError(f"No addresses available on specified kernel interface \"{name}\" for TCPServerInterface to bind to")
|
||||
@@ -453,9 +466,9 @@ class TCPServerInterface(Interface):
|
||||
bind_ip = ifaddr[netinfo.AF_INET6][0]["addr"]
|
||||
if bind_ip.lower().startswith("fe80::"):
|
||||
# We'll need to add the interface as scope for link-local addresses
|
||||
return TCPServerInterface.get_address_for_host(f"{bind_ip}%{name}", bind_port)
|
||||
return TCPServerInterface.get_address_for_host(f"{bind_ip}%{name}", bind_port, prefer_ipv6)
|
||||
else:
|
||||
return TCPServerInterface.get_address_for_host(bind_ip, bind_port)
|
||||
return TCPServerInterface.get_address_for_host(bind_ip, bind_port, prefer_ipv6)
|
||||
elif netinfo.AF_INET in ifaddr:
|
||||
bind_ip = ifaddr[netinfo.AF_INET][0]["addr"]
|
||||
return (bind_ip, bind_port)
|
||||
@@ -463,8 +476,15 @@ class TCPServerInterface(Interface):
|
||||
raise SystemError(f"No addresses available on specified kernel interface \"{name}\" for TCPServerInterface to bind to")
|
||||
|
||||
@staticmethod
|
||||
def get_address_for_host(name, bind_port):
|
||||
address_info = socket.getaddrinfo(name, bind_port, proto=socket.IPPROTO_TCP)[0]
|
||||
def get_address_for_host(name, bind_port, prefer_ipv6=False):
|
||||
address_infos = socket.getaddrinfo(name, bind_port, proto=socket.IPPROTO_TCP)
|
||||
address_info = address_infos[0]
|
||||
for entry in address_infos:
|
||||
if prefer_ipv6 and entry[0] == socket.AF_INET6:
|
||||
address_info = entry; break
|
||||
elif not prefer_ipv6 and entry[0] == socket.AF_INET:
|
||||
address_info = entry; break
|
||||
|
||||
if address_info[0] == socket.AF_INET6:
|
||||
return (name, bind_port, address_info[4][2], address_info[4][3])
|
||||
elif address_info[0] == socket.AF_INET:
|
||||
@@ -492,6 +512,7 @@ class TCPServerInterface(Interface):
|
||||
if port != None:
|
||||
bindport = port
|
||||
|
||||
self.supports_discovery = True
|
||||
self.HW_MTU = TCPInterface.HW_MTU
|
||||
|
||||
self.online = False
|
||||
@@ -516,7 +537,7 @@ class TCPServerInterface(Interface):
|
||||
else:
|
||||
if bindip == None:
|
||||
raise SystemError(f"No TCP bind IP configured for interface \"{name}\"")
|
||||
bind_address = TCPServerInterface.get_address_for_host(bindip, self.bind_port)
|
||||
bind_address = TCPServerInterface.get_address_for_host(bindip, self.bind_port, prefer_ipv6)
|
||||
|
||||
if bind_address != None:
|
||||
self.receives = True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -35,13 +43,13 @@ class UDPInterface(Interface):
|
||||
|
||||
@staticmethod
|
||||
def get_address_for_if(name):
|
||||
import RNS.vendor.ifaddr.niwrapper as netinfo
|
||||
from RNS.Interfaces import netinfo
|
||||
ifaddr = netinfo.ifaddresses(name)
|
||||
return ifaddr[netinfo.AF_INET][0]["addr"]
|
||||
|
||||
@staticmethod
|
||||
def get_broadcast_for_if(name):
|
||||
import RNS.vendor.ifaddr.niwrapper as netinfo
|
||||
from RNS.Interfaces import netinfo
|
||||
ifaddr = netinfo.ifaddresses(name)
|
||||
return ifaddr[netinfo.AF_INET][0]["broadcast"]
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -23,6 +31,8 @@
|
||||
import os
|
||||
import glob
|
||||
import RNS.Interfaces.Android
|
||||
import RNS.Interfaces.util
|
||||
import RNS.Interfaces.util.netinfo as netinfo
|
||||
|
||||
py_modules = glob.glob(os.path.dirname(__file__)+"/*.py")
|
||||
pyc_modules = glob.glob(os.path.dirname(__file__)+"/*.pyc")
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
import glob
|
||||
|
||||
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"))]))
|
||||
@@ -0,0 +1,325 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2014 Stefan C. Mueller
|
||||
# Copyright (c) 2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import os
|
||||
import socket
|
||||
import ipaddress
|
||||
import platform
|
||||
import ctypes.util
|
||||
import collections
|
||||
from typing import List, Iterable, Optional, Tuple, Union
|
||||
|
||||
AF_INET6 = socket.AF_INET6.value
|
||||
AF_INET = socket.AF_INET.value
|
||||
|
||||
def interfaces() -> List[str]:
|
||||
adapters = get_adapters(include_unconfigured=True)
|
||||
return [a.name for a in adapters]
|
||||
|
||||
def interface_names_to_indexes() -> dict:
|
||||
adapters = get_adapters(include_unconfigured=True)
|
||||
results = {}
|
||||
for adapter in adapters:
|
||||
results[adapter.name] = adapter.index
|
||||
return results
|
||||
|
||||
def interface_name_to_nice_name(ifname) -> str:
|
||||
try:
|
||||
adapters = get_adapters(include_unconfigured=True)
|
||||
for adapter in adapters:
|
||||
if adapter.name == ifname:
|
||||
if hasattr(adapter, "nice_name"):
|
||||
return adapter.nice_name
|
||||
|
||||
except: return None
|
||||
return None
|
||||
|
||||
def ifaddresses(ifname) -> dict:
|
||||
adapters = get_adapters(include_unconfigured=True)
|
||||
ifa = {}
|
||||
for a in adapters:
|
||||
if a.name == ifname:
|
||||
ipv4s = []
|
||||
ipv6s = []
|
||||
for ip in a.ips:
|
||||
t = {}
|
||||
if ip.is_IPv4:
|
||||
net = ipaddress.ip_network(str(ip.ip)+"/"+str(ip.network_prefix), strict=False)
|
||||
t["addr"] = ip.ip
|
||||
t["prefix"] = ip.network_prefix
|
||||
t["broadcast"] = str(net.broadcast_address)
|
||||
ipv4s.append(t)
|
||||
if ip.is_IPv6:
|
||||
t["addr"] = ip.ip[0]
|
||||
ipv6s.append(t)
|
||||
|
||||
if len(ipv4s) > 0: ifa[AF_INET] = ipv4s
|
||||
if len(ipv6s) > 0: ifa[AF_INET6] = ipv6s
|
||||
|
||||
return ifa
|
||||
|
||||
def get_adapters(include_unconfigured=False):
|
||||
if os.name == "posix": return _get_adapters_posix(include_unconfigured=include_unconfigured)
|
||||
elif os.name == "nt": return _get_adapters_win(include_unconfigured=include_unconfigured)
|
||||
else: raise RuntimeError(f"Unsupported Operating System: {os.name}")
|
||||
|
||||
class Adapter(object):
|
||||
def __init__(self, name: str, nice_name: str, ips: List["IP"], index: Optional[int] = None) -> None:
|
||||
self.name = name
|
||||
self.nice_name = nice_name
|
||||
self.ips = ips
|
||||
self.index = index
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Adapter(name={name}, nice_name={nice_name}, ips={ips}, index={index})".format(
|
||||
name=repr(self.name), nice_name=repr(self.nice_name), ips=repr(self.ips), index=repr(self.index))
|
||||
|
||||
_IPv4Address = str
|
||||
_IPv6Address = Tuple[str, int, int]
|
||||
class IP(object):
|
||||
def __init__(self, ip: Union[_IPv4Address, _IPv6Address], network_prefix: int, nice_name: str) -> None:
|
||||
self.ip = ip
|
||||
self.network_prefix = network_prefix
|
||||
self.nice_name = nice_name
|
||||
|
||||
@property
|
||||
def is_IPv4(self) -> bool: return not isinstance(self.ip, tuple)
|
||||
|
||||
@property
|
||||
def is_IPv6(self) -> bool: return isinstance(self.ip, tuple)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "IP(ip={ip}, network_prefix={network_prefix}, nice_name={nice_name})".format(ip=repr(self.ip), network_prefix=repr(self.network_prefix), nice_name=repr(self.nice_name))
|
||||
|
||||
if platform.system() == "Darwin" or "BSD" in platform.system():
|
||||
class sockaddr(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("sa_len", ctypes.c_uint8),
|
||||
("sa_familiy", ctypes.c_uint8),
|
||||
("sa_data", ctypes.c_uint8 * 14)]
|
||||
|
||||
class sockaddr_in(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("sa_len", ctypes.c_uint8),
|
||||
("sa_familiy", ctypes.c_uint8),
|
||||
("sin_port", ctypes.c_uint16),
|
||||
("sin_addr", ctypes.c_uint8 * 4),
|
||||
("sin_zero", ctypes.c_uint8 * 8)]
|
||||
|
||||
class sockaddr_in6(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("sa_len", ctypes.c_uint8),
|
||||
("sa_familiy", ctypes.c_uint8),
|
||||
("sin6_port", ctypes.c_uint16),
|
||||
("sin6_flowinfo", ctypes.c_uint32),
|
||||
("sin6_addr", ctypes.c_uint8 * 16),
|
||||
("sin6_scope_id", ctypes.c_uint32)]
|
||||
|
||||
else:
|
||||
class sockaddr(ctypes.Structure): # type: ignore
|
||||
_fields_ = [("sa_familiy", ctypes.c_uint16), ("sa_data", ctypes.c_uint8 * 14)]
|
||||
|
||||
class sockaddr_in(ctypes.Structure): # type: ignore
|
||||
_fields_ = [
|
||||
("sin_familiy", ctypes.c_uint16),
|
||||
("sin_port", ctypes.c_uint16),
|
||||
("sin_addr", ctypes.c_uint8 * 4),
|
||||
("sin_zero", ctypes.c_uint8 * 8)]
|
||||
|
||||
class sockaddr_in6(ctypes.Structure): # type: ignore
|
||||
_fields_ = [
|
||||
("sin6_familiy", ctypes.c_uint16),
|
||||
("sin6_port", ctypes.c_uint16),
|
||||
("sin6_flowinfo", ctypes.c_uint32),
|
||||
("sin6_addr", ctypes.c_uint8 * 16),
|
||||
("sin6_scope_id", ctypes.c_uint32)]
|
||||
|
||||
def sockaddr_to_ip(sockaddr_ptr: "ctypes.pointer[sockaddr]") -> Optional[Union[_IPv4Address, _IPv6Address]]:
|
||||
if sockaddr_ptr:
|
||||
if sockaddr_ptr[0].sa_familiy == socket.AF_INET:
|
||||
ipv4 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in))
|
||||
ippacked = bytes(bytearray(ipv4[0].sin_addr))
|
||||
ip = str(ipaddress.ip_address(ippacked))
|
||||
return ip
|
||||
elif sockaddr_ptr[0].sa_familiy == socket.AF_INET6:
|
||||
ipv6 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in6))
|
||||
flowinfo = ipv6[0].sin6_flowinfo
|
||||
ippacked = bytes(bytearray(ipv6[0].sin6_addr))
|
||||
ip = str(ipaddress.ip_address(ippacked))
|
||||
scope_id = ipv6[0].sin6_scope_id
|
||||
return (ip, flowinfo, scope_id)
|
||||
return None
|
||||
|
||||
|
||||
def ipv6_prefixlength(address: ipaddress.IPv6Address) -> int:
|
||||
prefix_length = 0
|
||||
for i in range(address.max_prefixlen):
|
||||
if int(address) >> i & 1: prefix_length = prefix_length + 1
|
||||
return prefix_length
|
||||
|
||||
if os.name == "posix":
|
||||
class ifaddrs(ctypes.Structure): pass
|
||||
ifaddrs._fields_ = [
|
||||
("ifa_next", ctypes.POINTER(ifaddrs)),
|
||||
("ifa_name", ctypes.c_char_p),
|
||||
("ifa_flags", ctypes.c_uint),
|
||||
("ifa_addr", ctypes.POINTER(sockaddr)),
|
||||
("ifa_netmask", ctypes.POINTER(sockaddr)),]
|
||||
|
||||
libc = ctypes.CDLL(ctypes.util.find_library("socket" if os.uname()[0] == "SunOS" else "c"), use_errno=True) # type: ignore
|
||||
|
||||
def _get_adapters_posix(include_unconfigured: bool = False) -> Iterable[Adapter]:
|
||||
addr0 = addr = ctypes.POINTER(ifaddrs)()
|
||||
retval = libc.getifaddrs(ctypes.byref(addr))
|
||||
if retval != 0:
|
||||
eno = ctypes.get_errno()
|
||||
raise OSError(eno, os.strerror(eno))
|
||||
|
||||
ips = collections.OrderedDict()
|
||||
|
||||
def add_ip(adapter_name: str, ip: Optional[IP]) -> None:
|
||||
if adapter_name not in ips:
|
||||
index = None # type: Optional[int]
|
||||
try:
|
||||
index = socket.if_nametoindex(adapter_name) # type: ignore
|
||||
except (OSError, AttributeError): pass
|
||||
ips[adapter_name] = Adapter(adapter_name, adapter_name, [], index=index)
|
||||
if ip is not None:
|
||||
ips[adapter_name].ips.append(ip)
|
||||
|
||||
while addr:
|
||||
name = addr[0].ifa_name.decode(encoding="UTF-8")
|
||||
ip_addr = sockaddr_to_ip(addr[0].ifa_addr)
|
||||
if ip_addr:
|
||||
if addr[0].ifa_netmask and not addr[0].ifa_netmask[0].sa_familiy:
|
||||
addr[0].ifa_netmask[0].sa_familiy = addr[0].ifa_addr[0].sa_familiy
|
||||
netmask = sockaddr_to_ip(addr[0].ifa_netmask)
|
||||
if isinstance(netmask, tuple):
|
||||
netmaskStr = str(netmask[0])
|
||||
prefixlen = ipv6_prefixlength(ipaddress.IPv6Address(netmaskStr))
|
||||
else:
|
||||
assert netmask is not None, f"sockaddr_to_ip({addr[0].ifa_netmask}) returned None"
|
||||
netmaskStr = str("0.0.0.0/" + netmask)
|
||||
prefixlen = ipaddress.IPv4Network(netmaskStr).prefixlen
|
||||
ip = IP(ip_addr, prefixlen, name)
|
||||
add_ip(name, ip)
|
||||
else:
|
||||
if include_unconfigured:
|
||||
add_ip(name, None)
|
||||
addr = addr[0].ifa_next
|
||||
|
||||
libc.freeifaddrs(addr0)
|
||||
return ips.values()
|
||||
|
||||
elif os.name == "nt":
|
||||
from ctypes import wintypes
|
||||
NO_ERROR = 0
|
||||
ERROR_BUFFER_OVERFLOW = 111
|
||||
MAX_ADAPTER_NAME_LENGTH = 256
|
||||
MAX_ADAPTER_DESCRIPTION_LENGTH = 128
|
||||
MAX_ADAPTER_ADDRESS_LENGTH = 8
|
||||
AF_UNSPEC = 0
|
||||
|
||||
class SOCKET_ADDRESS(ctypes.Structure): _fields_ = [("lpSockaddr", ctypes.POINTER(sockaddr)), ("iSockaddrLength", wintypes.INT)]
|
||||
class IP_ADAPTER_UNICAST_ADDRESS(ctypes.Structure): pass
|
||||
IP_ADAPTER_UNICAST_ADDRESS._fields_ = [
|
||||
("Length", wintypes.ULONG),
|
||||
("Flags", wintypes.DWORD),
|
||||
("Next", ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)),
|
||||
("Address", SOCKET_ADDRESS),
|
||||
("PrefixOrigin", ctypes.c_uint),
|
||||
("SuffixOrigin", ctypes.c_uint),
|
||||
("DadState", ctypes.c_uint),
|
||||
("ValidLifetime", wintypes.ULONG),
|
||||
("PreferredLifetime", wintypes.ULONG),
|
||||
("LeaseLifetime", wintypes.ULONG),
|
||||
("OnLinkPrefixLength", ctypes.c_uint8)]
|
||||
|
||||
class IP_ADAPTER_ADDRESSES(ctypes.Structure): pass
|
||||
IP_ADAPTER_ADDRESSES._fields_ = [
|
||||
("Length", wintypes.ULONG),
|
||||
("IfIndex", wintypes.DWORD),
|
||||
("Next", ctypes.POINTER(IP_ADAPTER_ADDRESSES)),
|
||||
("AdapterName", ctypes.c_char_p),
|
||||
("FirstUnicastAddress", ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)),
|
||||
("FirstAnycastAddress", ctypes.c_void_p),
|
||||
("FirstMulticastAddress", ctypes.c_void_p),
|
||||
("FirstDnsServerAddress", ctypes.c_void_p),
|
||||
("DnsSuffix", ctypes.c_wchar_p),
|
||||
("Description", ctypes.c_wchar_p),
|
||||
("FriendlyName", ctypes.c_wchar_p)]
|
||||
|
||||
iphlpapi = ctypes.windll.LoadLibrary("Iphlpapi") # type: ignore
|
||||
|
||||
def _enumerate_interfaces_of_adapter_win(nice_name: str, address: IP_ADAPTER_UNICAST_ADDRESS) -> Iterable[IP]:
|
||||
# Iterate through linked list and fill list
|
||||
addresses = [] # type: List[IP_ADAPTER_UNICAST_ADDRESS]
|
||||
while True:
|
||||
addresses.append(address)
|
||||
if not address.Next: break
|
||||
address = address.Next[0]
|
||||
|
||||
for address in addresses:
|
||||
ip = sockaddr_to_ip(address.Address.lpSockaddr)
|
||||
assert ip is not None, f"sockaddr_to_ip({address.Address.lpSockaddr}) returned None"
|
||||
network_prefix = address.OnLinkPrefixLength
|
||||
yield IP(ip, network_prefix, nice_name)
|
||||
|
||||
def _get_adapters_win(include_unconfigured: bool = False) -> Iterable[Adapter]:
|
||||
addressbuffersize = wintypes.ULONG(15 * 1024)
|
||||
retval = ERROR_BUFFER_OVERFLOW
|
||||
while retval == ERROR_BUFFER_OVERFLOW:
|
||||
addressbuffer = ctypes.create_string_buffer(addressbuffersize.value)
|
||||
retval = iphlpapi.GetAdaptersAddresses(
|
||||
wintypes.ULONG(AF_UNSPEC),
|
||||
wintypes.ULONG(0),
|
||||
None,
|
||||
ctypes.byref(addressbuffer),
|
||||
ctypes.byref(addressbuffersize))
|
||||
|
||||
if retval != NO_ERROR:
|
||||
raise ctypes.WinError() # type: ignore
|
||||
|
||||
# Iterate through adapters and fill array
|
||||
address_infos = [] # type: List[IP_ADAPTER_ADDRESSES]
|
||||
address_info = IP_ADAPTER_ADDRESSES.from_buffer(addressbuffer)
|
||||
while True:
|
||||
address_infos.append(address_info)
|
||||
if not address_info.Next: break
|
||||
address_info = address_info.Next[0]
|
||||
|
||||
# Iterate through unicast addresses
|
||||
result = [] # type: List[Adapter]
|
||||
for adapter_info in address_infos:
|
||||
name = adapter_info.AdapterName.decode()
|
||||
nice_name = adapter_info.Description
|
||||
index = adapter_info.IfIndex
|
||||
|
||||
if adapter_info.FirstUnicastAddress:
|
||||
ips = _enumerate_interfaces_of_adapter_win(adapter_info.FriendlyName, adapter_info.FirstUnicastAddress[0])
|
||||
ips = list(ips)
|
||||
result.append(Adapter(name, nice_name, ips, index=index))
|
||||
|
||||
elif include_unconfigured: result.append(Adapter(name, nice_name, [], index=index))
|
||||
|
||||
return result
|
||||
+213
-79
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -32,7 +40,7 @@ import struct
|
||||
import math
|
||||
import time
|
||||
import RNS
|
||||
|
||||
import io
|
||||
|
||||
class LinkCallbacks:
|
||||
def __init__(self):
|
||||
@@ -72,19 +80,23 @@ class Link:
|
||||
LINK_MTU_SIZE = 3
|
||||
TRAFFIC_TIMEOUT_MIN_MS = 5
|
||||
TRAFFIC_TIMEOUT_FACTOR = 6
|
||||
KEEPALIVE_MAX_RTT = 1.75
|
||||
KEEPALIVE_TIMEOUT_FACTOR = 4
|
||||
"""
|
||||
RTT timeout factor used in link timeout calculation.
|
||||
"""
|
||||
STALE_GRACE = 2
|
||||
STALE_GRACE = 5
|
||||
"""
|
||||
Grace period in seconds used in link timeout calculation.
|
||||
"""
|
||||
KEEPALIVE = 360
|
||||
KEEPALIVE_MAX = 360
|
||||
KEEPALIVE_MIN = 5
|
||||
KEEPALIVE = KEEPALIVE_MAX
|
||||
"""
|
||||
Interval for sending keep-alive packets on established links in seconds.
|
||||
Default interval for sending keep-alive packets on established links in seconds.
|
||||
"""
|
||||
STALE_TIME = 2*KEEPALIVE
|
||||
STALE_FACTOR = 2
|
||||
STALE_TIME = STALE_FACTOR*KEEPALIVE
|
||||
"""
|
||||
If no traffic or keep-alive packets are received within this period, the
|
||||
link will be marked as stale, and a final keep-alive packet will be sent.
|
||||
@@ -93,39 +105,82 @@ class Link:
|
||||
and will be torn down.
|
||||
"""
|
||||
|
||||
PENDING = 0x00
|
||||
HANDSHAKE = 0x01
|
||||
ACTIVE = 0x02
|
||||
STALE = 0x03
|
||||
CLOSED = 0x04
|
||||
WATCHDOG_MAX_SLEEP = 5
|
||||
|
||||
TIMEOUT = 0x01
|
||||
INITIATOR_CLOSED = 0x02
|
||||
DESTINATION_CLOSED = 0x03
|
||||
PENDING = 0x00
|
||||
HANDSHAKE = 0x01
|
||||
ACTIVE = 0x02
|
||||
STALE = 0x03
|
||||
CLOSED = 0x04
|
||||
|
||||
ACCEPT_NONE = 0x00
|
||||
ACCEPT_APP = 0x01
|
||||
ACCEPT_ALL = 0x02
|
||||
TIMEOUT = 0x01
|
||||
INITIATOR_CLOSED = 0x02
|
||||
DESTINATION_CLOSED = 0x03
|
||||
|
||||
ACCEPT_NONE = 0x00
|
||||
ACCEPT_APP = 0x01
|
||||
ACCEPT_ALL = 0x02
|
||||
resource_strategies = [ACCEPT_NONE, ACCEPT_APP, ACCEPT_ALL]
|
||||
|
||||
MODE_AES128_CBC = 0x00
|
||||
MODE_AES256_CBC = 0x01
|
||||
MODE_AES256_GCM = 0x02
|
||||
MODE_OTP_RESERVED = 0x03
|
||||
MODE_PQ_RESERVED_1 = 0x04
|
||||
MODE_PQ_RESERVED_2 = 0x05
|
||||
MODE_PQ_RESERVED_3 = 0x06
|
||||
MODE_PQ_RESERVED_4 = 0x07
|
||||
ENABLED_MODES = [MODE_AES256_CBC]
|
||||
MODE_DEFAULT = MODE_AES256_CBC
|
||||
MODE_DESCRIPTIONS = {MODE_AES128_CBC: "AES_128_CBC",
|
||||
MODE_AES256_CBC: "AES_256_CBC",
|
||||
MODE_AES256_GCM: "MODE_AES256_GCM",
|
||||
MODE_OTP_RESERVED: "MODE_OTP_RESERVED",
|
||||
MODE_PQ_RESERVED_1: "MODE_PQ_RESERVED_1",
|
||||
MODE_PQ_RESERVED_2: "MODE_PQ_RESERVED_2",
|
||||
MODE_PQ_RESERVED_3: "MODE_PQ_RESERVED_3",
|
||||
MODE_PQ_RESERVED_4: "MODE_PQ_RESERVED_4"}
|
||||
|
||||
MTU_BYTEMASK = 0x1FFFFF
|
||||
MODE_BYTEMASK = 0xE0
|
||||
|
||||
@staticmethod
|
||||
def mtu_bytes(mtu):
|
||||
return struct.pack(">I", mtu & 0xFFFFFF)[1:]
|
||||
def signalling_bytes(mtu, mode):
|
||||
if not mode in Link.ENABLED_MODES: raise TypeError(f"Requested link mode {Link.MODE_DESCRIPTIONS[mode]} not enabled")
|
||||
signalling_value = (mtu & Link.MTU_BYTEMASK)+(((mode<<5) & Link.MODE_BYTEMASK)<<16)
|
||||
return struct.pack(">I", signalling_value)[1:]
|
||||
|
||||
@staticmethod
|
||||
def mtu_from_lr_packet(packet):
|
||||
if len(packet.data) == Link.ECPUBSIZE+Link.LINK_MTU_SIZE:
|
||||
return (packet.data[Link.ECPUBSIZE] << 16) + (packet.data[Link.ECPUBSIZE+1] << 8) + (packet.data[Link.ECPUBSIZE+2])
|
||||
else:
|
||||
return None
|
||||
return (packet.data[Link.ECPUBSIZE] << 16) + (packet.data[Link.ECPUBSIZE+1] << 8) + (packet.data[Link.ECPUBSIZE+2]) & Link.MTU_BYTEMASK
|
||||
else: return None
|
||||
|
||||
@staticmethod
|
||||
def mtu_from_lp_packet(packet):
|
||||
if len(packet.data) == RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2+Link.LINK_MTU_SIZE:
|
||||
mtu_bytes = packet.data[RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2:RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2+Link.LINK_MTU_SIZE]
|
||||
return (mtu_bytes[0] << 16) + (mtu_bytes[1] << 8) + (mtu_bytes[2])
|
||||
else:
|
||||
return None
|
||||
return (mtu_bytes[0] << 16) + (mtu_bytes[1] << 8) + (mtu_bytes[2]) & Link.MTU_BYTEMASK
|
||||
else: return None
|
||||
|
||||
@staticmethod
|
||||
def mode_byte(mode):
|
||||
if mode in Link.ENABLED_MODES: return (mode << 5) & Link.MODE_BYTEMASK
|
||||
else: raise TypeError(f"Requested link mode {mode} not enabled")
|
||||
|
||||
@staticmethod
|
||||
def mode_from_lr_packet(packet):
|
||||
if len(packet.data) > Link.ECPUBSIZE:
|
||||
mode = (packet.data[Link.ECPUBSIZE] & Link.MODE_BYTEMASK) >> 5
|
||||
return mode
|
||||
else: return Link.MODE_DEFAULT
|
||||
|
||||
@staticmethod
|
||||
def mode_from_lp_packet(packet):
|
||||
if len(packet.data) > RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2:
|
||||
mode = packet.data[RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2] >> 5
|
||||
return mode
|
||||
else: return Link.MODE_DEFAULT
|
||||
|
||||
@staticmethod
|
||||
def validate_request(owner, data, packet):
|
||||
@@ -142,6 +197,11 @@ class Link:
|
||||
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)
|
||||
|
||||
link.update_mdu()
|
||||
link.destination = packet.destination
|
||||
link.establishment_timeout = Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, packet.hops) + Link.KEEPALIVE
|
||||
@@ -170,13 +230,14 @@ class Link:
|
||||
return None
|
||||
|
||||
|
||||
def __init__(self, destination=None, established_callback = None, closed_callback = None, owner=None, peer_pub_bytes = None, peer_sig_pub_bytes = None):
|
||||
if destination != None and destination.type != RNS.Destination.SINGLE:
|
||||
raise TypeError("Links can only be established to the \"single\" destination type")
|
||||
def __init__(self, destination=None, established_callback=None, closed_callback=None, owner=None, peer_pub_bytes=None, peer_sig_pub_bytes=None, mode=MODE_DEFAULT):
|
||||
if destination != None and destination.type != RNS.Destination.SINGLE: raise TypeError("Links can only be established to the \"single\" destination type")
|
||||
self.mode = mode
|
||||
self.rtt = None
|
||||
self.mtu = RNS.Reticulum.MTU
|
||||
self.establishment_cost = 0
|
||||
self.establishment_rate = None
|
||||
self.expected_rate = None
|
||||
self.callbacks = LinkCallbacks()
|
||||
self.resource_strategy = Link.ACCEPT_NONE
|
||||
self.last_resource_window = None
|
||||
@@ -186,6 +247,7 @@ class Link:
|
||||
self.pending_requests = []
|
||||
self.last_inbound = 0
|
||||
self.last_outbound = 0
|
||||
self.last_keepalive = 0
|
||||
self.last_proof = 0
|
||||
self.last_data = 0
|
||||
self.tx = 0
|
||||
@@ -244,12 +306,14 @@ class Link:
|
||||
self.set_link_closed_callback(closed_callback)
|
||||
|
||||
if self.initiator:
|
||||
link_mtu = b""
|
||||
signalling_bytes = b""
|
||||
nh_hw_mtu = RNS.Transport.next_hop_interface_hw_mtu(destination.hash)
|
||||
if RNS.Reticulum.link_mtu_discovery() and nh_hw_mtu:
|
||||
link_mtu = Link.mtu_bytes(nh_hw_mtu)
|
||||
signalling_bytes = Link.signalling_bytes(nh_hw_mtu, self.mode)
|
||||
RNS.log(f"Signalling link MTU of {RNS.prettysize(nh_hw_mtu)} for link", RNS.LOG_DEBUG) # TODO: Remove debug
|
||||
self.request_data = self.pub_bytes+self.sig_pub_bytes+link_mtu
|
||||
else: signalling_bytes = Link.signalling_bytes(RNS.Reticulum.MTU, self.mode)
|
||||
RNS.log(f"Establishing link with mode {Link.MODE_DESCRIPTIONS[self.mode]}", RNS.LOG_DEBUG) # TODO: Remove debug
|
||||
self.request_data = self.pub_bytes+self.sig_pub_bytes+signalling_bytes
|
||||
self.packet = RNS.Packet(destination, self.request_data, packet_type=RNS.Packet.LINKREQUEST)
|
||||
self.packet.pack()
|
||||
self.establishment_cost += len(self.packet.raw)
|
||||
@@ -291,25 +355,25 @@ class Link:
|
||||
self.status = Link.HANDSHAKE
|
||||
self.shared_key = self.prv.exchange(self.peer_pub)
|
||||
|
||||
if self.mode == Link.MODE_AES128_CBC: derived_key_length = 32
|
||||
elif self.mode == Link.MODE_AES256_CBC: derived_key_length = 64
|
||||
else: raise TypeError(f"Invalid link mode {self.mode} on {self}")
|
||||
|
||||
self.derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
length=derived_key_length,
|
||||
derive_from=self.shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
)
|
||||
else:
|
||||
RNS.log("Handshake attempt on "+str(self)+" with invalid state "+str(self.status), RNS.LOG_ERROR)
|
||||
context=self.get_context())
|
||||
|
||||
else: RNS.log("Handshake attempt on "+str(self)+" with invalid state "+str(self.status), RNS.LOG_ERROR)
|
||||
|
||||
|
||||
def prove(self):
|
||||
mtu_bytes = b""
|
||||
if self.mtu != RNS.Reticulum.MTU:
|
||||
mtu_bytes = Link.mtu_bytes(self.mtu)
|
||||
|
||||
signed_data = self.link_id+self.pub_bytes+self.sig_pub_bytes+mtu_bytes
|
||||
signalling_bytes = Link.signalling_bytes(self.mtu, self.mode)
|
||||
signed_data = self.link_id+self.pub_bytes+self.sig_pub_bytes+signalling_bytes
|
||||
signature = self.owner.identity.sign(signed_data)
|
||||
|
||||
proof_data = signature+self.pub_bytes+mtu_bytes
|
||||
proof_data = signature+self.pub_bytes+signalling_bytes
|
||||
proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.LRPROOF)
|
||||
proof.send()
|
||||
self.establishment_cost += len(proof.raw)
|
||||
@@ -332,11 +396,14 @@ class Link:
|
||||
def validate_proof(self, packet):
|
||||
try:
|
||||
if self.status == Link.PENDING:
|
||||
mtu_bytes = b""
|
||||
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
|
||||
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)
|
||||
mtu_bytes = Link.mtu_bytes(confirmed_mtu)
|
||||
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
|
||||
|
||||
@@ -347,7 +414,7 @@ class Link:
|
||||
self.handshake()
|
||||
|
||||
self.establishment_cost += len(packet.raw)
|
||||
signed_data = self.link_id+self.peer_pub_bytes+self.peer_sig_pub_bytes+mtu_bytes
|
||||
signed_data = self.link_id+self.peer_pub_bytes+self.peer_sig_pub_bytes+signalling_bytes
|
||||
signature = packet.data[:RNS.Identity.SIGLENGTH//8]
|
||||
|
||||
if self.destination.identity.validate(signature, signed_data):
|
||||
@@ -363,11 +430,13 @@ 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 "+str(round(self.rtt, 3))+"s", RNS.LOG_DEBUG)
|
||||
RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+RNS.prettyshorttime(self.rtt), RNS.LOG_DEBUG)
|
||||
|
||||
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
|
||||
|
||||
self.__update_keepalive()
|
||||
|
||||
rtt_data = umsgpack.packb(self.rtt)
|
||||
rtt_packet = RNS.Packet(self, rtt_data, context=RNS.Packet.LRRTT)
|
||||
rtt_packet.send()
|
||||
@@ -475,6 +544,8 @@ class Link:
|
||||
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
|
||||
|
||||
self.__update_keepalive()
|
||||
|
||||
try:
|
||||
if self.owner.callbacks.link_established != None:
|
||||
self.owner.callbacks.link_established(self)
|
||||
@@ -535,6 +606,39 @@ class Link:
|
||||
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
|
||||
|
||||
def get_mdu(self):
|
||||
"""
|
||||
:returns: The packet MDU of an established link.
|
||||
"""
|
||||
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
|
||||
|
||||
def get_mode(self):
|
||||
"""
|
||||
:returns: The mode of an established link.
|
||||
"""
|
||||
return self.mode
|
||||
|
||||
def get_salt(self):
|
||||
return self.link_id
|
||||
|
||||
@@ -584,23 +688,23 @@ class Link:
|
||||
|
||||
def had_outbound(self, is_keepalive=False):
|
||||
self.last_outbound = time.time()
|
||||
if not is_keepalive:
|
||||
self.last_data = self.last_outbound
|
||||
if not is_keepalive: self.last_data = self.last_outbound
|
||||
else: self.last_keepalive = self.last_outbound
|
||||
|
||||
def __teardown_packet(self):
|
||||
teardown_packet = RNS.Packet(self, self.link_id, context=RNS.Packet.LINKCLOSE)
|
||||
teardown_packet.send()
|
||||
self.had_outbound()
|
||||
|
||||
def teardown(self):
|
||||
"""
|
||||
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.PENDING and self.status != Link.CLOSED:
|
||||
teardown_packet = RNS.Packet(self, self.link_id, context=RNS.Packet.LINKCLOSE)
|
||||
teardown_packet.send()
|
||||
self.had_outbound()
|
||||
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
|
||||
else:
|
||||
self.teardown_reason = Link.DESTINATION_CLOSED
|
||||
if self.initiator: self.teardown_reason = Link.INITIATOR_CLOSED
|
||||
else: self.teardown_reason = Link.DESTINATION_CLOSED
|
||||
self.link_closed()
|
||||
|
||||
def teardown_packet(self, packet):
|
||||
@@ -687,9 +791,10 @@ class Link:
|
||||
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 time.time() >= last_inbound + self.keepalive:
|
||||
if self.initiator:
|
||||
if now >= last_inbound + self.keepalive:
|
||||
if self.initiator and now >= self.last_keepalive + self.keepalive:
|
||||
self.send_keepalive()
|
||||
|
||||
if time.time() >= last_inbound + self.stale_time:
|
||||
@@ -703,6 +808,7 @@ class Link:
|
||||
|
||||
elif self.status == Link.STALE:
|
||||
sleep_time = 0.001
|
||||
self.__teardown_packet()
|
||||
self.status = Link.CLOSED
|
||||
self.teardown_reason = Link.TIMEOUT
|
||||
self.link_closed()
|
||||
@@ -715,6 +821,7 @@ class Link:
|
||||
self.teardown()
|
||||
sleep_time = 0.1
|
||||
|
||||
sleep_time = min(sleep_time, Link.WATCHDOG_MAX_SLEEP)
|
||||
sleep(sleep_time)
|
||||
|
||||
if not self.__track_phy_stats:
|
||||
@@ -737,6 +844,10 @@ class Link:
|
||||
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)
|
||||
self.stale_time = self.keepalive * Link.STALE_FACTOR
|
||||
|
||||
def send_keepalive(self):
|
||||
keepalive_packet = RNS.Packet(self, bytes([0xFF]), context=RNS.Packet.KEEPALIVE)
|
||||
@@ -755,6 +866,7 @@ class Link:
|
||||
response_generator = request_handler[1]
|
||||
allow = request_handler[2]
|
||||
allowed_list = request_handler[3]
|
||||
auto_compress = request_handler[4]
|
||||
|
||||
allowed = False
|
||||
if not allow == RNS.Destination.ALLOW_NONE:
|
||||
@@ -773,18 +885,29 @@ class Link:
|
||||
else:
|
||||
raise TypeError("Invalid signature for response generator callback")
|
||||
|
||||
if response != None:
|
||||
packed_response = umsgpack.packb([request_id, response])
|
||||
file_response = False
|
||||
file_handle = None
|
||||
if type(response) == list or type(response) == tuple:
|
||||
metadata = None
|
||||
if len(response) > 0 and type(response[0]) == io.BufferedReader:
|
||||
if len(response) > 1: metadata = response[1]
|
||||
file_handle = response[0]
|
||||
file_response = True
|
||||
|
||||
if len(packed_response) <= self.mdu:
|
||||
RNS.Packet(self, packed_response, RNS.Packet.DATA, context = RNS.Packet.RESPONSE).send()
|
||||
if response != None:
|
||||
if file_response:
|
||||
response_resource = RNS.Resource(file_handle, self, metadata=metadata, request_id = request_id, is_response = True, auto_compress=auto_compress)
|
||||
else:
|
||||
response_resource = RNS.Resource(packed_response, self, request_id = request_id, is_response = True)
|
||||
packed_response = umsgpack.packb([request_id, response])
|
||||
if len(packed_response) <= self.mdu:
|
||||
RNS.Packet(self, packed_response, RNS.Packet.DATA, context = RNS.Packet.RESPONSE).send()
|
||||
else:
|
||||
response_resource = RNS.Resource(packed_response, self, request_id = request_id, is_response = True, auto_compress=auto_compress)
|
||||
else:
|
||||
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):
|
||||
def handle_response(self, request_id, response_data, response_size, response_transfer_size, metadata=None):
|
||||
if self.status == Link.ACTIVE:
|
||||
remove = None
|
||||
for pending_request in self.pending_requests:
|
||||
@@ -795,7 +918,7 @@ class Link:
|
||||
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)
|
||||
pending_request.response_received(response_data, metadata)
|
||||
except Exception as e:
|
||||
RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
@@ -818,12 +941,21 @@ class Link:
|
||||
|
||||
def response_resource_concluded(self, resource):
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
packed_response = resource.data.read()
|
||||
unpacked_response = umsgpack.unpackb(packed_response)
|
||||
request_id = unpacked_response[0]
|
||||
response_data = unpacked_response[1]
|
||||
# If the response resource has metadata, this
|
||||
# is a file response, and we'll pass the open
|
||||
# file handle directly.
|
||||
if resource.has_metadata:
|
||||
self.handle_response(resource.request_id, resource.data, resource.total_size, resource.size, metadata=resource.metadata)
|
||||
|
||||
# If not, we'll unpack the response data and
|
||||
# pass the unpacked structure to the handler
|
||||
else:
|
||||
packed_response = resource.data.read()
|
||||
unpacked_response = umsgpack.unpackb(packed_response)
|
||||
request_id = unpacked_response[0]
|
||||
response_data = unpacked_response[1]
|
||||
self.handle_response(request_id, response_data, resource.total_size, resource.size)
|
||||
|
||||
self.handle_response(request_id, response_data, resource.total_size, resource.size)
|
||||
else:
|
||||
RNS.log("Incoming response resource failed with status: "+RNS.hexrep([resource.status]), RNS.LOG_DEBUG)
|
||||
for pending_request in self.pending_requests:
|
||||
@@ -1058,8 +1190,7 @@ class Link:
|
||||
def encrypt(self, plaintext):
|
||||
try:
|
||||
if not self.token:
|
||||
try:
|
||||
self.token = Token(self.derived_key)
|
||||
try: self.token = Token(self.derived_key)
|
||||
except Exception as e:
|
||||
RNS.log("Could not instantiate token while performing encryption on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
raise e
|
||||
@@ -1073,9 +1204,7 @@ class Link:
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
try:
|
||||
if not self.token:
|
||||
self.token = Token(self.derived_key)
|
||||
|
||||
if not self.token: self.token = Token(self.derived_key)
|
||||
return self.token.decrypt(ciphertext)
|
||||
|
||||
except Exception as e:
|
||||
@@ -1153,12 +1282,15 @@ class Link:
|
||||
self.callbacks.remote_identified = callback
|
||||
|
||||
def resource_concluded(self, resource):
|
||||
concluded_at = time.time()
|
||||
if resource in self.incoming_resources:
|
||||
self.last_resource_window = resource.window
|
||||
self.last_resource_eifr = resource.eifr
|
||||
self.incoming_resources.remove(resource)
|
||||
self.expected_rate = (resource.size*8)/(max(concluded_at-resource.started_transferring, 0.0001))
|
||||
if resource in self.outgoing_resources:
|
||||
self.outgoing_resources.remove(resource)
|
||||
self.expected_rate = (resource.size*8)/(max(concluded_at-resource.started_transferring, 0.0001))
|
||||
|
||||
def set_resource_strategy(self, resource_strategy):
|
||||
"""
|
||||
@@ -1247,6 +1379,7 @@ class RequestReceipt():
|
||||
self.response = None
|
||||
self.response_transfer_size = None
|
||||
self.response_size = None
|
||||
self.metadata = None
|
||||
self.status = RequestReceipt.SENT
|
||||
self.sent_at = time.time()
|
||||
self.progress = 0
|
||||
@@ -1333,10 +1466,11 @@ class RequestReceipt():
|
||||
resource.cancel()
|
||||
|
||||
|
||||
def response_received(self, response):
|
||||
def response_received(self, response, metadata=None):
|
||||
if not self.status == RequestReceipt.FAILED:
|
||||
self.progress = 1.0
|
||||
self.response = response
|
||||
self.metadata = metadata
|
||||
self.status = RequestReceipt.READY
|
||||
self.response_concluded_at = time.time()
|
||||
|
||||
|
||||
+26
-11
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -35,10 +43,10 @@ class Packet:
|
||||
|
||||
For ``RNS.Destination.GROUP`` destinations, Reticulum will use the
|
||||
pre-shared key configured for the destination. All packets to group
|
||||
destinations are encrypted with the same AES-128 key.
|
||||
destinations are encrypted with the same AES-256 key.
|
||||
|
||||
For ``RNS.Destination.SINGLE`` destinations, Reticulum will use a newly
|
||||
derived ephemeral AES-128 key for every packet.
|
||||
derived ephemeral AES-256 key for every packet.
|
||||
|
||||
For :ref:`RNS.Link<api-link>` destinations, Reticulum will use per-link
|
||||
ephemeral keys, and offers **Forward Secrecy**.
|
||||
@@ -106,6 +114,11 @@ class Packet:
|
||||
|
||||
TIMEOUT_PER_HOP = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT
|
||||
|
||||
__slots__ = "hops", "header", "header_type", "packet_type", "transport_type", "context", "context_flag", "destination"
|
||||
__slots__ += "transport_id", "data", "flags", "raw", "packed", "sent", "create_receipt", "receipt", "fromPacked", "MTU"
|
||||
__slots__ += "sent_at", "packet_hash", "ratchet_id", "attached_interface", "receiving_interface", "rssi", "snr", "q"
|
||||
__slots__ += "ciphertext", "plaintext", "destination_hash", "destination_type", "link", "map_hash"
|
||||
|
||||
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST,
|
||||
header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True, context_flag=FLAG_UNSET):
|
||||
|
||||
@@ -266,17 +279,19 @@ class Packet:
|
||||
if not self.sent:
|
||||
if self.destination.type == RNS.Destination.LINK:
|
||||
if self.destination.status == RNS.Link.CLOSED:
|
||||
raise IOError("Attempt to transmit over a closed link")
|
||||
RNS.log("Attempt to transmit over a closed link, dropping packet", RNS.LOG_DEBUG)
|
||||
self.sent = False
|
||||
self.receipt = None
|
||||
return False
|
||||
|
||||
else:
|
||||
self.destination.last_outbound = time.time()
|
||||
self.destination.tx += 1
|
||||
self.destination.txbytes += len(self.data)
|
||||
|
||||
if not self.packed:
|
||||
self.pack()
|
||||
if not self.packed: self.pack()
|
||||
|
||||
if RNS.Transport.outbound(self):
|
||||
return self.receipt
|
||||
if RNS.Transport.outbound(self): return self.receipt
|
||||
else:
|
||||
RNS.log("No interfaces could process the outbound packet", RNS.LOG_ERROR)
|
||||
self.sent = False
|
||||
|
||||
+12
-4
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
+179
-96
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -25,6 +33,7 @@ import os
|
||||
import bz2
|
||||
import math
|
||||
import time
|
||||
import struct
|
||||
import tempfile
|
||||
import threading
|
||||
from threading import Lock
|
||||
@@ -99,22 +108,20 @@ class Resource:
|
||||
# it is to be handled within reasonable
|
||||
# time constraint, even on small systems.
|
||||
#
|
||||
# A small system in this regard is
|
||||
# defined as a Raspberry Pi, which should
|
||||
# be able to compress, encrypt and hash-map
|
||||
# the resource in about 10 seconds.
|
||||
#
|
||||
# This constant will be used when determining
|
||||
# how to sequence the sending of large resources.
|
||||
#
|
||||
# Capped at 16777215 (0xFFFFFF) per segment to
|
||||
# fit in 3 bytes in resource advertisements.
|
||||
MAX_EFFICIENT_SIZE = 16 * 1024 * 1024 - 1
|
||||
MAX_EFFICIENT_SIZE = 1 * 1024 * 1024 - 1
|
||||
RESPONSE_MAX_GRACE_TIME = 10
|
||||
|
||||
# Max metadata size is 16777215 (0xFFFFFF) bytes
|
||||
METADATA_MAX_SIZE = 16 * 1024 * 1024 - 1
|
||||
|
||||
# The maximum size to auto-compress with
|
||||
# bz2 before sending.
|
||||
AUTO_COMPRESS_MAX_SIZE = MAX_EFFICIENT_SIZE
|
||||
AUTO_COMPRESS_MAX_SIZE = 64 * 1024 * 1024
|
||||
|
||||
PART_TIMEOUT_FACTOR = 4
|
||||
PART_TIMEOUT_FACTOR_AFTER_RTT = 2
|
||||
@@ -163,36 +170,40 @@ class Resource:
|
||||
resource = Resource(None, advertisement_packet.link, request_id = request_id)
|
||||
resource.status = Resource.TRANSFERRING
|
||||
|
||||
resource.flags = adv.f
|
||||
resource.size = adv.t
|
||||
resource.total_size = adv.d
|
||||
resource.uncompressed_size = adv.d
|
||||
resource.hash = adv.h
|
||||
resource.original_hash = adv.o
|
||||
resource.random_hash = adv.r
|
||||
resource.hashmap_raw = adv.m
|
||||
resource.encrypted = True if resource.flags & 0x01 else False
|
||||
resource.compressed = True if resource.flags >> 1 & 0x01 else False
|
||||
resource.initiator = False
|
||||
resource.callback = callback
|
||||
resource.__progress_callback = progress_callback
|
||||
resource.total_parts = int(math.ceil(resource.size/float(resource.sdu)))
|
||||
resource.received_count = 0
|
||||
resource.outstanding_parts = 0
|
||||
resource.parts = [None] * resource.total_parts
|
||||
resource.window = Resource.WINDOW
|
||||
resource.window_max = Resource.WINDOW_MAX_SLOW
|
||||
resource.window_min = Resource.WINDOW_MIN
|
||||
resource.window_flexibility = Resource.WINDOW_FLEXIBILITY
|
||||
resource.last_activity = time.time()
|
||||
resource.flags = adv.f
|
||||
resource.size = adv.t
|
||||
resource.total_size = adv.d
|
||||
resource.uncompressed_size = adv.d
|
||||
resource.hash = adv.h
|
||||
resource.original_hash = adv.o
|
||||
resource.random_hash = adv.r
|
||||
resource.hashmap_raw = adv.m
|
||||
resource.encrypted = True if resource.flags & 0x01 else False
|
||||
resource.compressed = True if resource.flags >> 1 & 0x01 else False
|
||||
resource.initiator = False
|
||||
resource.callback = callback
|
||||
resource.__progress_callback = progress_callback
|
||||
resource.total_parts = int(math.ceil(resource.size/float(resource.sdu)))
|
||||
resource.received_count = 0
|
||||
resource.outstanding_parts = 0
|
||||
resource.parts = [None] * resource.total_parts
|
||||
resource.window = Resource.WINDOW
|
||||
resource.window_max = Resource.WINDOW_MAX_SLOW
|
||||
resource.window_min = Resource.WINDOW_MIN
|
||||
resource.window_flexibility = Resource.WINDOW_FLEXIBILITY
|
||||
resource.last_activity = time.time()
|
||||
resource.started_transferring = resource.last_activity
|
||||
|
||||
resource.storagepath = RNS.Reticulum.resourcepath+"/"+resource.original_hash.hex()
|
||||
resource.segment_index = adv.i
|
||||
resource.total_segments = adv.l
|
||||
if adv.l > 1:
|
||||
resource.split = True
|
||||
else:
|
||||
resource.split = False
|
||||
resource.storagepath = RNS.Reticulum.resourcepath+"/"+resource.original_hash.hex()
|
||||
resource.meta_storagepath = resource.storagepath+".meta"
|
||||
resource.segment_index = adv.i
|
||||
resource.total_segments = adv.l
|
||||
|
||||
if adv.l > 1: resource.split = True
|
||||
else: resource.split = False
|
||||
|
||||
if adv.x: resource.has_metadata = True
|
||||
else: resource.has_metadata = False
|
||||
|
||||
resource.hashmap = [None] * resource.total_parts
|
||||
resource.hashmap_height = 0
|
||||
@@ -218,9 +229,7 @@ class Resource:
|
||||
RNS.log("Error while executing resource started callback from "+str(resource)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
resource.hashmap_update(0, resource.hashmap_raw)
|
||||
|
||||
resource.watchdog_job()
|
||||
|
||||
return resource
|
||||
|
||||
else:
|
||||
@@ -234,15 +243,33 @@ class Resource:
|
||||
# Create a resource for transmission to a remote destination
|
||||
# The data passed can be either a bytes-array or a file opened
|
||||
# in binary read mode.
|
||||
def __init__(self, data, link, advertise=True, auto_compress=True, callback=None, progress_callback=None, timeout = None, segment_index = 1, original_hash = None, request_id = None, is_response = False):
|
||||
def __init__(self, data, link, metadata=None, advertise=True, auto_compress=True, callback=None, progress_callback=None,
|
||||
timeout = None, segment_index = 1, original_hash = None, request_id = None, is_response = False, sent_metadata_size=0):
|
||||
|
||||
data_size = None
|
||||
resource_data = None
|
||||
self.assembly_lock = False
|
||||
self.preparing_next_segment = False
|
||||
self.next_segment = None
|
||||
self.metadata = None
|
||||
self.has_metadata = False
|
||||
self.metadata_size = sent_metadata_size
|
||||
|
||||
if metadata != None:
|
||||
packed_metadata = umsgpack.packb(metadata)
|
||||
metadata_size = len(packed_metadata)
|
||||
if metadata_size > Resource.METADATA_MAX_SIZE:
|
||||
raise SystemError("Resource metadata size exceeded")
|
||||
else:
|
||||
self.metadata = struct.pack(">I", metadata_size)[1:] + packed_metadata
|
||||
self.metadata_size = len(self.metadata)
|
||||
self.has_metadata = True
|
||||
else:
|
||||
self.metadata = b""
|
||||
if sent_metadata_size > 0: self.has_metadata = True
|
||||
|
||||
if data != None:
|
||||
if not hasattr(data, "read") and len(data) > Resource.MAX_EFFICIENT_SIZE:
|
||||
if not hasattr(data, "read") and self.metadata_size + len(data) > Resource.MAX_EFFICIENT_SIZE:
|
||||
original_data = data
|
||||
data_size = len(original_data)
|
||||
data = tempfile.TemporaryFile()
|
||||
@@ -250,31 +277,43 @@ class Resource:
|
||||
del original_data
|
||||
|
||||
if hasattr(data, "read"):
|
||||
if data_size == None:
|
||||
data_size = os.stat(data.name).st_size
|
||||
if data_size == None: data_size = os.stat(data.name).st_size
|
||||
self.total_size = data_size + self.metadata_size
|
||||
|
||||
self.total_size = data_size
|
||||
|
||||
if data_size <= Resource.MAX_EFFICIENT_SIZE:
|
||||
if self.total_size <= Resource.MAX_EFFICIENT_SIZE:
|
||||
self.total_segments = 1
|
||||
self.segment_index = 1
|
||||
self.split = False
|
||||
resource_data = data.read()
|
||||
data.close()
|
||||
|
||||
else:
|
||||
self.total_segments = ((data_size-1)//Resource.MAX_EFFICIENT_SIZE)+1
|
||||
# self.total_segments = ((data_size-1)//Resource.MAX_EFFICIENT_SIZE)+1
|
||||
# self.segment_index = segment_index
|
||||
# self.split = True
|
||||
# seek_index = segment_index-1
|
||||
# seek_position = seek_index*Resource.MAX_EFFICIENT_SIZE
|
||||
|
||||
self.total_segments = ((self.total_size-1)//Resource.MAX_EFFICIENT_SIZE)+1
|
||||
self.segment_index = segment_index
|
||||
self.split = True
|
||||
seek_index = segment_index-1
|
||||
seek_position = seek_index*Resource.MAX_EFFICIENT_SIZE
|
||||
first_read_size = Resource.MAX_EFFICIENT_SIZE - self.metadata_size
|
||||
|
||||
if segment_index == 1:
|
||||
seek_position = 0
|
||||
segment_read_size = first_read_size
|
||||
else:
|
||||
seek_position = first_read_size + ((seek_index-1)*Resource.MAX_EFFICIENT_SIZE)
|
||||
segment_read_size = Resource.MAX_EFFICIENT_SIZE
|
||||
|
||||
data.seek(seek_position)
|
||||
resource_data = data.read(Resource.MAX_EFFICIENT_SIZE)
|
||||
resource_data = data.read(segment_read_size)
|
||||
self.input_file = data
|
||||
|
||||
elif isinstance(data, bytes):
|
||||
data_size = len(data)
|
||||
self.total_size = data_size
|
||||
self.total_size = data_size + self.metadata_size
|
||||
|
||||
resource_data = data
|
||||
self.total_segments = 1
|
||||
@@ -287,7 +326,9 @@ class Resource:
|
||||
else:
|
||||
raise TypeError("Invalid data instance type passed to resource initialisation")
|
||||
|
||||
data = resource_data
|
||||
if resource_data:
|
||||
if self.has_metadata: data = self.metadata + resource_data
|
||||
else: data = resource_data
|
||||
|
||||
self.status = Resource.NONE
|
||||
self.link = link
|
||||
@@ -316,8 +357,18 @@ class Resource:
|
||||
self.fast_rate_rounds = 0
|
||||
self.very_slow_rate_rounds = 0
|
||||
self.request_id = request_id
|
||||
self.started_transferring = None
|
||||
self.is_response = is_response
|
||||
self.auto_compress = auto_compress
|
||||
self.auto_compress_limit = Resource.AUTO_COMPRESS_MAX_SIZE
|
||||
self.auto_compress_option = auto_compress
|
||||
|
||||
if type(auto_compress) == bool:
|
||||
self.auto_compress = auto_compress
|
||||
elif type(auto_compress) == int:
|
||||
self.auto_compress = True
|
||||
self.auto_compress_limit = auto_compress
|
||||
else:
|
||||
raise TypeError(f"Invalid type {type(auto_compress)} for auto_compress option")
|
||||
|
||||
self.req_hashlist = []
|
||||
self.receiver_min_consecutive_height = 0
|
||||
@@ -333,7 +384,7 @@ class Resource:
|
||||
self.uncompressed_data = data
|
||||
|
||||
compression_began = time.time()
|
||||
if (auto_compress and len(self.uncompressed_data) <= Resource.AUTO_COMPRESS_MAX_SIZE):
|
||||
if self.auto_compress and data_size <= self.auto_compress_limit:
|
||||
RNS.log("Compressing resource data...", RNS.LOG_EXTREME)
|
||||
self.compressed_data = bz2.compress(self.uncompressed_data)
|
||||
RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_EXTREME)
|
||||
@@ -352,19 +403,20 @@ class Resource:
|
||||
self.data += self.compressed_data
|
||||
|
||||
self.compressed = True
|
||||
self.uncompressed_data = None
|
||||
|
||||
else:
|
||||
self.data = b""
|
||||
self.data += RNS.Identity.get_random_hash()[:Resource.RANDOM_HASH_SIZE]
|
||||
self.data += self.uncompressed_data
|
||||
self.uncompressed_data = self.data
|
||||
|
||||
self.compressed = False
|
||||
self.compressed_data = None
|
||||
if auto_compress:
|
||||
if self.auto_compress and data_size <= self.auto_compress_limit:
|
||||
RNS.log("Compression did not decrease size, sending uncompressed", RNS.LOG_EXTREME)
|
||||
|
||||
self.compressed_data = None
|
||||
self.uncompressed_data = None
|
||||
|
||||
# Resources handle encryption directly to
|
||||
# make optimal use of packet MTU on an entire
|
||||
# encrypted stream. The Resource instance will
|
||||
@@ -417,7 +469,8 @@ class Resource:
|
||||
self.parts.append(part)
|
||||
|
||||
RNS.log("Hashmap computation concluded in "+str(round(time.time()-hashmap_computation_began, 3))+" seconds", RNS.LOG_EXTREME)
|
||||
|
||||
|
||||
self.data = None
|
||||
if advertise:
|
||||
self.advertise()
|
||||
else:
|
||||
@@ -470,6 +523,7 @@ class Resource:
|
||||
try:
|
||||
self.advertisement_packet.send()
|
||||
self.last_activity = time.time()
|
||||
self.started_transferring = self.last_activity
|
||||
self.adv_sent = self.last_activity
|
||||
self.rtt = None
|
||||
self.status = Resource.ADVERTISED
|
||||
@@ -498,10 +552,10 @@ class Resource:
|
||||
expected_inflight_rate = self.link.establishment_cost*8 / rtt
|
||||
|
||||
self.eifr = expected_inflight_rate
|
||||
if self.link: self.link.expected_rate = self.eifr
|
||||
|
||||
def watchdog_job(self):
|
||||
thread = threading.Thread(target=self.__watchdog_job)
|
||||
thread.daemon = True
|
||||
thread = threading.Thread(target=self.__watchdog_job, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def __watchdog_job(self):
|
||||
@@ -547,13 +601,14 @@ class Resource:
|
||||
else:
|
||||
sleep_time = self.last_activity + self.part_timeout_factor*((3*self.sdu)/self.eifr) + Resource.RETRY_GRACE_TIME + extra_wait - time.time()
|
||||
|
||||
# TODO: Remove debug at some point
|
||||
# RNS.log(f"EIFR {RNS.prettyspeed(self.eifr)}, ETOF {RNS.prettyshorttime(expected_tof_remaining)} ", RNS.LOG_DEBUG, pt=True)
|
||||
# RNS.log(f"Resource ST {RNS.prettyshorttime(sleep_time)}, RTT {RNS.prettyshorttime(self.rtt or self.link.rtt)}, {self.outstanding_parts} left", RNS.LOG_DEBUG, pt=True)
|
||||
|
||||
if sleep_time < 0:
|
||||
if self.retries_left > 0:
|
||||
ms = "" if self.outstanding_parts == 1 else "s"
|
||||
RNS.log("Timed out waiting for "+str(self.outstanding_parts)+" part"+ms+", requesting retry", RNS.LOG_DEBUG)
|
||||
RNS.log(f"Timed out waiting for {self.outstanding_parts} part{ms}, requesting retry on {self}", RNS.LOG_DEBUG)
|
||||
if self.window > self.window_min:
|
||||
self.window -= 1
|
||||
if self.window_max > self.window_min:
|
||||
@@ -616,29 +671,37 @@ class Resource:
|
||||
self.status = Resource.ASSEMBLING
|
||||
stream = b"".join(self.parts)
|
||||
|
||||
if self.encrypted:
|
||||
data = self.link.decrypt(stream)
|
||||
else:
|
||||
data = stream
|
||||
if self.encrypted: data = self.link.decrypt(stream)
|
||||
else: data = stream
|
||||
|
||||
# Strip off random hash
|
||||
data = data[Resource.RANDOM_HASH_SIZE:]
|
||||
|
||||
if self.compressed:
|
||||
self.data = bz2.decompress(data)
|
||||
else:
|
||||
self.data = data
|
||||
if self.compressed: self.data = bz2.decompress(data)
|
||||
else: self.data = data
|
||||
|
||||
calculated_hash = RNS.Identity.full_hash(self.data+self.random_hash)
|
||||
|
||||
if calculated_hash == self.hash:
|
||||
if self.has_metadata and self.segment_index == 1:
|
||||
# TODO: Add early metadata_ready callback
|
||||
metadata_size = self.data[0] << 16 | self.data[1] << 8 | self.data[2]
|
||||
packed_metadata = self.data[3:3+metadata_size]
|
||||
metadata_file = open(self.meta_storagepath, "wb")
|
||||
metadata_file.write(packed_metadata)
|
||||
metadata_file.close()
|
||||
del packed_metadata
|
||||
data = self.data[3+metadata_size:]
|
||||
else:
|
||||
data = self.data
|
||||
|
||||
self.file = open(self.storagepath, "ab")
|
||||
self.file.write(self.data)
|
||||
self.file.write(data)
|
||||
self.file.close()
|
||||
self.status = Resource.COMPLETE
|
||||
del data
|
||||
self.prove()
|
||||
else:
|
||||
self.status = Resource.CORRUPT
|
||||
|
||||
else: self.status = Resource.CORRUPT
|
||||
|
||||
|
||||
except Exception as e:
|
||||
@@ -650,21 +713,27 @@ class Resource:
|
||||
|
||||
if self.segment_index == self.total_segments:
|
||||
if self.callback != None:
|
||||
if not os.path.isfile(self.meta_storagepath):
|
||||
self.metadata = None
|
||||
else:
|
||||
metadata_file = open(self.meta_storagepath, "rb")
|
||||
self.metadata = umsgpack.unpackb(metadata_file.read())
|
||||
metadata_file.close()
|
||||
try: os.unlink(self.meta_storagepath)
|
||||
except Exception as e:
|
||||
RNS.log(f"Error while cleaning up resource metadata file, the contained exception was: {e}", RNS.LOG_ERROR)
|
||||
|
||||
self.data = open(self.storagepath, "rb")
|
||||
try:
|
||||
self.callback(self)
|
||||
try: self.callback(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing resource assembled callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
try:
|
||||
if hasattr(self.data, "close") and callable(self.data.close):
|
||||
self.data.close()
|
||||
|
||||
os.unlink(self.storagepath)
|
||||
if hasattr(self.data, "close") and callable(self.data.close): self.data.close()
|
||||
if os.path.isfile(self.storagepath): os.unlink(self.storagepath)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while cleaning up resource files, the contained exception was:", RNS.LOG_ERROR)
|
||||
RNS.log(str(e))
|
||||
RNS.log(f"Error while cleaning up resource files, the contained exception was: {e}", RNS.LOG_ERROR)
|
||||
else:
|
||||
RNS.log("Resource segment "+str(self.segment_index)+" of "+str(self.total_segments)+" received, waiting for next segment to be announced", RNS.LOG_DEBUG)
|
||||
|
||||
@@ -695,7 +764,8 @@ class Resource:
|
||||
request_id = self.request_id,
|
||||
is_response = self.is_response,
|
||||
advertise = False,
|
||||
auto_compress = self.auto_compress,
|
||||
auto_compress = self.auto_compress_option,
|
||||
sent_metadata_size = self.metadata_size,
|
||||
)
|
||||
|
||||
def validate_proof(self, proof_data):
|
||||
@@ -708,18 +778,18 @@ class Resource:
|
||||
# If all segments were processed, we'll
|
||||
# signal that the resource sending concluded
|
||||
if self.callback != None:
|
||||
try:
|
||||
self.callback(self)
|
||||
except Exception as e:
|
||||
RNS.log("Error while executing resource concluded callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
try: self.callback(self)
|
||||
except Exception as e: RNS.log("Error while executing resource concluded callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
finally:
|
||||
try:
|
||||
if hasattr(self, "input_file"):
|
||||
if hasattr(self.input_file, "close") and callable(self.input_file.close):
|
||||
self.input_file.close()
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR)
|
||||
if hasattr(self.input_file, "close") and callable(self.input_file.close): self.input_file.close()
|
||||
except Exception as e: RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR)
|
||||
else:
|
||||
try:
|
||||
if hasattr(self, "input_file"):
|
||||
if hasattr(self.input_file, "close") and callable(self.input_file.close): self.input_file.close()
|
||||
except Exception as e: RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR)
|
||||
else:
|
||||
# Otherwise we'll recursively create the
|
||||
# next segment of the resource
|
||||
@@ -727,8 +797,16 @@ class Resource:
|
||||
RNS.log(f"Next segment preparation for resource {self} was not started yet, manually preparing now. This will cause transfer slowdown.", RNS.LOG_WARNING)
|
||||
self.__prepare_next_segment()
|
||||
|
||||
while self.next_segment == None:
|
||||
time.sleep(0.05)
|
||||
while self.next_segment == None: time.sleep(0.05)
|
||||
|
||||
self.data = None
|
||||
self.metadata = None
|
||||
self.parts = None
|
||||
self.input_file = None
|
||||
self.link = None
|
||||
self.req_hashlist = None
|
||||
self.hashmap = None
|
||||
|
||||
self.next_segment.advertise()
|
||||
else:
|
||||
pass
|
||||
@@ -1190,6 +1268,7 @@ class ResourceAdvertisement:
|
||||
self.c = resource.compressed # Compression flag
|
||||
self.e = resource.encrypted # Encryption flag
|
||||
self.s = resource.split # Split flag
|
||||
self.x = resource.has_metadata # Metadata flag
|
||||
self.i = resource.segment_index # Segment index
|
||||
self.l = resource.total_segments # Total segments
|
||||
self.q = resource.request_id # ID of associated request
|
||||
@@ -1205,7 +1284,7 @@ class ResourceAdvertisement:
|
||||
self.p = True
|
||||
|
||||
# Flags
|
||||
self.f = 0x00 | self.p << 4 | self.u << 3 | self.s << 2 | self.c << 1 | self.e
|
||||
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
|
||||
@@ -1225,6 +1304,9 @@ class ResourceAdvertisement:
|
||||
def is_compressed(self):
|
||||
return self.c
|
||||
|
||||
def has_metadata(self):
|
||||
return self.x
|
||||
|
||||
def get_link(self):
|
||||
return self.link
|
||||
|
||||
@@ -1274,5 +1356,6 @@ class ResourceAdvertisement:
|
||||
adv.s = True if ((adv.f >> 2) & 0x01) == 0x01 else False
|
||||
adv.u = True if ((adv.f >> 3) & 0x01) == 0x01 else False
|
||||
adv.p = True if ((adv.f >> 4) & 0x01) == 0x01 else False
|
||||
adv.x = True if ((adv.f >> 5) & 0x01) == 0x01 else False
|
||||
|
||||
return adv
|
||||
+709
-391
File diff suppressed because it is too large
Load Diff
+1013
-668
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
+169
-156
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -11,8 +11,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -25,6 +33,7 @@
|
||||
import RNS
|
||||
import argparse
|
||||
import threading
|
||||
import shutil
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
@@ -34,22 +43,45 @@ from RNS._version import __version__
|
||||
APP_NAME = "rncp"
|
||||
allow_all = False
|
||||
allow_fetch = False
|
||||
allow_overwrite_on_receive = False
|
||||
fetch_auto_compress = True
|
||||
fetch_jail = None
|
||||
save_path = None
|
||||
show_phy_rates = False
|
||||
allowed_identity_hashes = []
|
||||
identity = None
|
||||
|
||||
def prepare_identity(identity_path):
|
||||
global identity
|
||||
if identity_path == None:
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
if identity == None:
|
||||
RNS.log(f"Could not load identity for rncp. The identity file at \"{identity_path}\" may be corrupt or unreadable.", RNS.LOG_ERROR)
|
||||
RNS.exit(2)
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
|
||||
REQ_FETCH_NOT_ALLOWED = 0xF0
|
||||
|
||||
es = " "
|
||||
erase_str = "\33[2K\r"
|
||||
|
||||
def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False,
|
||||
limit = None, disable_auth = None, fetch_allowed = False, jail = None, save = None, announce = False):
|
||||
global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path
|
||||
from tempfile import TemporaryFile
|
||||
def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed = [], display_identity = False,
|
||||
limit = None, disable_auth = None, fetch_allowed = False, no_compress=False,
|
||||
jail = None, save = None, announce = False, allow_overwrite=False):
|
||||
|
||||
global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path, identity
|
||||
global fetch_auto_compress, allow_overwrite_on_receive
|
||||
|
||||
allow_fetch = fetch_allowed
|
||||
fetch_auto_compress = not no_compress
|
||||
allow_overwrite_on_receive = allow_overwrite
|
||||
identity = None
|
||||
if announce < 0:
|
||||
announce = False
|
||||
@@ -68,28 +100,21 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
|
||||
save_path = sp
|
||||
else:
|
||||
RNS.log("Output directory not writable", RNS.LOG_ERROR)
|
||||
exit(4)
|
||||
RNS.exit(4)
|
||||
else:
|
||||
RNS.log("Output directory not found", RNS.LOG_ERROR)
|
||||
exit(3)
|
||||
RNS.exit(3)
|
||||
|
||||
RNS.log("Saving received files in \""+save_path+"\"", RNS.LOG_VERBOSE)
|
||||
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
prepare_identity(identitypath)
|
||||
|
||||
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, "receive")
|
||||
|
||||
if display_identity:
|
||||
print("Identity : "+str(identity))
|
||||
print("Listening on : "+RNS.prettyhexrep(destination.hash))
|
||||
exit(0)
|
||||
RNS.exit(0)
|
||||
|
||||
if disable_auth:
|
||||
allow_all = True
|
||||
@@ -139,13 +164,13 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
|
||||
if len(allowed_identity_hashes) < 1 and not disable_auth:
|
||||
print("Warning: No allowed identities configured, rncp will not accept any files!")
|
||||
|
||||
def fetch_request(path, data, request_id, link_id, remote_identity, requested_at):
|
||||
global allow_fetch, fetch_jail
|
||||
global allow_fetch, fetch_jail, fetch_auto_compress
|
||||
if not allow_fetch:
|
||||
return REQ_FETCH_NOT_ALLOWED
|
||||
|
||||
@@ -171,22 +196,15 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
|
||||
if target_link != None:
|
||||
RNS.log("Sending file "+str(file_path)+" to client", RNS.LOG_VERBOSE)
|
||||
|
||||
temp_file = TemporaryFile()
|
||||
real_file = open(file_path, "rb")
|
||||
filename_bytes = os.path.basename(file_path).encode("utf-8")
|
||||
filename_len = len(filename_bytes)
|
||||
try:
|
||||
metadata = {"name": os.path.basename(file_path).encode("utf-8") }
|
||||
fetch_resource = RNS.Resource(open(file_path, "rb"), target_link, metadata=metadata, auto_compress=fetch_auto_compress)
|
||||
return True
|
||||
|
||||
if filename_len > 0xFFFF:
|
||||
print("Filename exceeds max size, cannot send")
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not send file to client. The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
temp_file.write(filename_len.to_bytes(2, "big"))
|
||||
temp_file.write(filename_bytes)
|
||||
temp_file.write(real_file.read())
|
||||
temp_file.seek(0)
|
||||
|
||||
fetch_resource = RNS.Resource(temp_file, target_link)
|
||||
return True
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -211,8 +229,7 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
|
||||
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
while True: time.sleep(1)
|
||||
|
||||
def client_link_established(link):
|
||||
RNS.log("Incoming link established", RNS.LOG_VERBOSE)
|
||||
@@ -257,34 +274,42 @@ def receive_resource_started(resource):
|
||||
print("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str)
|
||||
|
||||
def receive_resource_concluded(resource):
|
||||
global save_path
|
||||
global save_path, allow_overwrite_on_receive
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
print(str(resource)+" completed")
|
||||
|
||||
if resource.total_size > 4:
|
||||
filename_len = int.from_bytes(resource.data.read(2), "big")
|
||||
filename = resource.data.read(filename_len).decode("utf-8")
|
||||
|
||||
counter = 0
|
||||
if save_path:
|
||||
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
|
||||
if not saved_filename.startswith(save_path+"/"):
|
||||
RNS.log(f"Invalid save path {saved_filename}, ignoring", RNS.LOG_ERROR)
|
||||
return
|
||||
else:
|
||||
saved_filename = filename
|
||||
|
||||
full_save_path = saved_filename
|
||||
while os.path.isfile(full_save_path):
|
||||
counter += 1
|
||||
full_save_path = saved_filename+"."+str(counter)
|
||||
|
||||
file = open(full_save_path, "wb")
|
||||
file.write(resource.data.read())
|
||||
file.close()
|
||||
if resource.metadata == None:
|
||||
print("Invalid data received, ignoring resource")
|
||||
return
|
||||
|
||||
else:
|
||||
print("Invalid data received, ignoring resource")
|
||||
try:
|
||||
filename = os.path.basename(resource.metadata["name"].decode("utf-8"))
|
||||
counter = 0
|
||||
if save_path:
|
||||
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
|
||||
if not saved_filename.startswith(save_path+"/"):
|
||||
RNS.log(f"Invalid save path {saved_filename}, ignoring", RNS.LOG_ERROR)
|
||||
return
|
||||
else:
|
||||
saved_filename = filename
|
||||
|
||||
full_save_path = saved_filename
|
||||
if allow_overwrite_on_receive:
|
||||
if os.path.isfile(full_save_path):
|
||||
try: os.unlink(full_save_path)
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not overwrite existing file {full_save_path}, renaming instead", RNS.LOG_ERROR)
|
||||
|
||||
while os.path.isfile(full_save_path):
|
||||
counter += 1
|
||||
full_save_path = saved_filename+"."+str(counter)
|
||||
|
||||
shutil.move(resource.data.name, full_save_path)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while saving received resource: {e}", RNS.LOG_ERROR)
|
||||
return
|
||||
|
||||
else:
|
||||
print("Resource failed")
|
||||
@@ -330,10 +355,11 @@ def sender_progress(resource):
|
||||
resource_done = True
|
||||
|
||||
link = None
|
||||
def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None):
|
||||
global current_resource, resource_done, link, speed, show_phy_rates, save_path
|
||||
def fetch(configdir, identitypath = None, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None, allow_overwrite=False):
|
||||
global current_resource, resource_done, link, speed, show_phy_rates, save_path, allow_overwrite_on_receive, identity
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
show_phy_rates = phy_rates
|
||||
allow_overwrite_on_receive = allow_overwrite
|
||||
|
||||
if save:
|
||||
sp = os.path.abspath(os.path.expanduser(save))
|
||||
@@ -342,10 +368,10 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
save_path = sp
|
||||
else:
|
||||
RNS.log("Output directory not writable", RNS.LOG_ERROR)
|
||||
exit(4)
|
||||
RNS.exit(4)
|
||||
else:
|
||||
RNS.log("Output directory not found", RNS.LOG_ERROR)
|
||||
exit(3)
|
||||
RNS.exit(3)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
@@ -357,23 +383,12 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
||||
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
if identity == None:
|
||||
RNS.log("Could not load identity for rncp. The identity file at \""+str(identity_path)+"\" may be corrupt or unreadable.", RNS.LOG_ERROR)
|
||||
exit(2)
|
||||
else:
|
||||
identity = None
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
prepare_identity(identitypath)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
@@ -398,7 +413,7 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
print("Path not found")
|
||||
else:
|
||||
print(f"{erase_str}Path not found")
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
else:
|
||||
if silent:
|
||||
print("Establishing link with "+RNS.prettyhexrep(destination_hash))
|
||||
@@ -427,7 +442,7 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
print("Could not establish link with "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print(f"{erase_str}Could not establish link with "+RNS.prettyhexrep(destination_hash))
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
else:
|
||||
if silent:
|
||||
print("Requesting file from remote...")
|
||||
@@ -441,6 +456,7 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
resource_resolved = False
|
||||
resource_status = "unrequested"
|
||||
current_resource = None
|
||||
current_transfer_started = None
|
||||
def request_response(request_receipt):
|
||||
nonlocal request_resolved, request_status
|
||||
if request_receipt.response == False:
|
||||
@@ -460,39 +476,48 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
request_resolved = True
|
||||
|
||||
def fetch_resource_started(resource):
|
||||
nonlocal resource_status
|
||||
nonlocal resource_status, current_transfer_started
|
||||
current_resource = resource
|
||||
current_resource.progress_callback(sender_progress)
|
||||
resource_status = "started"
|
||||
if not current_transfer_started: current_transfer_started = time.time()
|
||||
|
||||
def fetch_resource_concluded(resource):
|
||||
nonlocal resource_resolved, resource_status
|
||||
global save_path
|
||||
global save_path, allow_overwrite_on_receive
|
||||
if resource.status == RNS.Resource.COMPLETE:
|
||||
if resource.total_size > 4:
|
||||
filename_len = int.from_bytes(resource.data.read(2), "big")
|
||||
filename = resource.data.read(filename_len).decode("utf-8")
|
||||
|
||||
counter = 0
|
||||
if save_path:
|
||||
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
|
||||
|
||||
else:
|
||||
saved_filename = filename
|
||||
|
||||
full_save_path = saved_filename
|
||||
while os.path.isfile(full_save_path):
|
||||
counter += 1
|
||||
full_save_path = saved_filename+"."+str(counter)
|
||||
|
||||
file = open(full_save_path, "wb")
|
||||
file.write(resource.data.read())
|
||||
file.close()
|
||||
resource_status = "completed"
|
||||
if resource.metadata == None:
|
||||
print("Invalid data received, ignoring resource")
|
||||
return
|
||||
|
||||
else:
|
||||
print("Invalid data received, ignoring resource")
|
||||
resource_status = "invalid_data"
|
||||
try:
|
||||
filename = os.path.basename(resource.metadata["name"].decode("utf-8"))
|
||||
counter = 0
|
||||
if save_path:
|
||||
saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename))
|
||||
if not saved_filename.startswith(save_path+"/"):
|
||||
print(f"Invalid save path {saved_filename}, ignoring")
|
||||
return
|
||||
else:
|
||||
saved_filename = filename
|
||||
|
||||
full_save_path = saved_filename
|
||||
if allow_overwrite_on_receive:
|
||||
if os.path.isfile(full_save_path):
|
||||
try: os.unlink(full_save_path)
|
||||
except Exception as e:
|
||||
print(f"Could not overwrite existing file {full_save_path}, renaming instead")
|
||||
|
||||
while os.path.isfile(full_save_path):
|
||||
counter += 1
|
||||
full_save_path = saved_filename+"."+str(counter)
|
||||
|
||||
shutil.move(resource.data.name, full_save_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred while saving received resource: {e}")
|
||||
return
|
||||
|
||||
else:
|
||||
print("Resource failed")
|
||||
@@ -518,25 +543,25 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
print("Fetch request failed, fetching the file "+str(file)+" was not allowed by the remote")
|
||||
link.teardown()
|
||||
time.sleep(0.15)
|
||||
exit(0)
|
||||
RNS.exit(0)
|
||||
elif request_status == "not_found":
|
||||
if not silent: print(f"{erase_str}", end="")
|
||||
print("Fetch request failed, the file "+str(file)+" was not found on the remote")
|
||||
link.teardown()
|
||||
time.sleep(0.15)
|
||||
exit(0)
|
||||
RNS.exit(0)
|
||||
elif request_status == "remote_error":
|
||||
if not silent: print(f"{erase_str}", end="")
|
||||
print("Fetch request failed due to an error on the remote system")
|
||||
link.teardown()
|
||||
time.sleep(0.15)
|
||||
exit(0)
|
||||
RNS.exit(0)
|
||||
elif request_status == "unknown":
|
||||
if not silent: print(f"{erase_str}", end="")
|
||||
print("Fetch request failed due to an unknown error (probably not authorised)")
|
||||
link.teardown()
|
||||
time.sleep(0.15)
|
||||
exit(0)
|
||||
RNS.exit(0)
|
||||
elif request_status == "found":
|
||||
if not silent: print(f"{erase_str}", end="")
|
||||
|
||||
@@ -558,34 +583,38 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
if prg != 1.0:
|
||||
print(f"{erase_str}Transferring file {syms[i]} {stat_str}", end=es)
|
||||
else:
|
||||
end_time = time.time(); delta_time = end_time - current_transfer_started
|
||||
speed = current_resource.total_size/delta_time; dt_str = RNS.prettytime(delta_time)
|
||||
ss = size_str(speed, "b")
|
||||
stat_str = f"{percent}% - {ps} of {ts} in {dt_str} - {ss}ps{phy_str}"
|
||||
print(f"{erase_str}Transfer complete {stat_str}", end=es)
|
||||
else:
|
||||
print(f"{erase_str}Waiting for transfer to start {syms[i]} ", end=es)
|
||||
sys.stdout.flush()
|
||||
i = (i+1)%len(syms)
|
||||
|
||||
if current_resource.status != RNS.Resource.COMPLETE:
|
||||
if not current_resource or current_resource.status != RNS.Resource.COMPLETE:
|
||||
if silent:
|
||||
print("The transfer failed")
|
||||
else:
|
||||
print(f"{erase_str}The transfer failed")
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
else:
|
||||
if silent:
|
||||
print(str(file)+" fetched from "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print("\n"+str(file)+" fetched from "+RNS.prettyhexrep(destination_hash))
|
||||
link.teardown()
|
||||
time.sleep(0.15)
|
||||
exit(0)
|
||||
time.sleep(0.1)
|
||||
RNS.exit(0)
|
||||
|
||||
link.teardown()
|
||||
exit(0)
|
||||
time.sleep(0.1)
|
||||
RNS.exit(0)
|
||||
|
||||
|
||||
def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, no_compress=False):
|
||||
global current_resource, resource_done, link, speed, show_phy_rates, phy_got_total, phy_speed
|
||||
from tempfile import TemporaryFile
|
||||
def send(configdir, identitypath = None, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, no_compress=False):
|
||||
global current_resource, resource_done, link, speed, show_phy_rates, phy_got_total, phy_speed, identity
|
||||
targetloglevel = 3+verbosity-quietness
|
||||
show_phy_rates = phy_rates
|
||||
|
||||
@@ -599,47 +628,22 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
|
||||
|
||||
file_path = os.path.expanduser(file)
|
||||
if not os.path.isfile(file_path):
|
||||
print("File not found")
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
temp_file = TemporaryFile()
|
||||
real_file = open(file_path, "rb")
|
||||
filename_bytes = os.path.basename(file_path).encode("utf-8")
|
||||
filename_len = len(filename_bytes)
|
||||
|
||||
if filename_len > 0xFFFF:
|
||||
print("Filename exceeds max size, cannot send")
|
||||
exit(1)
|
||||
else:
|
||||
print("Preparing file...", end=es)
|
||||
|
||||
temp_file.write(filename_len.to_bytes(2, "big"))
|
||||
temp_file.write(filename_bytes)
|
||||
temp_file.write(real_file.read())
|
||||
temp_file.seek(0)
|
||||
metadata = {"name": os.path.basename(file_path).encode("utf-8") }
|
||||
|
||||
print(f"{erase_str}", end="")
|
||||
|
||||
reticulum = RNS.Reticulum(configdir=configdir, loglevel=targetloglevel)
|
||||
|
||||
identity_path = RNS.Reticulum.identitypath+"/"+APP_NAME
|
||||
if os.path.isfile(identity_path):
|
||||
identity = RNS.Identity.from_file(identity_path)
|
||||
if identity == None:
|
||||
RNS.log("Could not load identity for rncp. The identity file at \""+str(identity_path)+"\" may be corrupt or unreadable.", RNS.LOG_ERROR)
|
||||
exit(2)
|
||||
else:
|
||||
identity = None
|
||||
|
||||
if identity == None:
|
||||
RNS.log("No valid saved identity found, creating new...", RNS.LOG_INFO)
|
||||
identity = RNS.Identity()
|
||||
identity.to_file(identity_path)
|
||||
prepare_identity(identitypath)
|
||||
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
@@ -664,7 +668,7 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
print("Path not found")
|
||||
else:
|
||||
print(f"{erase_str}Path not found")
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
else:
|
||||
if silent:
|
||||
print("Establishing link with "+RNS.prettyhexrep(destination_hash))
|
||||
@@ -693,13 +697,13 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
print("Link establishment with "+RNS.prettyhexrep(destination_hash)+" timed out")
|
||||
else:
|
||||
print(f"{erase_str}Link establishment with "+RNS.prettyhexrep(destination_hash)+" timed out")
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
elif not RNS.Transport.has_path(destination_hash):
|
||||
if silent:
|
||||
print("No path found to "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print(f"{erase_str}No path found to "+RNS.prettyhexrep(destination_hash))
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
else:
|
||||
if silent:
|
||||
print("Advertising file resource...")
|
||||
@@ -708,9 +712,12 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
|
||||
link.identify(identity)
|
||||
auto_compress = True
|
||||
if no_compress:
|
||||
auto_compress = False
|
||||
resource = RNS.Resource(temp_file, link, callback = sender_progress, progress_callback = sender_progress, auto_compress = auto_compress)
|
||||
if no_compress: auto_compress = False
|
||||
try: resource = RNS.Resource(open(file_path, "rb"), link, metadata=metadata, callback = sender_progress, progress_callback = sender_progress, auto_compress = auto_compress)
|
||||
except Exception as e:
|
||||
print(f"Could not start transfer: {e}")
|
||||
RNS.exit(1)
|
||||
|
||||
current_resource = resource
|
||||
|
||||
while resource.status < RNS.Resource.TRANSFERRING:
|
||||
@@ -727,7 +734,7 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
print("File was not accepted by "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print(f"{erase_str}File was not accepted by "+RNS.prettyhexrep(destination_hash))
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
else:
|
||||
if silent:
|
||||
print("Transferring file...")
|
||||
@@ -773,7 +780,7 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
print("The transfer failed")
|
||||
else:
|
||||
print(f"{erase_str}The transfer failed")
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
else:
|
||||
if silent:
|
||||
print(str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
|
||||
@@ -781,9 +788,7 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
print("\n"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash))
|
||||
link.teardown()
|
||||
time.sleep(0.25)
|
||||
real_file.close()
|
||||
temp_file.close()
|
||||
exit(0)
|
||||
RNS.exit(0)
|
||||
|
||||
def main():
|
||||
try:
|
||||
@@ -800,10 +805,12 @@ def main():
|
||||
parser.add_argument("-f", '--fetch', action='store_true', default=False, help="fetch file from remote listener instead of sending")
|
||||
parser.add_argument("-j", "--jail", metavar="path", action="store", default=None, help="restrict fetch requests to specified path", type=str)
|
||||
parser.add_argument("-s", "--save", metavar="path", action="store", default=None, help="save received files in specified path", type=str)
|
||||
parser.add_argument('-O', '--overwrite', action='store_true', default=False, help="Allow overwriting received files, instead of adding postfix")
|
||||
parser.add_argument("-b", action='store', metavar="seconds", default=-1, help="announce interval, 0 to only announce at startup", type=int)
|
||||
parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="allow this identity (or add in ~/.rncp/allowed_identities)", type=str)
|
||||
parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept requests from anyone")
|
||||
parser.add_argument('-p', '--print-identity', action='store_true', default=False, help="print identity and destination info and exit")
|
||||
parser.add_argument('-i', metavar="identity", action='store', dest="identity", default=None, help="path to identity to use", type=str)
|
||||
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="sender timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
parser.add_argument('-P', '--phy-rates', action='store_true', default=False, help="display physical layer transfer rates")
|
||||
# parser.add_argument("--limit", action="store", metavar="files", type=float, help="maximum number of files to accept", default=None)
|
||||
@@ -814,22 +821,26 @@ def main():
|
||||
if args.listen or args.print_identity:
|
||||
listen(
|
||||
configdir = args.config,
|
||||
identitypath = args.identity,
|
||||
verbosity=args.verbose,
|
||||
quietness=args.quiet,
|
||||
allowed = args.allowed,
|
||||
fetch_allowed = args.allow_fetch,
|
||||
no_compress = args.no_compress,
|
||||
jail = args.jail,
|
||||
save = args.save,
|
||||
display_identity=args.print_identity,
|
||||
# limit=args.limit,
|
||||
disable_auth=args.no_auth,
|
||||
announce=args.b,
|
||||
allow_overwrite=args.overwrite,
|
||||
)
|
||||
|
||||
elif args.fetch:
|
||||
if args.destination != None and args.file != None:
|
||||
fetch(
|
||||
configdir = args.config,
|
||||
identitypath = args.identity,
|
||||
verbosity = args.verbose,
|
||||
quietness = args.quiet,
|
||||
destination = args.destination,
|
||||
@@ -838,6 +849,7 @@ def main():
|
||||
silent = args.silent,
|
||||
phy_rates = args.phy_rates,
|
||||
save = args.save,
|
||||
allow_overwrite=args.overwrite,
|
||||
)
|
||||
else:
|
||||
print("")
|
||||
@@ -847,6 +859,7 @@ def main():
|
||||
elif args.destination != None and args.file != None:
|
||||
send(
|
||||
configdir = args.config,
|
||||
identitypath = args.identity,
|
||||
verbosity = args.verbose,
|
||||
quietness = args.quiet,
|
||||
destination = args.destination,
|
||||
@@ -868,7 +881,7 @@ def main():
|
||||
resource.cancel()
|
||||
if link != None:
|
||||
link.teardown()
|
||||
exit()
|
||||
RNS.exit()
|
||||
|
||||
def size_str(num, suffix='B'):
|
||||
units = ['','K','M','G','T','P','E','Z']
|
||||
|
||||
+27
-15
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2023 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -11,8 +11,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -63,7 +71,7 @@ def main():
|
||||
# parser.add_argument("file", nargs="?", default=None, help="input file path", type=str)
|
||||
|
||||
parser.add_argument("--config", metavar="path", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("-i", "--identity", metavar="identity", action="store", default=None, help="hexadecimal Reticulum Destination hash or path to Identity file", type=str)
|
||||
parser.add_argument("-i", "--identity", metavar="identity", action="store", default=None, help="hexadecimal Reticulum identity or destination hash, or path to Identity file", type=str)
|
||||
parser.add_argument("-g", "--generate", metavar="file", action="store", default=None, help="generate a new Identity")
|
||||
parser.add_argument("-m", "--import", dest="import_str", metavar="identity_data", action="store", default=None, help="import Reticulum identity in hex, base32 or base64 format", type=str)
|
||||
parser.add_argument("-x", "--export", action="store_true", default=None, help="export identity to hex, base32 or base64 format")
|
||||
@@ -194,7 +202,7 @@ def main():
|
||||
else:
|
||||
try:
|
||||
identity.to_file(args.generate)
|
||||
RNS.log("New identity written to "+str(args.generate))
|
||||
RNS.log(f"New identity {identity} written to {args.generate}")
|
||||
exit(0)
|
||||
except Exception as e:
|
||||
RNS.log("An error ocurred while saving the generated Identity.", RNS.LOG_ERROR)
|
||||
@@ -205,29 +213,32 @@ def main():
|
||||
if len(identity_str) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2 and not os.path.isfile(identity_str):
|
||||
# Try recalling Identity from hex-encoded hash
|
||||
try:
|
||||
destination_hash = bytes.fromhex(identity_str)
|
||||
identity = RNS.Identity.recall(destination_hash)
|
||||
ident_hash = bytes.fromhex(identity_str)
|
||||
identity = RNS.Identity.recall(ident_hash) or RNS.Identity.recall(ident_hash, from_identity_hash=True)
|
||||
|
||||
if identity == None:
|
||||
if not args.request:
|
||||
RNS.log("Could not recall Identity for "+RNS.prettyhexrep(destination_hash)+".", RNS.LOG_ERROR)
|
||||
RNS.log("Could not recall Identity for "+RNS.prettyhexrep(ident_hash)+".", RNS.LOG_ERROR)
|
||||
RNS.log("You can query the network for unknown Identities with the -R option.", RNS.LOG_ERROR)
|
||||
exit(5)
|
||||
else:
|
||||
RNS.Transport.request_path(destination_hash)
|
||||
RNS.Transport.request_path(ident_hash)
|
||||
def spincheck():
|
||||
return RNS.Identity.recall(destination_hash) != None
|
||||
spin(spincheck, "Requesting unknown Identity for "+RNS.prettyhexrep(destination_hash), args.t)
|
||||
return RNS.Identity.recall(ident_hash) != None
|
||||
spin(spincheck, "Requesting unknown Identity for "+RNS.prettyhexrep(ident_hash), args.t)
|
||||
|
||||
if not spincheck():
|
||||
RNS.log("Identity request timed out", RNS.LOG_ERROR)
|
||||
exit(6)
|
||||
else:
|
||||
identity = RNS.Identity.recall(destination_hash)
|
||||
RNS.log("Received Identity "+str(identity)+" for destination "+RNS.prettyhexrep(destination_hash)+" from the network")
|
||||
identity = RNS.Identity.recall(ident_hash)
|
||||
RNS.log("Received Identity "+str(identity)+" for destination "+RNS.prettyhexrep(ident_hash)+" from the network")
|
||||
|
||||
else:
|
||||
RNS.log("Recalled Identity "+str(identity)+" for destination "+RNS.prettyhexrep(destination_hash))
|
||||
ident_str = str(identity)
|
||||
hash_str = RNS.prettyhexrep(ident_hash)
|
||||
if ident_str == hash_str: RNS.log(f"Recalled Identity {ident_str}")
|
||||
else: RNS.log(f"Recalled Identity {ident_str} for destination {hash_str}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
@@ -286,6 +297,7 @@ def main():
|
||||
destination = RNS.Destination(identity, RNS.Destination.IN, RNS.Destination.SINGLE, app_name, *aspects)
|
||||
RNS.log("Created destination "+str(destination))
|
||||
RNS.log("Announcing destination "+RNS.prettyhexrep(destination.hash))
|
||||
time.sleep(1.1)
|
||||
destination.announce()
|
||||
time.sleep(0.25)
|
||||
exit(0)
|
||||
|
||||
+12
-4
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2023 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -11,8 +11,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
+468
-84
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2018-2025 Mark Qvist
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -11,8 +11,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -41,9 +49,9 @@ import RNS
|
||||
RNS.logtimefmt = "%H:%M:%S"
|
||||
RNS.compact_log_fmt = True
|
||||
|
||||
program_version = "2.4.0"
|
||||
eth_addr = "0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73"
|
||||
btc_addr = "35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH"
|
||||
program_version = "2.5.0"
|
||||
eth_addr = "0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E"
|
||||
btc_addr = "bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6"
|
||||
xmr_addr = "87HcDx6jRSkMQ9nPRd5K9hGGpZLn2s7vWETjMaVM5KfV4TD36NcYa8J8WSxhTSvBzzFpqDwp2fg5GX2moZ7VAP9QMZCZGET"
|
||||
|
||||
rnode = None
|
||||
@@ -89,10 +97,17 @@ class KISS():
|
||||
CMD_BT_CTRL = 0x46
|
||||
CMD_BT_PIN = 0x62
|
||||
CMD_DIS_IA = 0x69
|
||||
CMD_WIFI_MODE = 0x6A
|
||||
CMD_WIFI_SSID = 0x6B
|
||||
CMD_WIFI_PSK = 0x6C
|
||||
CMD_WIFI_CHN = 0x6E
|
||||
CMD_WIFI_IP = 0x84
|
||||
CMD_WIFI_NM = 0x85
|
||||
CMD_BOARD = 0x47
|
||||
CMD_PLATFORM = 0x48
|
||||
CMD_MCU = 0x49
|
||||
CMD_FW_VERSION = 0x50
|
||||
CMD_CFG_READ = 0x6D
|
||||
CMD_ROM_READ = 0x51
|
||||
CMD_ROM_WRITE = 0x52
|
||||
CMD_ROM_WIPE = 0x59
|
||||
@@ -159,6 +174,7 @@ class ROM():
|
||||
MODEL_B9 = 0xB9
|
||||
MODEL_B4_TCXO = 0x04 # The TCXO model codes are only used here to select the correct firmware,
|
||||
MODEL_B9_TCXO = 0x09 # actual model codes in firmware is still 0xB4 and 0xB9.
|
||||
|
||||
PRODUCT_H32_V2 = 0xC0
|
||||
MODEL_C4 = 0xC4
|
||||
MODEL_C9 = 0xC9
|
||||
@@ -167,6 +183,9 @@ class ROM():
|
||||
MODEL_C5 = 0xC5
|
||||
MODEL_CA = 0xCA
|
||||
|
||||
PRODUCT_H32_V4 = 0xC3
|
||||
MODEL_C8 = 0xC8 # 868/915/923 MHz with PA
|
||||
|
||||
PRODUCT_TBEAM = 0xE0
|
||||
MODEL_E4 = 0xE4
|
||||
MODEL_E9 = 0xE9
|
||||
@@ -199,6 +218,11 @@ class ROM():
|
||||
MODEL_C6 = 0xC6 # Heltec Mesh Node T114, 470-510 MHz (HT-n5262-LF)
|
||||
MODEL_C7 = 0xC7 # Heltec Mesh Node T114, 863-928 MHz (HT-n5262-HF)
|
||||
|
||||
PRODUCT_XIAO_S3 = 0xEB
|
||||
BOARD_XIAO_S3 = 0x3E
|
||||
MODEL_DE = 0xDE # Xiao ESP32S3 with Wio-SX1262 module, 433 MHz
|
||||
MODEL_DD = 0xDD # Xiao ESP32S3 with Wio-SX1262 module, 868 MHz
|
||||
|
||||
PRODUCT_HMBRW = 0xF0
|
||||
MODEL_FF = 0xFF
|
||||
MODEL_FE = 0xFE
|
||||
@@ -228,6 +252,12 @@ class ROM():
|
||||
ADDR_CONF_PINT = 0xB6
|
||||
ADDR_CONF_BSET = 0xB7
|
||||
ADDR_CONF_DIA = 0xB9
|
||||
ADDR_CONF_WIFI = 0xBA
|
||||
ADDR_CONF_WCHN = 0xBB
|
||||
ADDR_CONF_SSID = 0x00
|
||||
ADDR_CONF_PSK = 0x21
|
||||
ADDR_CONF_IP = 0x42
|
||||
ADDR_CONF_NM = 0x46
|
||||
|
||||
INFO_LOCK_BYTE = 0x73
|
||||
CONF_OK_BYTE = 0x73
|
||||
@@ -257,10 +287,12 @@ products = {
|
||||
ROM.PRODUCT_T32_21: "LilyGO LoRa32 v2.1",
|
||||
ROM.PRODUCT_H32_V2: "Heltec LoRa32 v2",
|
||||
ROM.PRODUCT_H32_V3: "Heltec LoRa32 v3",
|
||||
ROM.PRODUCT_H32_V4: "Heltec LoRa32 v4",
|
||||
ROM.PRODUCT_TECHO: "LilyGO T-Echo",
|
||||
ROM.PRODUCT_RAK4631: "RAK4631",
|
||||
ROM.PRODUCT_OPENCOM_XL: "openCom XL",
|
||||
ROM.PRODUCT_HELTEC_T114: "Heltec Mesh Node T114",
|
||||
ROM.PRODUCT_XIAO_S3: "Seeed XIAO ESP32S3 Wio-SX1262",
|
||||
}
|
||||
|
||||
platforms = {
|
||||
@@ -300,6 +332,7 @@ models = {
|
||||
0xC9: [850000000, 950000000, 17, "850 - 950 MHz", "rnode_firmware_heltec32v2.zip", "SX1276"],
|
||||
0xC5: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_heltec32v3.zip", "SX1268"],
|
||||
0xCA: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_heltec32v3.zip", "SX1262"],
|
||||
0xC8: [860000000, 930000000, 28, "850 - 950 MHz", "rnode_firmware_heltec32v4pa.zip", "SX1262"],
|
||||
0xC6: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_heltec_t114.zip", "SX1268"],
|
||||
0xC7: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_heltec_t114.zip", "SX1262"],
|
||||
0xE4: [420000000, 520000000, 17, "420 - 520 MHz", "rnode_firmware_tbeam.zip", "SX1278"],
|
||||
@@ -317,6 +350,8 @@ models = {
|
||||
0x16: [779000000, 928000000, 22, "430 - 510 Mhz", "rnode_firmware_techo.zip", "SX1262"],
|
||||
0x17: [779000000, 928000000, 22, "779 - 928 Mhz", "rnode_firmware_techo.zip", "SX1262"],
|
||||
0x21: [820000000, 960000000, 22, "820 - 960 MHz", "rnode_firmware_opencom_xl.zip", "SX1262 + SX1280"],
|
||||
0xDE: [420000000, 520000000, 22, "420 - 520 MHz", "rnode_firmware_xiao_esp32s3.zip", "SX1262"],
|
||||
0xDD: [850000000, 950000000, 22, "850 - 950 MHz", "rnode_firmware_xiao_esp32s3.zip", "SX1262"],
|
||||
0xFE: [100000000, 1100000000, 17, "(Band capabilities unknown)", None, "Unknown"],
|
||||
0xFF: [100000000, 1100000000, 14, "(Band capabilities unknown)", None, "Unknown"],
|
||||
}
|
||||
@@ -380,6 +415,7 @@ class RNode():
|
||||
self.platform = None
|
||||
self.mcu = None
|
||||
self.eeprom = None
|
||||
self.cfg_sector = None
|
||||
self.major_version = None
|
||||
self.minor_version = None
|
||||
self.version = None
|
||||
@@ -439,12 +475,17 @@ class RNode():
|
||||
in_frame = False
|
||||
data_buffer = b""
|
||||
command_buffer = b""
|
||||
elif (in_frame and byte == KISS.FEND and command == KISS.CMD_CFG_READ):
|
||||
self.cfg_sector = data_buffer
|
||||
in_frame = False
|
||||
data_buffer = b""
|
||||
command_buffer = b""
|
||||
elif (byte == KISS.FEND):
|
||||
in_frame = True
|
||||
command = KISS.CMD_UNKNOWN
|
||||
data_buffer = b""
|
||||
command_buffer = b""
|
||||
elif (in_frame and len(data_buffer) < 512):
|
||||
elif (in_frame and len(data_buffer) < 1024):
|
||||
if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
|
||||
command = byte
|
||||
elif (command == KISS.CMD_ROM_READ):
|
||||
@@ -458,6 +499,17 @@ class RNode():
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
elif (command == KISS.CMD_CFG_READ):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
else:
|
||||
if (escape):
|
||||
if (byte == KISS.TFEND):
|
||||
byte = KISS.FEND
|
||||
if (byte == KISS.TFESC):
|
||||
byte = KISS.FESC
|
||||
escape = False
|
||||
data_buffer = data_buffer+bytes([byte])
|
||||
elif (command == KISS.CMD_DATA):
|
||||
if (byte == KISS.FESC):
|
||||
escape = True
|
||||
@@ -766,6 +818,92 @@ class RNode():
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while sending firmware update command to device")
|
||||
|
||||
def set_wifi_mode(self, mode):
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_MODE, mode])+bytes([KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while sending wifi mode command to device")
|
||||
|
||||
def set_wifi_channel(self, channel):
|
||||
try: ch = int(channel)
|
||||
except: raise ValueError("Invalid WiFi channel")
|
||||
if ch < 1 or ch > 14: raise ValueError("Invalid WiFi channel")
|
||||
ch_data = bytes([ch])
|
||||
data = KISS.escape(ch_data)
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_CHN])+data+bytes([KISS.FEND])
|
||||
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while sending wifi channel to device")
|
||||
|
||||
def set_wifi_ip(self, ip):
|
||||
if ip == None: ip_data = bytes([0x00, 0x00, 0x00, 0x00])
|
||||
else:
|
||||
ip_data = b""
|
||||
if not type(ip) == str: raise TypeError("Invalid IP address")
|
||||
octets = ip.split(".")
|
||||
if not len(octets) == 4: raise ValueError("Invalid IP address length")
|
||||
try:
|
||||
for i in range(0, 4):
|
||||
octet = int(octets[i])
|
||||
if octet < 0 or octet > 255: raise ValueError("Invalid IP octet value")
|
||||
else: ip_data += bytes([octet])
|
||||
except Exception as e:
|
||||
raise ValueError(f"Could not decode IP address octet: {e}")
|
||||
|
||||
data = KISS.escape(ip_data)
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_IP])+data+bytes([KISS.FEND])
|
||||
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command): raise IOError("An IO error occurred while sending wifi IP address to device")
|
||||
|
||||
def set_wifi_nm(self, nm):
|
||||
if nm == None: nm_data = bytes([0x00, 0x00, 0x00, 0x00])
|
||||
else:
|
||||
nm_data = b""
|
||||
if not type(nm) == str: raise TypeError("Invalid IP address")
|
||||
octets = nm.split(".")
|
||||
if not len(octets) == 4: raise ValueError("Invalid IP address length")
|
||||
try:
|
||||
for i in range(0, 4):
|
||||
octet = int(octets[i])
|
||||
if octet < 0 or octet > 255: raise ValueError("Invalid IP octet value")
|
||||
else: nm_data += bytes([octet])
|
||||
except Exception as e:
|
||||
raise ValueError(f"Could not decode IP address octet: {e}")
|
||||
|
||||
data = KISS.escape(nm_data)
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_NM])+data+bytes([KISS.FEND])
|
||||
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command): raise IOError("An IO error occurred while sending wifi netmask to device")
|
||||
|
||||
def set_wifi_ssid(self, ssid):
|
||||
if ssid == None: data = bytes([0x00])
|
||||
else:
|
||||
ssid_data = ssid.encode("utf-8")+bytes([0x00])
|
||||
if len(ssid_data) < 0 or len(ssid_data) > 33: raise ValueError("Invalid SSID length")
|
||||
data = KISS.escape(ssid_data)
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_SSID])+data+bytes([KISS.FEND])
|
||||
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while sending wifi SSID to device")
|
||||
|
||||
def set_wifi_psk(self, psk):
|
||||
if psk == None: data = bytes([0x00])
|
||||
else:
|
||||
psk_data = psk.encode("utf-8")+bytes([0x00])
|
||||
if len(psk_data) < 8 or len(psk_data) > 33: raise ValueError("Invalid psk length")
|
||||
data = KISS.escape(psk_data)
|
||||
|
||||
kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_WIFI_PSK])+data+bytes([KISS.FEND])
|
||||
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while sending wifi SSID to device")
|
||||
|
||||
def initRadio(self):
|
||||
self.setFrequency()
|
||||
self.setBandwidth()
|
||||
@@ -872,7 +1010,7 @@ class RNode():
|
||||
kiss_command = bytes([KISS.FEND, KISS.CMD_ROM_READ, 0x00, KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while configuring radio state")
|
||||
raise IOError("An IO error occurred while downloading EEPROM")
|
||||
|
||||
sleep(0.6)
|
||||
if self.eeprom == None:
|
||||
@@ -881,6 +1019,15 @@ class RNode():
|
||||
else:
|
||||
self.parse_eeprom()
|
||||
|
||||
def download_cfg_sector(self):
|
||||
self.cfg_sector = None
|
||||
kiss_command = bytes([KISS.FEND, KISS.CMD_CFG_READ, 0x00, KISS.FEND])
|
||||
written = self.serial.write(kiss_command)
|
||||
if written != len(kiss_command):
|
||||
raise IOError("An IO error occurred while downloading config sector")
|
||||
|
||||
sleep(0.6)
|
||||
|
||||
def parse_eeprom(self):
|
||||
global squashvw;
|
||||
try:
|
||||
@@ -1038,8 +1185,8 @@ class RNode():
|
||||
print(" Always use a firmware downloaded as binaries or compiled from source")
|
||||
print(" from one of the following locations:")
|
||||
print(" ")
|
||||
print(" https://unsigned.io/rnode")
|
||||
print(" https://github.com/markqvist/rnode_firmware")
|
||||
print(" https://github.com/liberatedsystems/RNode_Firmware_CE")
|
||||
print(" ")
|
||||
print(" You can reflash and bootstrap this device to a verifiable state")
|
||||
print(" by using this utility. It is recommended to do so NOW!")
|
||||
@@ -1081,7 +1228,7 @@ class RNode():
|
||||
|
||||
selected_version = None
|
||||
selected_hash = None
|
||||
firmware_version_url = "https://unsigned.io/firmware/latest/?v="+program_version+"&variant="
|
||||
firmware_version_url = "https://github.com/markqvist/rnode_firmware/releases/latest/download/release.json"
|
||||
fallback_firmware_version_url = "https://github.com/markqvist/rnode_firmware/releases/latest/download/release.json"
|
||||
def ensure_firmware_file(fw_filename):
|
||||
global selected_version, selected_hash, upd_nocheck
|
||||
@@ -1122,9 +1269,15 @@ def ensure_firmware_file(fw_filename):
|
||||
try:
|
||||
# if custom firmware url, download latest release
|
||||
if selected_version == None and fw_url == None:
|
||||
version_url = firmware_version_url+fw_filename
|
||||
RNS.log("Retrieving latest version info from "+version_url)
|
||||
urlretrieve(firmware_version_url+fw_filename, UPD_DIR+"/"+fw_filename+".version.latest")
|
||||
urlretrieve(firmware_version_url, UPD_DIR+"/release_info.json")
|
||||
import json
|
||||
with open(UPD_DIR+"/release_info.json", "rb") as rif:
|
||||
rdat = json.loads(rif.read())
|
||||
variant = rdat[fw_filename]
|
||||
with open(UPD_DIR+"/"+fw_filename+".version.latest", "wb") as verf:
|
||||
inf_str = str(variant["version"])+" "+str(variant["hash"])
|
||||
verf.write(inf_str.encode("utf-8"))
|
||||
|
||||
else:
|
||||
if fw_url != None:
|
||||
if selected_version == None:
|
||||
@@ -1343,6 +1496,14 @@ def main():
|
||||
parser.add_argument("-B", "--bluetooth-off", action="store_true", help="Turn device bluetooth off")
|
||||
parser.add_argument("-p", "--bluetooth-pair", action="store_true", help="Put device into bluetooth pairing mode")
|
||||
|
||||
parser.add_argument("-w", "--wifi", action="store", metavar="mode", default=None, help="Set WiFi mode (OFF, AP or STATION)")
|
||||
parser.add_argument("--channel", action="store", metavar="channel", default=None, help="Set WiFi channel")
|
||||
parser.add_argument("--ssid", action="store", metavar="ssid", default=None, help="Set WiFi SSID (NONE to delete)")
|
||||
parser.add_argument("--psk", action="store", metavar="psk", default=None, help="Set WiFi PSK (NONE to delete)")
|
||||
parser.add_argument("--show-psk", action="store_true", default=False, help="Display stored WiFi PSK")
|
||||
parser.add_argument("--ip", action="store", metavar="ip", default=None, help="Set static WiFi IP address (NONE for DHCP)")
|
||||
parser.add_argument("--nm", action="store", metavar="nm", default=None, help="Set static WiFi network mask (NONE for DHCP)")
|
||||
|
||||
parser.add_argument("-D", "--display", action="store", metavar="i", type=int, default=None, help="Set display intensity (0-255)")
|
||||
parser.add_argument("-t", "--timeout", action="store", metavar="s", type=int, default=None, help="Set display timeout in seconds, 0 to disable")
|
||||
parser.add_argument("-R", "--rotation", action="store", metavar="rotation", type=int, default=None, help="Set display rotation, valid values are 0 through 3")
|
||||
@@ -1387,13 +1548,14 @@ def main():
|
||||
args = parser.parse_args()
|
||||
|
||||
def print_donation_block():
|
||||
print(" Ethereum : "+eth_addr)
|
||||
print(" Bitcoin : "+btc_addr)
|
||||
print(" Monero : "+xmr_addr)
|
||||
print(" Ko-Fi : https://ko-fi.com/markqvist")
|
||||
print(" Ethereum : "+eth_addr)
|
||||
print(" Bitcoin : "+btc_addr)
|
||||
print(" Monero : "+xmr_addr)
|
||||
print(" Ko-Fi : https://ko-fi.com/markqvist")
|
||||
print(" LiberaPay : https://liberapay.com/reticulum")
|
||||
print("")
|
||||
print(" Info : https://unsigned.io/")
|
||||
print(" Code : https://github.com/markqvist")
|
||||
print(" Info : https://reticulum.network")
|
||||
print(" Code : https://github.com/markqvist")
|
||||
|
||||
if args.version:
|
||||
print("rnodeconf "+program_version)
|
||||
@@ -1700,7 +1862,7 @@ def main():
|
||||
print(" '")
|
||||
print("[1] A specific kind of RNode")
|
||||
print("")
|
||||
print(" | Select this option if you have put toghether an RNode")
|
||||
print(" | Select this option if you have put together an RNode")
|
||||
print(" \\ / of your own design, or if you are prototyping one.")
|
||||
print(" '")
|
||||
print("[2] Homebrew RNode")
|
||||
@@ -1714,12 +1876,14 @@ def main():
|
||||
print("[6] LilyGO T-Beam")
|
||||
print("[7] Heltec LoRa32 v2")
|
||||
print("[8] Heltec LoRa32 v3")
|
||||
print("[9] LilyGO LoRa T3S3")
|
||||
print("[10] RAK4631")
|
||||
print("[11] LilyGo T-Echo")
|
||||
print("[12] LilyGO T-Beam Supreme")
|
||||
print("[13] LilyGO T-Deck")
|
||||
print("[14] Heltec T114")
|
||||
print("[9] Heltec LoRa32 v4")
|
||||
print("[10] LilyGO LoRa T3S3")
|
||||
print("[11] RAK4631")
|
||||
print("[12] LilyGo T-Echo")
|
||||
print("[13] LilyGO T-Beam Supreme")
|
||||
print("[14] LilyGO T-Deck")
|
||||
print("[15] Heltec T114")
|
||||
print("[16] Seeed XIAO ESP32S3 Wio-SX1262")
|
||||
print("")
|
||||
print("---------------------------------------------------------------------------")
|
||||
print("\nEnter the number that matches your device type:\n? ", end="")
|
||||
@@ -1728,7 +1892,7 @@ def main():
|
||||
try:
|
||||
c_dev = int(input())
|
||||
c_mod = False
|
||||
if c_dev < 1 or c_dev > 14:
|
||||
if c_dev < 1 or c_dev > 16:
|
||||
raise ValueError()
|
||||
elif c_dev == 1:
|
||||
selected_product = ROM.PRODUCT_RNODE
|
||||
@@ -1745,7 +1909,7 @@ def main():
|
||||
print("")
|
||||
print("Important! Using RNode firmware on homebrew devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
@@ -1761,11 +1925,11 @@ def main():
|
||||
print("")
|
||||
print("Important! Using RNode firmware on T-Beam devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
elif c_dev == 12:
|
||||
elif c_dev == 13:
|
||||
selected_product = ROM.PRODUCT_TBEAM_S_V1
|
||||
clear()
|
||||
print("")
|
||||
@@ -1777,11 +1941,11 @@ def main():
|
||||
print("")
|
||||
print("Important! Using RNode firmware on T-Beam devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
elif c_dev == 13:
|
||||
elif c_dev == 14:
|
||||
selected_product = ROM.PRODUCT_TDECK
|
||||
clear()
|
||||
print("")
|
||||
@@ -1793,7 +1957,7 @@ def main():
|
||||
print("")
|
||||
print("Important! Using RNode firmware on T-Beam devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
@@ -1806,7 +1970,7 @@ def main():
|
||||
print("")
|
||||
print("Important! Using RNode firmware on LoRa32 devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
@@ -1819,7 +1983,7 @@ def main():
|
||||
print("")
|
||||
print("Important! Using RNode firmware on LoRa32 devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it.")
|
||||
print("")
|
||||
print("Please Note! This device is known to have a faulty battery charging circuit,")
|
||||
@@ -1838,7 +2002,7 @@ def main():
|
||||
print("")
|
||||
print("Important! Using RNode firmware on LoRa32 devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
@@ -1852,11 +2016,11 @@ def main():
|
||||
print("Important! Using RNode firmware on Heltec devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
elif c_dev == 9:
|
||||
elif c_dev == 10:
|
||||
selected_product = ROM.PRODUCT_RNODE
|
||||
c_mod = True
|
||||
clear()
|
||||
@@ -1867,7 +2031,7 @@ def main():
|
||||
print("Important! Using RNode firmware on T3S3 devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
@@ -1881,11 +2045,25 @@ def main():
|
||||
print("Important! Using RNode firmware on Heltec devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
elif c_dev == 10:
|
||||
elif c_dev == 9:
|
||||
selected_product = ROM.PRODUCT_H32_V4
|
||||
clear()
|
||||
print("")
|
||||
print("---------------------------------------------------------------------------")
|
||||
print(" Heltec LoRa32 v4 RNode Installer")
|
||||
print("")
|
||||
print("Important! Using RNode firmware on Heltec devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
elif c_dev == 11:
|
||||
selected_product = ROM.PRODUCT_RAK4631
|
||||
clear()
|
||||
print("")
|
||||
@@ -1894,11 +2072,11 @@ def main():
|
||||
print("")
|
||||
print("Important! Using RNode firmware on RAKwireless devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
elif c_dev == 11:
|
||||
elif c_dev == 12:
|
||||
selected_product = ROM.PRODUCT_TECHO
|
||||
clear()
|
||||
print("")
|
||||
@@ -1907,11 +2085,11 @@ def main():
|
||||
print("")
|
||||
print("Important! Using RNode firmware on LilyGo T-Echo devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
elif c_dev == 14:
|
||||
elif c_dev == 15:
|
||||
selected_product = ROM.PRODUCT_HELTEC_T114
|
||||
clear()
|
||||
print("")
|
||||
@@ -1920,7 +2098,20 @@ def main():
|
||||
print("")
|
||||
print("Important! Using RNode firmware on Heltec T114 devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesey to those")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
elif c_dev == 16:
|
||||
selected_product = ROM.PRODUCT_XIAO_S3
|
||||
clear()
|
||||
print("")
|
||||
print("---------------------------------------------------------------------------")
|
||||
print(" SeeedStudio XIAO esp32s3 wio RNode Installer")
|
||||
print("")
|
||||
print("Important! Using RNode firmware on SeeedStudio XIAO/wio devices should currently be")
|
||||
print("considered experimental. It is not intended for production or critical use.")
|
||||
print("The currently supplied firmware is provided AS-IS as a courtesy to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
@@ -2234,6 +2425,7 @@ def main():
|
||||
print("[2] 868 MHz")
|
||||
print("[3] 915 MHz")
|
||||
print("[4] 923 MHz")
|
||||
print("\n? ", end="")
|
||||
try:
|
||||
c_model = int(input())
|
||||
if c_model < 1 or c_model > 4:
|
||||
@@ -2248,6 +2440,24 @@ def main():
|
||||
print("That band does not exist, exiting now.")
|
||||
exit()
|
||||
|
||||
elif selected_product == ROM.PRODUCT_H32_V4:
|
||||
selected_mcu = ROM.MCU_ESP32
|
||||
print("\nWhat band is this Heltec LoRa32 V4 for?\n")
|
||||
print("[1] 868 MHz (28 dBm output)")
|
||||
print("[2] 915 MHz (28 dBm output)")
|
||||
print("[3] 923 MHz (28 dBm output)")
|
||||
print("\n? ", end="")
|
||||
try:
|
||||
c_model = int(input())
|
||||
if c_model < 1 or c_model > 3:
|
||||
raise ValueError()
|
||||
else:
|
||||
selected_model = ROM.MODEL_C8
|
||||
selected_platform = ROM.PLATFORM_ESP32
|
||||
except Exception as e:
|
||||
print("That band does not exist, exiting now.")
|
||||
exit()
|
||||
|
||||
elif selected_product == ROM.PRODUCT_HELTEC_T114:
|
||||
selected_mcu = ROM.MCU_NRF52
|
||||
print("\nWhat band is this Heltec T114 for?\n")
|
||||
@@ -2255,6 +2465,7 @@ def main():
|
||||
print("[2] 868 MHz")
|
||||
print("[3] 915 MHz")
|
||||
print("[4] 923 MHz")
|
||||
print("\n? ", end="")
|
||||
try:
|
||||
c_model = int(input())
|
||||
if c_model < 1 or c_model > 4:
|
||||
@@ -2268,7 +2479,27 @@ def main():
|
||||
except Exception as e:
|
||||
print("That band does not exist, exiting now.")
|
||||
exit()
|
||||
|
||||
|
||||
elif selected_product == ROM.PRODUCT_XIAO_S3:
|
||||
selected_mcu = ROM.MCU_ESP32
|
||||
print("\nWhat band is this XIAO esp32s3 wio module for?\n")
|
||||
print("[1] 433 MHz")
|
||||
print("[2] 868 MHz")
|
||||
print("\n? ", end="")
|
||||
try:
|
||||
c_model = int(input())
|
||||
if c_model < 1 or c_model > 2:
|
||||
raise ValueError()
|
||||
elif c_model == 1:
|
||||
selected_model = ROM.MODEL_DE
|
||||
selected_platform = ROM.PLATFORM_ESP32
|
||||
elif c_model == 2:
|
||||
selected_model = ROM.MODEL_DD
|
||||
selected_platform = ROM.PLATFORM_ESP32
|
||||
except Exception as e:
|
||||
print("That band does not exist, exiting now.")
|
||||
exit()
|
||||
|
||||
elif selected_product == ROM.PRODUCT_RAK4631:
|
||||
selected_mcu = ROM.MCU_NRF52
|
||||
print("\nWhat band is this RAK4631 for?\n")
|
||||
@@ -2826,6 +3057,24 @@ def main():
|
||||
"0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
|
||||
"0x8000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v3.partitions",
|
||||
]
|
||||
elif fw_filename == "rnode_firmware_heltec32v4pa.zip":
|
||||
return [
|
||||
sys.executable, flasher,
|
||||
"--chip", "esp32-s3",
|
||||
"--port", args.port,
|
||||
"--baud", args.baud_flash,
|
||||
"--before", "default_reset",
|
||||
"--after", "hard_reset",
|
||||
"write_flash", "-z",
|
||||
"--flash_mode", "dio",
|
||||
"--flash_freq", "80m",
|
||||
"--flash_size", "16MB",
|
||||
"0xe000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.boot_app0",
|
||||
"0x0", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.bootloader",
|
||||
"0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.bin",
|
||||
"0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
|
||||
"0x8000", UPD_DIR+"/"+selected_version+"/rnode_firmware_heltec32v4pa.partitions",
|
||||
]
|
||||
elif fw_filename == "rnode_firmware_featheresp32.zip":
|
||||
if numeric_version >= 1.55:
|
||||
return [
|
||||
@@ -3060,6 +3309,24 @@ def main():
|
||||
"0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
|
||||
"0x8000", UPD_DIR+"/"+selected_version+"/rnode_firmware_tdeck.partitions",
|
||||
]
|
||||
elif fw_filename == "rnode_firmware_xiao_esp32s3.zip":
|
||||
return [
|
||||
sys.executable, flasher,
|
||||
"--chip", "esp32s3",
|
||||
"--port", args.port,
|
||||
"--baud", args.baud_flash,
|
||||
"--before", "default_reset",
|
||||
"--after", "hard_reset",
|
||||
"write_flash", "-z",
|
||||
"--flash_mode", "dio",
|
||||
"--flash_freq", "80m",
|
||||
"--flash_size", "8MB",
|
||||
"0xe000", UPD_DIR+"/"+selected_version+"/rnode_firmware_xiao_esp32s3.boot_app0",
|
||||
"0x0", UPD_DIR+"/"+selected_version+"/rnode_firmware_xiao_esp32s3.bootloader",
|
||||
"0x10000", UPD_DIR+"/"+selected_version+"/rnode_firmware_xiao_esp32s3.bin",
|
||||
"0x210000",UPD_DIR+"/"+selected_version+"/console_image.bin",
|
||||
"0x8000", UPD_DIR+"/"+selected_version+"/rnode_firmware_xiao_esp32s3.partitions",
|
||||
]
|
||||
elif fw_filename == "extracted_rnode_firmware.zip":
|
||||
return [
|
||||
sys.executable, flasher,
|
||||
@@ -3431,17 +3698,14 @@ def main():
|
||||
graceful_exit()
|
||||
|
||||
if args.config:
|
||||
rnode.download_cfg_sector()
|
||||
eeprom_reserved = 200
|
||||
if rnode.platform == ROM.PLATFORM_ESP32:
|
||||
eeprom_size = 296
|
||||
elif rnode.platform == ROM.PLATFORM_NRF52:
|
||||
eeprom_size = 296
|
||||
else:
|
||||
eeprom_size = 4096
|
||||
if rnode.platform == ROM.PLATFORM_ESP32: eeprom_size = 296
|
||||
elif rnode.platform == ROM.PLATFORM_NRF52: eeprom_size = 296
|
||||
else: eeprom_size = 4096
|
||||
|
||||
eeprom_offset = eeprom_size-eeprom_reserved
|
||||
def ea(a):
|
||||
return a+eeprom_offset
|
||||
def ea(a): return a+eeprom_offset
|
||||
ec_bt = rnode.eeprom[ROM.ADDR_CONF_BT]
|
||||
ec_dint = rnode.eeprom[ROM.ADDR_CONF_DINT]
|
||||
ec_dadr = rnode.eeprom[ROM.ADDR_CONF_DADR]
|
||||
@@ -3451,40 +3715,89 @@ def main():
|
||||
ec_pint = rnode.eeprom[ROM.ADDR_CONF_PINT]
|
||||
ec_bset = rnode.eeprom[ROM.ADDR_CONF_BSET]
|
||||
ec_dia = rnode.eeprom[ROM.ADDR_CONF_DIA]
|
||||
ec_wifi = rnode.eeprom[ROM.ADDR_CONF_WIFI]
|
||||
ec_wchn = rnode.eeprom[ROM.ADDR_CONF_WCHN]
|
||||
ec_ssid = None
|
||||
ec_psk = None
|
||||
ec_ip = None
|
||||
ec_nm = None
|
||||
|
||||
if ec_wchn < 1 or ec_wchn > 14: ec_wchn = 1
|
||||
if rnode.cfg_sector:
|
||||
ssid_bytes = b""
|
||||
for i in range(0, 32):
|
||||
byte = rnode.cfg_sector[ROM.ADDR_CONF_SSID+i]
|
||||
if byte == 0xFF: byte = 0x00
|
||||
if byte == 0x00: break
|
||||
else: ssid_bytes += bytes([byte])
|
||||
|
||||
try: ec_ssid = ssid_bytes.decode("utf-8")
|
||||
except Exception as e: print(f"Error: Could not decode WiFi SSID read from device")
|
||||
|
||||
psk_bytes = b""
|
||||
for i in range(0, 32):
|
||||
byte = rnode.cfg_sector[ROM.ADDR_CONF_PSK+i]
|
||||
if byte == 0xFF: byte = 0x00
|
||||
if byte == 0x00: break
|
||||
else: psk_bytes += bytes([byte])
|
||||
|
||||
ip_bytes = b""
|
||||
for i in range(0, 4):
|
||||
byte = rnode.cfg_sector[ROM.ADDR_CONF_IP+i]
|
||||
ip_bytes += bytes([byte])
|
||||
if len(ip_bytes) == 4: ec_ip = f"{int(ip_bytes[0])}.{int(ip_bytes[1])}.{int(ip_bytes[2])}.{int(ip_bytes[3])}"
|
||||
if ec_ip == "255.255.255.255" or ec_ip == "0.0.0.0": ec_ip = None
|
||||
|
||||
nm_bytes = b""
|
||||
for i in range(0, 4):
|
||||
byte = rnode.cfg_sector[ROM.ADDR_CONF_NM+i]
|
||||
nm_bytes += bytes([byte])
|
||||
if len(nm_bytes) == 4: ec_nm = f"{int(nm_bytes[0])}.{int(nm_bytes[1])}.{int(nm_bytes[2])}.{int(nm_bytes[3])}"
|
||||
if ec_nm == "255.255.255.255" or ec_nm == "0.0.0.0": ec_nm = None
|
||||
|
||||
if ec_wifi == 0x02:
|
||||
ec_ip = "10.0.0.1"
|
||||
ec_nm = "255.255.255.0"
|
||||
|
||||
try: ec_psk = psk_bytes.decode("utf-8")
|
||||
except Exception as e: print(f"Error: Could not decode WiFi PSK read from device")
|
||||
if not args.show_psk and ec_psk: ec_psk = "*"*len(ec_psk)
|
||||
|
||||
print("\nDevice configuration:")
|
||||
if ec_bt == 0x73:
|
||||
print(f" Bluetooth : Enabled")
|
||||
else:
|
||||
print(f" Bluetooth : Disabled")
|
||||
if ec_dia == 0x00:
|
||||
print(f" Interference avoidance : Enabled")
|
||||
else:
|
||||
print(f" Interference avoidance : Disabled")
|
||||
if ec_bt == 0x73: print(f" Bluetooth : Enabled")
|
||||
else: print(f" Bluetooth : Disabled")
|
||||
if ec_wifi == 0x01: print(f" WiFi : Enabled (Station)")
|
||||
if ec_wifi == 0x02: print(f" WiFi : Enabled (AP)")
|
||||
else: print(f" WiFi : Disabled")
|
||||
if ec_wifi == 0x01 or ec_wifi == 0x02:
|
||||
if not ec_wchn: print(f" Channel : Unknown")
|
||||
else: print(f" Channel : {ec_wchn}")
|
||||
if not ec_ssid: print(f" SSID : Not set")
|
||||
else: print(f" SSID : {ec_ssid}")
|
||||
if not ec_psk: print(f" PSK : Not set")
|
||||
else: print(f" PSK : {ec_psk}")
|
||||
if not ec_ip: print(f" IP Address : DHCP")
|
||||
else: print(f" IP Address : {ec_ip}")
|
||||
if ec_ip and ec_nm: print(f" Network Mask : {ec_nm}")
|
||||
if ec_dia == 0x00: print(f" Interference avoidance : Enabled")
|
||||
else: print(f" Interference avoidance : Disabled")
|
||||
print( f" Display brightness : {ec_dint}")
|
||||
if ec_dadr == 0xFF:
|
||||
print(f" Display address : Default")
|
||||
else:
|
||||
print(f" Display address : {RNS.hexrep(ec_dadr, delimit=False)}")
|
||||
if ec_bset == 0x73 and ec_dblk != 0x00:
|
||||
print(f" Display blanking : {ec_dblk}s")
|
||||
else:
|
||||
print(f" Display blanking : Disabled")
|
||||
if ec_dadr == 0xFF: print(f" Display address : Default")
|
||||
else: print(f" Display address : {RNS.hexrep(ec_dadr, delimit=False)}")
|
||||
if ec_bset == 0x73 and ec_dblk != 0x00: print(f" Display blanking : {ec_dblk}s")
|
||||
else: print(f" Display blanking : Disabled")
|
||||
if ec_drot != 0xFF:
|
||||
if ec_drot == 0x00:
|
||||
rstr = "Landscape"
|
||||
if ec_drot == 0x01:
|
||||
rstr = "Portrait"
|
||||
if ec_drot == 0x02:
|
||||
rstr = "Landscape 180"
|
||||
if ec_drot == 0x03:
|
||||
rstr = "Portrait 180"
|
||||
if ec_drot == 0x00: rstr = "Landscape"
|
||||
if ec_drot == 0x01: rstr = "Portrait"
|
||||
if ec_drot == 0x02: rstr = "Landscape 180"
|
||||
if ec_drot == 0x03: rstr = "Portrait 180"
|
||||
print(f" Display rotation : {rstr}")
|
||||
else:
|
||||
print(f" Display rotation : Default")
|
||||
if ec_pset == 0x73:
|
||||
print(f" Neopixel Intensity : {ec_pint}")
|
||||
if ec_pset == 0x73: print(f" Neopixel Intensity : {ec_pint}")
|
||||
print("")
|
||||
|
||||
rnode.leave()
|
||||
graceful_exit()
|
||||
|
||||
if args.eeprom_dump:
|
||||
@@ -3595,6 +3908,77 @@ def main():
|
||||
input()
|
||||
rnode.leave()
|
||||
|
||||
if args.channel:
|
||||
try:
|
||||
RNS.log(f"Setting WiFi channel to {args.channel}")
|
||||
rnode.set_wifi_channel(args.channel)
|
||||
except Exception as e:
|
||||
print(f"Could not set WiFi channel: {e}")
|
||||
graceful_exit()
|
||||
|
||||
if args.ssid:
|
||||
try:
|
||||
if args.ssid.lower() == "none":
|
||||
ssid_str = None
|
||||
RNS.log(f"Deleting WiFi SSID")
|
||||
else:
|
||||
ssid_str = str(args.ssid)
|
||||
RNS.log(f"Setting WiFi SSID to: {ssid_str}")
|
||||
rnode.set_wifi_ssid(ssid_str)
|
||||
except Exception as e:
|
||||
print(f"Could not set WiFi SSID: {e}")
|
||||
graceful_exit()
|
||||
|
||||
if args.psk:
|
||||
try:
|
||||
if args.psk.lower() == "none":
|
||||
psk_str = None
|
||||
RNS.log(f"Deleting WiFi PSK")
|
||||
else:
|
||||
psk_str = str(args.psk)
|
||||
RNS.log(f"Setting WiFi PSK")
|
||||
rnode.set_wifi_psk(psk_str)
|
||||
except Exception as e:
|
||||
print(f"Could not set WiFi PSK: {e}")
|
||||
graceful_exit()
|
||||
|
||||
if args.ip:
|
||||
try:
|
||||
if args.ip.lower() == "none":
|
||||
RNS.log(f"Setting WiFi IP to DHCP...")
|
||||
rnode.set_wifi_ip(None)
|
||||
else:
|
||||
RNS.log(f"Setting WiFi static IP to: {args.ip}")
|
||||
rnode.set_wifi_ip(args.ip)
|
||||
except Exception as e:
|
||||
print(f"Could not set WiFi IP: {e}")
|
||||
graceful_exit()
|
||||
|
||||
if args.nm:
|
||||
try:
|
||||
if args.nm.lower() == "none":
|
||||
RNS.log(f"Deleting WiFi static netmask configuration...")
|
||||
rnode.set_wifi_nm(None)
|
||||
else:
|
||||
RNS.log(f"Setting WiFi static netmask to: {args.nm}")
|
||||
rnode.set_wifi_nm(args.nm)
|
||||
except Exception as e:
|
||||
print(f"Could not set WiFi netmask: {e}")
|
||||
graceful_exit()
|
||||
|
||||
if args.wifi:
|
||||
try:
|
||||
mode = 0x00
|
||||
if str(args.wifi).lower().startswith("sta"): mode = 0x01
|
||||
elif str(args.wifi).lower().startswith("ap"): mode = 0x02
|
||||
if mode == 0x00: RNS.log(f"Disabling WiFi...")
|
||||
elif mode == 0x01: RNS.log(f"Setting WiFi to station mode")
|
||||
elif mode == 0x02: RNS.log(f"Setting WiFi to AP mode")
|
||||
rnode.set_wifi_mode(mode)
|
||||
except Exception as e:
|
||||
print(f"Could not set WiFi mode: {e}")
|
||||
graceful_exit()
|
||||
|
||||
if args.info:
|
||||
if rnode.provisioned:
|
||||
timestamp = struct.unpack(">I", rnode.made)[0]
|
||||
@@ -3969,8 +4353,8 @@ def main():
|
||||
print("cases, and copies of the source code for both the RNode Firmware,")
|
||||
print("Reticulum and other utilities.")
|
||||
print("")
|
||||
print("To activate the RNode Bootstrap Console, power up your RNode and press")
|
||||
print("the reset button twice with a one second interval. The RNode will now")
|
||||
print("To activate the RNode Bootstrap Console, power up your RNode and hold")
|
||||
print("down the user button for 10+ seconds, then release. The RNode will now")
|
||||
print("reboot into console mode, and activate a WiFi access point for you to")
|
||||
print("connect to. The console is then reachable at: http://10.0.0.1")
|
||||
print("")
|
||||
|
||||
+236
-259
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -11,8 +11,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -31,7 +39,8 @@ import argparse
|
||||
from RNS._version import __version__
|
||||
|
||||
remote_link = None
|
||||
def connect_remote(destination_hash, auth_identity, timeout, no_output = False):
|
||||
output_rst_str = "\r \r"
|
||||
def connect_remote(destination_hash, auth_identity, timeout, no_output = False, purpose="management"):
|
||||
global remote_link, reticulum
|
||||
if not RNS.Transport.has_path(destination_hash):
|
||||
if not no_output:
|
||||
@@ -43,7 +52,7 @@ def connect_remote(destination_hash, auth_identity, timeout, no_output = False):
|
||||
time.sleep(0.1)
|
||||
if time.time() - pr_time > timeout:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Path request timed out")
|
||||
exit(12)
|
||||
|
||||
@@ -52,98 +61,210 @@ def connect_remote(destination_hash, auth_identity, timeout, no_output = False):
|
||||
def remote_link_closed(link):
|
||||
if link.teardown_reason == RNS.Link.TIMEOUT:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("The link timed out, exiting now")
|
||||
elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("The link was closed by the server, exiting now")
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Link closed unexpectedly, exiting now")
|
||||
exit(10)
|
||||
|
||||
def remote_link_established(link):
|
||||
global remote_link
|
||||
link.identify(auth_identity)
|
||||
if purpose == "management": link.identify(auth_identity)
|
||||
remote_link = link
|
||||
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Establishing link with remote transport instance...", end=" ")
|
||||
sys.stdout.flush()
|
||||
|
||||
remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
|
||||
if purpose == "management": remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "remote", "management")
|
||||
elif purpose == "blackhole": remote_destination = RNS.Destination(remote_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "rnstransport", "info", "blackhole")
|
||||
link = RNS.Link(remote_destination)
|
||||
link.set_link_established_callback(remote_link_established)
|
||||
link.set_link_closed_callback(remote_link_closed)
|
||||
|
||||
def parse_hash(input_str):
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(input_str) != dest_len: raise ValueError("Hash length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
hash_bytes = bytes.fromhex(input_str)
|
||||
return hash_bytes
|
||||
except Exception as e: raise ValueError("Invalid hash entered. Check your input.")
|
||||
|
||||
def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity, timeout, drop_queues,
|
||||
drop_via, max_hops, remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT,
|
||||
no_output=False, json=False):
|
||||
blackholed=False, blackhole=False, unblackhole=False, blackhole_duration=None, blackhole_reason=None,
|
||||
remote_blackhole_list=False, remote_blackhole_list_filter=None, no_output=False, json=False):
|
||||
|
||||
global remote_link, reticulum
|
||||
reticulum = RNS.Reticulum(configdir = configdir, loglevel = 3+verbosity)
|
||||
if remote:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(remote) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
if len(remote) != dest_len: raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
try:
|
||||
identity_hash = bytes.fromhex(remote)
|
||||
remote_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.remote.management", identity_hash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
|
||||
identity = RNS.Identity.from_file(os.path.expanduser(management_identity))
|
||||
if identity == None:
|
||||
raise ValueError("Could not load management identity from "+str(management_identity))
|
||||
if identity == None: raise ValueError("Could not load management identity from "+str(management_identity))
|
||||
|
||||
try:
|
||||
connect_remote(remote_hash, identity, remote_timeout, no_output)
|
||||
except Exception as e:
|
||||
raise e
|
||||
try: connect_remote(remote_hash, identity, remote_timeout, no_output)
|
||||
except Exception as e: raise e
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(20)
|
||||
|
||||
while remote_link == None:
|
||||
time.sleep(0.1)
|
||||
while remote_link == None: time.sleep(0.1)
|
||||
|
||||
if blackholed or remote_blackhole_list:
|
||||
blackholed_list = None
|
||||
if blackholed:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("Listing blackholed identities on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
if table:
|
||||
try: blackholed_list = reticulum.get_blackholed_identities()
|
||||
except Exception as e:
|
||||
print(f"Could not get blackholed identities from RNS instance: {e}")
|
||||
exit(20)
|
||||
|
||||
elif remote_blackhole_list:
|
||||
try: identity_hash = parse_hash(destination_hexhash)
|
||||
except Exception as e:
|
||||
print(f"{e}")
|
||||
exit(20)
|
||||
|
||||
remote_hash = RNS.Destination.hash_from_name_and_identity("rnstransport.info.blackhole", identity_hash)
|
||||
connect_remote(remote_hash, None, remote_timeout, no_output, purpose="blackhole")
|
||||
while remote_link == None: time.sleep(0.1)
|
||||
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
receipt = remote_link.request("/list")
|
||||
while not receipt.concluded(): time.sleep(0.1)
|
||||
response = receipt.get_response()
|
||||
if type(response) == dict:
|
||||
blackholed_list = response
|
||||
print(output_rst_str, end="")
|
||||
else:
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("The remote request failed.")
|
||||
exit(10)
|
||||
|
||||
else:
|
||||
print(f"Nowhere to fetch blackhole list from")
|
||||
exit(255)
|
||||
|
||||
if not blackholed_list:
|
||||
print("No blackholed identity data available")
|
||||
exit(20)
|
||||
|
||||
else:
|
||||
rmlen = 64
|
||||
def trunc(input_str):
|
||||
if len(input_str) <= rmlen: return input_str
|
||||
else: return f"{input_str[:rmlen-1]}…"
|
||||
|
||||
try:
|
||||
now = time.time()
|
||||
for identity_hash in blackholed_list:
|
||||
until = blackholed_list[identity_hash]["until"]
|
||||
reason = blackholed_list[identity_hash]["reason"]
|
||||
source = blackholed_list[identity_hash]["source"]
|
||||
until_str = f"for {RNS.prettytime(max(0, until-now))}" if until else "indefinitely"
|
||||
reason_str = f" ({trunc(reason)})" if reason else ""
|
||||
by_str = f" by {RNS.prettyhexrep(source)}" if source != RNS.Transport.identity.hash else ""
|
||||
filter_str = f"{RNS.prettyhexrep(identity_hash)} {until_str} {reason_str} {by_str}"
|
||||
|
||||
if not remote_blackhole_list:
|
||||
if destination_hexhash and not destination_hexhash in filter_str: continue
|
||||
else:
|
||||
if remote_blackhole_list_filter and not remote_blackhole_list_filter in filter_str: continue
|
||||
|
||||
print(f"{RNS.prettyhexrep(identity_hash)} blackholed {until_str}{reason_str}{by_str}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error while displaying collected blackhole data: {e}")
|
||||
exit(20)
|
||||
|
||||
elif blackhole:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("Blackholing identity on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
identity_hash = parse_hash(destination_hexhash)
|
||||
until = time.time()+blackhole_duration*60*60 if blackhole_duration else None
|
||||
result = reticulum.blackhole_identity(identity_hash, until=until, reason=blackhole_reason)
|
||||
if result == True: print(f"Blackholed identity {destination_hexhash}")
|
||||
elif result == None: print(f"Identity {destination_hexhash} already blackholed")
|
||||
else: print(f"Could not blackhole identity {destination_hexhash}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Could not blackhole identity: {e}")
|
||||
exit(20)
|
||||
|
||||
elif unblackhole:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print(output_rst_str, end="")
|
||||
print("Blackholing identity on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
identity_hash = parse_hash(destination_hexhash)
|
||||
result = reticulum.unblackhole_identity(identity_hash)
|
||||
if result == True: print(f"Lifted blackhole for identity {destination_hexhash}")
|
||||
elif result == None: print(f"Identity {destination_hexhash} not blackholed")
|
||||
else: print(f"Could not unblackhole identity {destination_hexhash}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Could not unblackhole identity: {e}")
|
||||
exit(20)
|
||||
|
||||
elif table:
|
||||
destination_hash = None
|
||||
if destination_hexhash != None:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("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_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
if len(destination_hexhash) != dest_len: raise ValueError("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_hexhash)
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if not remote_link:
|
||||
table = sorted(reticulum.get_path_table(max_hops=max_hops), key=lambda e: (e["interface"], e["hops"]) )
|
||||
if not remote_link: table = sorted(reticulum.get_path_table(max_hops=max_hops), key=lambda e: (e["interface"], e["hops"]) )
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
receipt = remote_link.request("/path", data = ["table", destination_hash, max_hops])
|
||||
while not receipt.concluded():
|
||||
time.sleep(0.1)
|
||||
while not receipt.concluded(): time.sleep(0.1)
|
||||
response = receipt.get_response()
|
||||
if response:
|
||||
table = response
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("The remote request failed. Likely authentication failure.")
|
||||
exit(10)
|
||||
|
||||
@@ -152,20 +273,18 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
import json
|
||||
for p in table:
|
||||
for k in p:
|
||||
if isinstance(p[k], bytes):
|
||||
p[k] = RNS.hexrep(p[k], delimit=False)
|
||||
if isinstance(p[k], bytes): p[k] = RNS.hexrep(p[k], delimit=False)
|
||||
|
||||
print(json.dumps(table))
|
||||
exit()
|
||||
|
||||
else:
|
||||
for path in table:
|
||||
if destination_hash == None or destination_hash == path["hash"]:
|
||||
displayed += 1
|
||||
exp_str = RNS.timestamp_str(path["expires"])
|
||||
if path["hops"] == 1:
|
||||
m_str = " "
|
||||
else:
|
||||
m_str = "s"
|
||||
if path["hops"] == 1: m_str = " "
|
||||
else: m_str = "s"
|
||||
print(RNS.prettyhexrep(path["hash"])+" is "+str(path["hops"])+" hop"+m_str+" away via "+RNS.prettyhexrep(path["via"])+" on "+path["interface"]+" expires "+RNS.timestamp_str(path["expires"]))
|
||||
|
||||
if destination_hash != None and displayed == 0:
|
||||
@@ -177,21 +296,17 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
if destination_hexhash != None:
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("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_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
if len(destination_hexhash) != dest_len: raise ValueError("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_hexhash)
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if not remote_link:
|
||||
table = reticulum.get_rate_table()
|
||||
if not remote_link: table = reticulum.get_rate_table()
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Sending request...", end=" ")
|
||||
sys.stdout.flush()
|
||||
receipt = remote_link.request("/path", data = ["rates", destination_hash])
|
||||
@@ -200,10 +315,10 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
response = receipt.get_response()
|
||||
if response:
|
||||
table = response
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
else:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("The remote request failed. Likely authentication failure.")
|
||||
exit(10)
|
||||
|
||||
@@ -212,15 +327,12 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
import json
|
||||
for p in table:
|
||||
for k in p:
|
||||
if isinstance(p[k], bytes):
|
||||
p[k] = RNS.hexrep(p[k], delimit=False)
|
||||
if isinstance(p[k], bytes): p[k] = RNS.hexrep(p[k], delimit=False)
|
||||
|
||||
print(json.dumps(table))
|
||||
exit()
|
||||
else:
|
||||
if len(table) == 0:
|
||||
print("No information available")
|
||||
|
||||
if len(table) == 0: print("No information available")
|
||||
else:
|
||||
displayed = 0
|
||||
for entry in table:
|
||||
@@ -266,7 +378,7 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
elif drop_queues:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Dropping announce queues on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
@@ -276,24 +388,20 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
elif drop:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Dropping path on remote instances not yet implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("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_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
if len(destination_hexhash) != dest_len: raise ValueError("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_hexhash)
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if reticulum.drop_path(destination_hash):
|
||||
print("Dropped path to "+RNS.prettyhexrep(destination_hash))
|
||||
if reticulum.drop_path(destination_hash): print("Dropped path to "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print("Unable to drop path to "+RNS.prettyhexrep(destination_hash)+". Does it exist?")
|
||||
sys.exit(1)
|
||||
@@ -301,24 +409,20 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
elif drop_via:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Dropping all paths via specific transport instance on remote instances yet not implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("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_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
if len(destination_hexhash) != dest_len: raise ValueError("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_hexhash)
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
if reticulum.drop_all_via(destination_hash):
|
||||
print("Dropped all paths via "+RNS.prettyhexrep(destination_hash))
|
||||
if reticulum.drop_all_via(destination_hash): print("Dropped all paths via "+RNS.prettyhexrep(destination_hash))
|
||||
else:
|
||||
print("Unable to drop paths via "+RNS.prettyhexrep(destination_hash)+". Does the transport instance exist?")
|
||||
sys.exit(1)
|
||||
@@ -326,18 +430,15 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
else:
|
||||
if remote_link:
|
||||
if not no_output:
|
||||
print("\r \r", end="")
|
||||
print(output_rst_str, end="")
|
||||
print("Requesting paths on remote instances not implemented")
|
||||
exit(255)
|
||||
|
||||
try:
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(destination_hexhash) != dest_len:
|
||||
raise ValueError("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_hexhash)
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid destination entered. Check your input.")
|
||||
if len(destination_hexhash) != dest_len: raise ValueError("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_hexhash)
|
||||
except Exception as e: raise ValueError("Invalid destination entered. Check your input.")
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
@@ -366,166 +467,57 @@ def program_setup(configdir, table, rates, drop, destination_hexhash, verbosity,
|
||||
next_hop = RNS.prettyhexrep(next_hop_bytes)
|
||||
next_hop_interface = reticulum.get_next_hop_if_name(destination_hash)
|
||||
|
||||
if hops != 1:
|
||||
ms = "s"
|
||||
else:
|
||||
ms = ""
|
||||
if hops != 1: ms = "s"
|
||||
else: ms = ""
|
||||
|
||||
print("\rPath found, destination "+RNS.prettyhexrep(destination_hash)+" is "+str(hops)+" hop"+ms+" away via "+next_hop+" on "+next_hop_interface)
|
||||
else:
|
||||
print("\r \rPath not found")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description="Reticulum Path Discovery Utility")
|
||||
|
||||
parser.add_argument("--config",
|
||||
action="store",
|
||||
default=None,
|
||||
help="path to alternative Reticulum config directory",
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version="rnpath {version}".format(version=__version__)
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--table",
|
||||
action="store_true",
|
||||
help="show all known paths",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--max",
|
||||
action="store",
|
||||
metavar="hops",
|
||||
type=int,
|
||||
help="maximum hops to filter path table by",
|
||||
default=None
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--rates",
|
||||
action="store_true",
|
||||
help="show announce rate info",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--drop",
|
||||
action="store_true",
|
||||
help="remove the path to a destination",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-D",
|
||||
"--drop-announces",
|
||||
action="store_true",
|
||||
help="drop all queued announces",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-x", "--drop-via",
|
||||
action="store_true",
|
||||
help="drop all paths via specified transport instance",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
action="store",
|
||||
metavar="seconds",
|
||||
type=float,
|
||||
help="timeout before giving up",
|
||||
default=RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-R",
|
||||
action="store",
|
||||
metavar="hash",
|
||||
help="transport identity hash of remote instance to manage",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
action="store",
|
||||
metavar="path",
|
||||
help="path to identity used for remote management",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-W",
|
||||
action="store",
|
||||
metavar="seconds",
|
||||
type=float,
|
||||
help="timeout before giving up on remote queries",
|
||||
default=RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="output in JSON format",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"destination",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="hexadecimal hash of the destination",
|
||||
type=str
|
||||
)
|
||||
|
||||
parser = argparse.ArgumentParser(description="Reticulum Path Management Utility")
|
||||
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("--version", action="version", version="rnpath {version}".format(version=__version__))
|
||||
parser.add_argument("-t", "--table", action="store_true", help="show all known paths", default=False)
|
||||
parser.add_argument("-m", "--max", action="store", metavar="hops", type=int, help="maximum hops to filter path table by", default=None)
|
||||
parser.add_argument("-r", "--rates", action="store_true", help="show announce rate info", default=False)
|
||||
parser.add_argument("-d", "--drop", action="store_true", help="remove the path to a destination", default=False)
|
||||
parser.add_argument("-D", "--drop-announces", action="store_true", help="drop all queued announces", default=False)
|
||||
parser.add_argument("-x", "--drop-via", action="store_true", help="drop all paths via specified transport instance", default=False)
|
||||
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="timeout before giving up", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
parser.add_argument("-R", action="store", metavar="hash", help="transport identity hash of remote instance to manage", default=None, type=str)
|
||||
parser.add_argument("-i", action="store", metavar="path", help="path to identity used for remote management", default=None, type=str)
|
||||
parser.add_argument("-W", action="store", metavar="seconds", type=float, help="timeout before giving up on remote queries", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
parser.add_argument("-b", "--blackholed", action="store_true", help="list blackholed identities", default=False)
|
||||
parser.add_argument("-B", "--blackhole", action="store_true", help="blackhole identity", default=False)
|
||||
parser.add_argument("-U", "--unblackhole", action="store_true", help="unblackhole identity", default=False)
|
||||
parser.add_argument( "--duration", action="store", type=float, help="duration of blackhole enforcement in hours", default=None)
|
||||
parser.add_argument( "--reason", action="store", type=str, help="reason for blackholing identity", default=None)
|
||||
parser.add_argument("-p", "--blackholed-list", action="store_true", help="view published blackhole list for remote transport instance", default=False)
|
||||
parser.add_argument("-j", "--json", action="store_true", help="output in JSON format", default=False)
|
||||
parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the destination", type=str)
|
||||
parser.add_argument("list_filter", nargs="?", default=None, help="filter for remote blackhole list view", type=str)
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.config:
|
||||
configarg = args.config
|
||||
else:
|
||||
configarg = None
|
||||
if args.config: configarg = args.config
|
||||
else: configarg = None
|
||||
|
||||
if not args.drop_announces and not args.table and not args.rates and not args.destination and not args.drop_via:
|
||||
if not args.drop_announces and not args.table and not args.rates and not args.destination and not args.drop_via and not args.blackholed:
|
||||
print("")
|
||||
parser.print_help()
|
||||
print("")
|
||||
else:
|
||||
program_setup(
|
||||
configdir = configarg,
|
||||
table = args.table,
|
||||
rates = args.rates,
|
||||
drop = args.drop,
|
||||
destination_hexhash = args.destination,
|
||||
verbosity = args.verbose,
|
||||
timeout = args.w,
|
||||
drop_queues = args.drop_announces,
|
||||
drop_via = args.drop_via,
|
||||
max_hops = args.max,
|
||||
remote=args.R,
|
||||
management_identity=args.i,
|
||||
remote_timeout=args.W,
|
||||
json=args.json,
|
||||
)
|
||||
program_setup(configdir = configarg, table = args.table, rates = args.rates, drop = args.drop, destination_hexhash = args.destination,
|
||||
verbosity = args.verbose, timeout = args.w, drop_queues = args.drop_announces, drop_via = args.drop_via, max_hops = args.max,
|
||||
remote=args.R, management_identity=args.i, remote_timeout=args.W, blackholed=args.blackholed, blackhole=args.blackhole,
|
||||
unblackhole=args.unblackhole, blackhole_duration=args.duration, blackhole_reason=args.reason, remote_blackhole_list=args.blackholed_list,
|
||||
remote_blackhole_list_filter=args.list_filter, json=args.json)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
@@ -535,38 +527,23 @@ def main():
|
||||
def pretty_date(time=False):
|
||||
from datetime import datetime
|
||||
now = datetime.now()
|
||||
if type(time) is int:
|
||||
diff = now - datetime.fromtimestamp(time)
|
||||
elif isinstance(time,datetime):
|
||||
diff = now - time
|
||||
elif not time:
|
||||
diff = now - now
|
||||
if type(time) is int: diff = now - datetime.fromtimestamp(time)
|
||||
elif isinstance(time,datetime): diff = now - time
|
||||
elif not time: diff = now - now
|
||||
second_diff = diff.seconds
|
||||
day_diff = diff.days
|
||||
if day_diff < 0:
|
||||
return ''
|
||||
if day_diff < 0: return ''
|
||||
if day_diff == 0:
|
||||
if second_diff < 10:
|
||||
return str(second_diff) + " seconds"
|
||||
if second_diff < 60:
|
||||
return str(second_diff) + " seconds"
|
||||
if second_diff < 120:
|
||||
return "1 minute"
|
||||
if second_diff < 3600:
|
||||
return str(int(second_diff / 60)) + " minutes"
|
||||
if second_diff < 7200:
|
||||
return "an hour"
|
||||
if second_diff < 86400:
|
||||
return str(int(second_diff / 3600)) + " hours"
|
||||
if day_diff == 1:
|
||||
return "1 day"
|
||||
if day_diff < 7:
|
||||
return str(day_diff) + " days"
|
||||
if day_diff < 31:
|
||||
return str(int(day_diff / 7)) + " weeks"
|
||||
if day_diff < 365:
|
||||
return str(int(day_diff / 30)) + " months"
|
||||
if second_diff < 10: return str(second_diff) + " seconds"
|
||||
if second_diff < 60: return str(second_diff) + " seconds"
|
||||
if second_diff < 120: return "1 minute"
|
||||
if second_diff < 3600: return str(int(second_diff / 60)) + " minutes"
|
||||
if second_diff < 7200: return "an hour"
|
||||
if second_diff < 86400: return str(int(second_diff / 3600)) + " hours"
|
||||
if day_diff == 1: return "1 day"
|
||||
if day_diff < 7: return str(day_diff) + " days"
|
||||
if day_diff < 31: return str(int(day_diff / 7)) + " weeks"
|
||||
if day_diff < 365: return str(int(day_diff / 30)) + " months"
|
||||
return str(int(day_diff / 365)) + " years"
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
if __name__ == "__main__": main()
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -11,8 +11,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
|
||||
+109
-17
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -11,8 +11,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -29,7 +37,7 @@ import time
|
||||
from RNS._version import __version__
|
||||
|
||||
|
||||
def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
|
||||
def program_setup(configdir, verbosity = 0, quietness = 0, service = False, interactive=False):
|
||||
targetverbosity = verbosity-quietness
|
||||
|
||||
if service:
|
||||
@@ -42,12 +50,14 @@ def program_setup(configdir, verbosity = 0, quietness = 0, service = False):
|
||||
if reticulum.is_connected_to_shared_instance:
|
||||
RNS.log("Started rnsd version {version} connected to another shared local instance, this is probably NOT what you want!".format(version=__version__), RNS.LOG_WARNING)
|
||||
else:
|
||||
if RNS.Reticulum.get_instance().shared_instance_interface:
|
||||
RNS.Reticulum.get_instance().shared_instance_interface.server.daemon_threads = True
|
||||
# TODO: Rethink why this was added
|
||||
# if RNS.Reticulum.get_instance().shared_instance_interface:
|
||||
# RNS.Reticulum.get_instance().shared_instance_interface.server.daemon_threads = True
|
||||
RNS.log("Started rnsd version {version}".format(version=__version__), RNS.LOG_NOTICE)
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
if interactive: import code; code.interact(local=globals())
|
||||
else:
|
||||
while True: time.sleep(1)
|
||||
|
||||
def main():
|
||||
try:
|
||||
@@ -56,6 +66,7 @@ def main():
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
parser.add_argument('-q', '--quiet', action='count', default=0)
|
||||
parser.add_argument('-s', '--service', action='store_true', default=False, help="rnsd is running as a service and should log to file")
|
||||
parser.add_argument('-i', '--interactive', action='store_true', default=False, help="drop into interactive shell after initialisation")
|
||||
parser.add_argument("--exampleconfig", action='store_true', default=False, help="print verbose configuration example to stdout and exit")
|
||||
parser.add_argument("--version", action="version", version="rnsd {version}".format(version=__version__))
|
||||
|
||||
@@ -70,7 +81,7 @@ def main():
|
||||
else:
|
||||
configarg = None
|
||||
|
||||
program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet, service=args.service)
|
||||
program_setup(configdir = configarg, verbosity=args.verbose, quietness=args.quiet, service=args.service, interactive=args.interactive)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
@@ -106,12 +117,24 @@ share_instance = Yes
|
||||
|
||||
# If you want to run multiple *different* shared instances
|
||||
# on the same system, you will need to specify different
|
||||
# shared instance ports for each. The defaults are given
|
||||
# below, and again, these options can be left out if you
|
||||
# don't need them.
|
||||
# instance names for each. On platforms supporting domain
|
||||
# sockets, this can be done with the instance_name option:
|
||||
|
||||
shared_instance_port = 37428
|
||||
instance_control_port = 37429
|
||||
instance_name = default
|
||||
|
||||
# Some platforms don't support domain sockets, and if that
|
||||
# is the case, you can isolate different instances by
|
||||
# specifying a unique set of ports for each:
|
||||
|
||||
# shared_instance_port = 37428
|
||||
# instance_control_port = 37429
|
||||
|
||||
|
||||
# If you want to explicitly use TCP for shared instance
|
||||
# communication, instead of domain sockets, this is also
|
||||
# possible, by using the following option:
|
||||
|
||||
# shared_instance_type = tcp
|
||||
|
||||
|
||||
# On systems where running instances may not have access
|
||||
@@ -137,13 +160,62 @@ instance_control_port = 37429
|
||||
# remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc
|
||||
|
||||
|
||||
# For easier management, discovery and configuration of
|
||||
# networks with many individual transport instances,
|
||||
# you can specify a network identity to be used across
|
||||
# a set of instances. If sending interface discovery
|
||||
# announces, these will all be signed by the specified
|
||||
# network identity, and other nodes discovering your
|
||||
# interfaces will be able to identify that they belong
|
||||
# to the same network, even though they exist on different
|
||||
# transport nodes.
|
||||
|
||||
# network_identity = ~/.reticulum/storage/identity/network
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# If you only want to discover interfaces from specific
|
||||
# networks, you can provide a list of network identities
|
||||
# from which to discover interfaces. If this option is not
|
||||
# provided, interfaces will be discovered from all transport
|
||||
# instances on all connected networks.
|
||||
|
||||
# interface_discovery_sources = 78616ff7c4b8d3886d67d494b440f333, cb127015e13aa6ea1e0a606cdc9123d0
|
||||
|
||||
|
||||
# It is possible to automatically bring up and connect new
|
||||
# interfaces discovered over the network. This option is
|
||||
# disabled by default, but allows you to specify a maximum
|
||||
# number of discovered interfaces to automatically connect.
|
||||
# Additionally, if this option is enabled, Reticulum will
|
||||
# also try to autoconnect available auto-discovered inter-
|
||||
# faces on startup, up to the maximum number specified.
|
||||
|
||||
# autoconnect_discovered_interfaces = 0
|
||||
|
||||
|
||||
# To prevent interface discovery spamming, a valid crypto-
|
||||
# graphic stamp is required per announced interface. You
|
||||
# can configure the minimum required value to accept as
|
||||
# valid for discovered interfaces.
|
||||
|
||||
# required_discovery_value = 14
|
||||
|
||||
|
||||
# You can configure Reticulum to panic and forcibly close
|
||||
# if an unrecoverable interface error occurs, such as the
|
||||
# hardware device for an interface disappearing. This is
|
||||
# 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
|
||||
|
||||
|
||||
# When Transport is enabled, it is possible to allow the
|
||||
@@ -154,7 +226,27 @@ panic_on_interface_error = No
|
||||
# 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
|
||||
|
||||
# List of remote transport identities from which to auto-
|
||||
# matically source lists of blackholed identities.
|
||||
#
|
||||
# If you're connecting to a large external network, you
|
||||
# can use one or more external blackhole list to block
|
||||
# spammy and excessive announces onto your network. This
|
||||
# funtionality is especially useful if you're hosting public
|
||||
# entrypoints or gateways. The list source below provides a
|
||||
# functional example, but better, more timely maintained
|
||||
# lists probably exist in the community.
|
||||
|
||||
# blackhole_sources = 521c87a83afb8f29e4455e77930b973b
|
||||
|
||||
|
||||
[logging]
|
||||
|
||||
+280
-170
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -11,8 +11,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -27,6 +35,7 @@ import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import io
|
||||
|
||||
from RNS._version import __version__
|
||||
|
||||
@@ -133,14 +142,12 @@ def get_remote_status(destination_hash, include_lstats, identity, no_output=Fals
|
||||
|
||||
return request_result
|
||||
|
||||
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False,
|
||||
lstats=False, sorting=None, sort_reverse=False, remote=None, management_identity=None,
|
||||
remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True, rns_instance=None, traffic_totals=False):
|
||||
|
||||
if remote:
|
||||
require_shared = False
|
||||
else:
|
||||
require_shared = True
|
||||
def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=False, astats=False, lstats=False, sorting=None, sort_reverse=False,
|
||||
remote=None, management_identity=None, remote_timeout=RNS.Transport.PATH_REQUEST_TIMEOUT, must_exit=True, rns_instance=None,
|
||||
traffic_totals=False, discovered_interfaces=False, config_entries=False):
|
||||
|
||||
if remote: require_shared = False
|
||||
else: require_shared = True
|
||||
|
||||
try:
|
||||
if rns_instance:
|
||||
@@ -151,15 +158,150 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
|
||||
except Exception as e:
|
||||
print("No shared RNS instance available to get status from")
|
||||
if must_exit:
|
||||
exit(1)
|
||||
else:
|
||||
return
|
||||
if must_exit: exit(1)
|
||||
else: return
|
||||
|
||||
link_count = None
|
||||
stats = None
|
||||
|
||||
details = False
|
||||
if config_entries:
|
||||
discovered_interfaces = True
|
||||
details = True
|
||||
|
||||
if discovered_interfaces:
|
||||
if_discovery = RNS.Discovery.InterfaceDiscovery(discover_interfaces=False)
|
||||
ifs = if_discovery.list_discovered_interfaces()
|
||||
print("")
|
||||
|
||||
if json:
|
||||
import json
|
||||
for i in ifs:
|
||||
for e in i:
|
||||
if isinstance(i[e], bytes): i[e] = RNS.hexrep(i[e], delimit=False)
|
||||
|
||||
print(json.dumps(ifs))
|
||||
|
||||
else:
|
||||
filtered_ifs = []
|
||||
for i in ifs:
|
||||
name = i["name"]
|
||||
if not name_filter or name_filter.lower() in name.lower(): filtered_ifs.append(i)
|
||||
|
||||
if details:
|
||||
for idx, i in enumerate(filtered_ifs):
|
||||
try:
|
||||
name = i["name"]
|
||||
if_type = i["type"]
|
||||
status = i["status"]
|
||||
|
||||
if status == "available": status_display = "Available"
|
||||
elif status == "unknown": status_display = "Unknown"
|
||||
elif status == "stale": status_display = "Stale"
|
||||
else: status_display = status
|
||||
|
||||
now = time.time()
|
||||
dago = now-i["discovered"]
|
||||
hago = now-i["last_heard"]
|
||||
discovered_display = f"{RNS.prettytime(dago, compact=True)} ago"
|
||||
last_heard_display = f"{RNS.prettytime(hago, compact=True)} ago"
|
||||
transport_str = "Enabled" if i["transport"] else "Disabled"
|
||||
|
||||
if i["latitude"] is not None and i["longitude"] is not None:
|
||||
lat = round(i["latitude"], 4)
|
||||
lon = round(i["longitude"], 4)
|
||||
if i["height"] != None: height = ", "+str(i["height"])+"m h"
|
||||
else: height = ""
|
||||
location = f"{lat}, {lon}{height}"
|
||||
else: location = "Unknown"
|
||||
|
||||
transport_id = None
|
||||
network = None
|
||||
if "transport_id" in i: transport_id = i["transport_id"]
|
||||
if "transport_id" in i and "network_id" in i and i["transport_id"] != i["network_id"]:
|
||||
network = i["network_id"]
|
||||
|
||||
if idx > 0: print("\n"+"="*32+"\n")
|
||||
if network: print(f"Network ID : {network}")
|
||||
if transport_id: print(f"Transport ID : {transport_id}")
|
||||
|
||||
print(f"Name : {name}")
|
||||
print(f"Type : {if_type}")
|
||||
print(f"Status : {status_display}")
|
||||
print(f"Transport : {transport_str}")
|
||||
print(f"Distance : {i['hops']} hop{'' if i['hops'] == 1 else 's'}")
|
||||
print(f"Discovered : {discovered_display}")
|
||||
print(f"Last Heard : {last_heard_display}")
|
||||
print(f"Location : {location}")
|
||||
|
||||
if "frequency" in i: print(f"Frequency : {i['frequency']:,} Hz")
|
||||
if "bandwidth" in i: print(f"Bandwidth : {i['bandwidth']:,} Hz")
|
||||
if "sf" in i: print(f"Sprd. Factor : {i['sf']}")
|
||||
if "cr" in i: print(f"Coding Rate : {i['cr']}")
|
||||
if "modulation" in i: print(f"Modulation : {i['modulation']}")
|
||||
if "reachable_on" in i: print(f"Address : {i['reachable_on']}:{i['port']}")
|
||||
|
||||
print(f"Stamp Value : {i['value']}")
|
||||
|
||||
print(f"\nConfiguration Entry:")
|
||||
config_lines = i["config_entry"].split('\n')
|
||||
for line in config_lines: print(f" {line}")
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
else:
|
||||
print(f"{'Name':<25} {'Type':<12} {'Status':<12} {'Last Heard':<12} {'Value':<8} {'Location':<15}")
|
||||
print("-" * 89)
|
||||
|
||||
for i in filtered_ifs:
|
||||
try:
|
||||
name = i["name"][:24] + "…" if len(i["name"]) > 24 else i["name"]
|
||||
|
||||
if_type = i["type"].replace("Interface", "")
|
||||
|
||||
status = i["status"]
|
||||
if status == "available": status_display = "✓ Available"
|
||||
elif status == "unknown": status_display = "? Unknown"
|
||||
elif status == "stale": status_display = "× Stale"
|
||||
else: status_display = status
|
||||
|
||||
now = time.time()
|
||||
last_heard = i["last_heard"]
|
||||
diff = now - last_heard
|
||||
|
||||
if diff < 60: last_heard_display = "Just now"
|
||||
elif diff < 3600:
|
||||
mins = int(diff / 60)
|
||||
last_heard_display = f"{mins}m ago"
|
||||
elif diff < 86400:
|
||||
hours = int(diff / 3600)
|
||||
last_heard_display = f"{hours}h ago"
|
||||
else:
|
||||
days = int(diff / 86400)
|
||||
last_heard_display = f"{days}d ago"
|
||||
|
||||
value = str(i["value"])
|
||||
|
||||
if i["latitude"] is not None and i["longitude"] is not None:
|
||||
lat = round(i["latitude"], 4)
|
||||
lon = round(i["longitude"], 4)
|
||||
location = f"{lat}, {lon}"
|
||||
else: location = "N/A"
|
||||
|
||||
print(f"{name:<25} {if_type:<12} {status_display:<12} {last_heard_display:<12} {value:<8} {location:<15}")
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if must_exit: exit(0)
|
||||
else: return
|
||||
|
||||
if remote:
|
||||
try:
|
||||
if management_identity is None:
|
||||
raise ValueError("Remote management requires an identity file. Use -i to specify the path to a management identity.")
|
||||
|
||||
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
|
||||
if len(remote) != dest_len:
|
||||
raise ValueError("Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2))
|
||||
@@ -179,25 +321,19 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
stats, link_count = remote_status
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
if must_exit:
|
||||
exit(20)
|
||||
else:
|
||||
return
|
||||
if must_exit: exit(20)
|
||||
else: return
|
||||
|
||||
else:
|
||||
if lstats:
|
||||
try:
|
||||
link_count = reticulum.get_link_count()
|
||||
except Exception as e:
|
||||
pass
|
||||
try: link_count = reticulum.get_link_count()
|
||||
except Exception as e: pass
|
||||
|
||||
try:
|
||||
stats = reticulum.get_interface_stats()
|
||||
except Exception as e:
|
||||
pass
|
||||
try: stats = reticulum.get_interface_stats()
|
||||
except Exception as e: pass
|
||||
|
||||
if stats != None:
|
||||
if json:
|
||||
@@ -214,10 +350,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
i[k] = RNS.hexrep(i[k], delimit=False)
|
||||
|
||||
print(json.dumps(stats))
|
||||
if must_exit:
|
||||
exit()
|
||||
else:
|
||||
return
|
||||
if must_exit: exit()
|
||||
else: return
|
||||
|
||||
interfaces = stats["interfaces"]
|
||||
if sorting != None and isinstance(sorting, str):
|
||||
@@ -243,13 +377,16 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
if sorting == "held":
|
||||
interfaces.sort(key=lambda i: i["held_announces"], reverse=not sort_reverse)
|
||||
|
||||
|
||||
|
||||
for ifstat in interfaces:
|
||||
name = ifstat["name"]
|
||||
|
||||
if dispall or not (
|
||||
name.startswith("LocalInterface[") or
|
||||
name.startswith("TCPInterface[Client") or
|
||||
name.startswith("BackboneInterface[Client on") or
|
||||
name.startswith("AutoInterfacePeer[") or
|
||||
name.startswith("WeaveInterfacePeer[") or
|
||||
name.startswith("I2PInterfacePeer[Connected peer") or
|
||||
(name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False))
|
||||
):
|
||||
@@ -258,23 +395,15 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
if name_filter == None or name_filter.lower() in name.lower():
|
||||
print("")
|
||||
|
||||
if ifstat["status"]:
|
||||
ss = "Up"
|
||||
else:
|
||||
ss = "Down"
|
||||
if ifstat["status"]: ss = "Up"
|
||||
else: ss = "Down"
|
||||
|
||||
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"
|
||||
else:
|
||||
modestr = "Full"
|
||||
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"
|
||||
else: modestr = "Full"
|
||||
|
||||
|
||||
if ifstat["clients"] != None:
|
||||
@@ -306,6 +435,9 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
|
||||
print(" {n}".format(n=ifstat["name"]))
|
||||
|
||||
if "autoconnect_source" in ifstat and ifstat["autoconnect_source"] != None:
|
||||
print(" Source : Auto-connect via <{ns}>".format(ns=ifstat["autoconnect_source"]))
|
||||
|
||||
if "ifac_netname" in ifstat and ifstat["ifac_netname"] != None:
|
||||
print(" Network : {nn}".format(nn=ifstat["ifac_netname"]))
|
||||
|
||||
@@ -321,10 +453,32 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
print(" Rate : {ss}".format(ss=speed_str(ifstat["bitrate"])))
|
||||
|
||||
if "noise_floor" in ifstat:
|
||||
if ifstat["noise_floor"] != None:
|
||||
print(" Noise Fl. : {nfl} dBm".format(nfl=str(ifstat["noise_floor"])))
|
||||
if not "interference" in ifstat: nstr = ""
|
||||
else:
|
||||
print(" Noise Fl. : Unknown")
|
||||
nf = ifstat["interference"]
|
||||
lstr = ", no interference"
|
||||
if "interference_last_ts" in ifstat and "interference_last_dbm" in ifstat:
|
||||
lago = time.time()-ifstat["interference_last_ts"]
|
||||
ldbm = ifstat["interference_last_dbm"]
|
||||
lstr = f"\n Intrfrnc. : {ldbm} dBm {RNS.prettytime(lago, compact=True)} ago"
|
||||
|
||||
|
||||
nstr = f"\n Intrfrnc. : {nf} dBm" if nf else lstr
|
||||
|
||||
if ifstat["noise_floor"] != None: print(" Noise Fl. : {nfl} dBm{ntr}".format(nfl=str(ifstat["noise_floor"]), ntr=nstr))
|
||||
else: print(" Noise Fl. : Unknown")
|
||||
|
||||
if "cpu_load" in ifstat:
|
||||
if ifstat["cpu_load"] != None: print(" CPU load : {v} %".format(v=str(ifstat["cpu_load"])))
|
||||
else: print(" CPU load : Unknown")
|
||||
|
||||
if "cpu_temp" in ifstat:
|
||||
if ifstat["cpu_temp"] != None: print(" CPU temp : {v}°C".format(v=str(ifstat["cpu_temp"])))
|
||||
else: print(" CPU load : Unknown")
|
||||
|
||||
if "mem_load" in ifstat:
|
||||
if ifstat["cpu_load"] != None: print(" Mem usage : {v} %".format(v=str(ifstat["mem_load"])))
|
||||
else: print(" Mem usage : Unknown")
|
||||
|
||||
if "battery_percent" in ifstat and ifstat["battery_percent"] != None:
|
||||
try:
|
||||
@@ -336,10 +490,22 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
|
||||
if "airtime_short" in ifstat and "airtime_long" in ifstat:
|
||||
print(" Airtime : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["airtime_short"]),atl=str(ifstat["airtime_long"])))
|
||||
|
||||
|
||||
if "channel_load_short" in ifstat and "channel_load_long" in ifstat:
|
||||
print(" Ch. Load : {ats}% (15s), {atl}% (1h)".format(ats=str(ifstat["channel_load_short"]),atl=str(ifstat["channel_load_long"])))
|
||||
|
||||
if "switch_id" in ifstat:
|
||||
if ifstat["switch_id"] != None: print(" Switch ID : {v}".format(v=str(ifstat["switch_id"])))
|
||||
else: print(" Switch ID : Unknown")
|
||||
|
||||
if "endpoint_id" in ifstat:
|
||||
if ifstat["endpoint_id"] != None: print(" Endpoint : {v}".format(v=str(ifstat["endpoint_id"])))
|
||||
else: print(" Endpoint : Unknown")
|
||||
|
||||
if "via_switch_id" in ifstat:
|
||||
if ifstat["via_switch_id"] != None: print(" Via : {v}".format(v=str(ifstat["via_switch_id"])))
|
||||
else: print(" Via : Unknown")
|
||||
|
||||
if "peers" in ifstat and ifstat["peers"] != None:
|
||||
print(" Peers : {np} reachable".format(np=ifstat["peers"]))
|
||||
|
||||
@@ -349,7 +515,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
if "ifac_signature" in ifstat and ifstat["ifac_signature"] != None:
|
||||
sigstr = "<…"+RNS.hexrep(ifstat["ifac_signature"][-5:], delimit=False)+">"
|
||||
print(" Access : {nb}-bit IFAC by {sig}".format(nb=ifstat["ifac_size"]*8, sig=sigstr))
|
||||
|
||||
|
||||
if "i2p_b32" in ifstat and ifstat["i2p_b32"] != None:
|
||||
print(" I2P B32 : {ep}".format(ep=str(ifstat["i2p_b32"])))
|
||||
|
||||
@@ -359,14 +525,14 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
print(" Queued : {np} announce".format(np=aqn))
|
||||
else:
|
||||
print(" Queued : {np} announces".format(np=aqn))
|
||||
|
||||
|
||||
if astats and "held_announces" in ifstat and ifstat["held_announces"] != None and ifstat["held_announces"] > 0:
|
||||
aqn = ifstat["held_announces"]
|
||||
if aqn == 1:
|
||||
print(" Held : {np} announce".format(np=aqn))
|
||||
else:
|
||||
print(" Held : {np} announces".format(np=aqn))
|
||||
|
||||
|
||||
if astats and "incoming_announce_frequency" in ifstat and ifstat["incoming_announce_frequency"] != None:
|
||||
print(" Announces : {iaf}↑".format(iaf=RNS.prettyfrequency(ifstat["outgoing_announce_frequency"])))
|
||||
print(" {iaf}↓".format(iaf=RNS.prettyfrequency(ifstat["incoming_announce_frequency"])))
|
||||
@@ -384,7 +550,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
if "rxs" in ifstat and "txs" in ifstat:
|
||||
rxstat += " "+RNS.prettyspeed(ifstat["rxs"])
|
||||
txstat += " "+RNS.prettyspeed(ifstat["txs"])
|
||||
|
||||
|
||||
print(f" Traffic : {txstat}\n {rxstat}")
|
||||
|
||||
lstr = ""
|
||||
@@ -410,6 +576,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
|
||||
if "transport_id" in stats and stats["transport_id"] != None:
|
||||
print("\n Transport Instance "+RNS.prettyhexrep(stats["transport_id"])+" running")
|
||||
if "network_id" in stats and stats["network_id"] != None:
|
||||
print(" Network Identity "+RNS.prettyhexrep(stats["network_id"]))
|
||||
if "probe_responder" in stats and stats["probe_responder"] != None:
|
||||
print(" Probe responder at "+RNS.prettyhexrep(stats["probe_responder"])+ " active")
|
||||
if "transport_uptime" in stats and stats["transport_uptime"] != None:
|
||||
@@ -419,7 +587,7 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
print(f"\n{lstr}")
|
||||
|
||||
print("")
|
||||
|
||||
|
||||
else:
|
||||
if not remote:
|
||||
print("Could not get RNS status")
|
||||
@@ -436,125 +604,67 @@ def main(must_exit=True, rns_instance=None):
|
||||
parser.add_argument("--config", action="store", default=None, help="path to alternative Reticulum config directory", type=str)
|
||||
parser.add_argument("--version", action="version", version="rnstatus {version}".format(version=__version__))
|
||||
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="show all interfaces",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-A",
|
||||
"--announce-stats",
|
||||
action="store_true",
|
||||
help="show announce stats",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--link-stats",
|
||||
action="store_true",
|
||||
help="show link stats",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--totals",
|
||||
action="store_true",
|
||||
help="display traffic totals",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--sort",
|
||||
action="store",
|
||||
help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, held]",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--reverse",
|
||||
action="store_true",
|
||||
help="reverse sorting",
|
||||
default=False,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="output in JSON format",
|
||||
default=False
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-R",
|
||||
action="store",
|
||||
metavar="hash",
|
||||
help="transport identity hash of remote instance to get status from",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
action="store",
|
||||
metavar="path",
|
||||
help="path to identity used for remote management",
|
||||
default=None,
|
||||
type=str
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
action="store",
|
||||
metavar="seconds",
|
||||
type=float,
|
||||
help="timeout before giving up on remote queries",
|
||||
default=RNS.Transport.PATH_REQUEST_TIMEOUT
|
||||
)
|
||||
|
||||
parser.add_argument("-a", "--all", action="store_true", help="show all interfaces", default=False)
|
||||
parser.add_argument("-A", "--announce-stats", action="store_true", help="show announce stats", default=False)
|
||||
parser.add_argument("-l", "--link-stats", action="store_true", help="show link stats", default=False)
|
||||
parser.add_argument("-t", "--totals", action="store_true", help="display traffic totals", default=False)
|
||||
parser.add_argument("-s", "--sort", action="store", help="sort interfaces by [rate, traffic, rx, tx, rxs, txs, announces, arx, atx, held]", default=None, type=str)
|
||||
parser.add_argument("-r", "--reverse", action="store_true", help="reverse sorting", default=False)
|
||||
parser.add_argument("-j", "--json", action="store_true", help="output in JSON format", default=False)
|
||||
parser.add_argument("-R", action="store", metavar="hash", help="transport identity hash of remote instance to get status from", default=None, type=str)
|
||||
parser.add_argument("-i", action="store", metavar="path", help="path to identity used for remote management", default=None, type=str)
|
||||
parser.add_argument("-w", action="store", metavar="seconds", type=float, help="timeout before giving up on remote queries", default=RNS.Transport.PATH_REQUEST_TIMEOUT)
|
||||
parser.add_argument("-d", "--discovered", action="store_true", help="list discovered interfaces", default=False)
|
||||
parser.add_argument("-D", action="store_true", help="show details and config entries for discovered interfaces", default=False)
|
||||
parser.add_argument("-m", "--monitor", action="store_true", help="continuously monitor status", default=False)
|
||||
parser.add_argument("-I", "--monitor-interval", action="store", metavar="seconds", type=float, help="refresh interval for monitor mode (default: 1)", default=1.0)
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0)
|
||||
|
||||
parser.add_argument("filter", nargs="?", default=None, help="only display interfaces with names including filter", type=str)
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.config:
|
||||
configarg = args.config
|
||||
else:
|
||||
configarg = None
|
||||
if args.config: configarg = args.config
|
||||
else: configarg = None
|
||||
|
||||
program_setup(
|
||||
configdir = configarg,
|
||||
dispall = args.all,
|
||||
verbosity=args.verbose,
|
||||
name_filter=args.filter,
|
||||
json=args.json,
|
||||
astats=args.announce_stats,
|
||||
lstats=args.link_stats,
|
||||
sorting=args.sort,
|
||||
sort_reverse=args.reverse,
|
||||
remote=args.R,
|
||||
management_identity=args.i,
|
||||
remote_timeout=args.w,
|
||||
must_exit=must_exit,
|
||||
rns_instance=rns_instance,
|
||||
traffic_totals=args.totals,
|
||||
)
|
||||
if args.monitor:
|
||||
if args.R: require_shared = False
|
||||
else: require_shared = True
|
||||
|
||||
try: reticulum = RNS.Reticulum(configdir=configarg, loglevel=3+args.verbose, require_shared_instance=require_shared)
|
||||
except Exception as e:
|
||||
print("No shared RNS instance available to get status from")
|
||||
exit(1)
|
||||
|
||||
while True:
|
||||
buffer = io.StringIO()
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = buffer
|
||||
|
||||
try:
|
||||
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json,
|
||||
astats=args.announce_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, remote=args.R,
|
||||
management_identity=args.i, remote_timeout=args.w, must_exit=False, rns_instance=reticulum, traffic_totals=args.totals,
|
||||
discovered_interfaces=args.discovered, config_entries=args.D)
|
||||
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
|
||||
output = buffer.getvalue()
|
||||
print("\033[H\033[2J", end="")
|
||||
print(output, end="", flush=True)
|
||||
|
||||
time.sleep(args.monitor_interval)
|
||||
|
||||
else:
|
||||
program_setup(configdir = configarg, dispall = args.all, verbosity=args.verbose, name_filter=args.filter, json=args.json,
|
||||
astats=args.announce_stats, lstats=args.link_stats, sorting=args.sort, sort_reverse=args.reverse, remote=args.R,
|
||||
management_identity=args.i, remote_timeout=args.w, must_exit=must_exit, rns_instance=rns_instance, traffic_totals=args.totals,
|
||||
discovered_interfaces=args.discovered, config_entries=args.D)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("")
|
||||
if must_exit:
|
||||
exit()
|
||||
else:
|
||||
return
|
||||
if must_exit: exit()
|
||||
else: return
|
||||
|
||||
def speed_str(num, suffix='bps'):
|
||||
units = ['','k','M','G','T','P','E','Z']
|
||||
|
||||
+31
-5
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2022 Mark Qvist / unsigned.io
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -11,8 +11,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -83,7 +91,25 @@ def listen(configdir, identitypath = None, verbosity = 0, quietness = 0, allowed
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
allowed_file_name = "allowed_identities"
|
||||
allowed_file = None
|
||||
if os.path.isfile(os.path.expanduser("/etc/rnx/"+allowed_file_name)):
|
||||
allowed_file = os.path.expanduser("/etc/rnx/"+allowed_file_name)
|
||||
elif os.path.isfile(os.path.expanduser("~/.config/rnx/"+allowed_file_name)):
|
||||
allowed_file = os.path.expanduser("~/.config/rnx/"+allowed_file_name)
|
||||
elif os.path.isfile(os.path.expanduser("~/.rnx/"+allowed_file_name)):
|
||||
allowed_file = os.path.expanduser("~/.rnx/"+allowed_file_name)
|
||||
if allowed_file != None:
|
||||
with open(allowed_file, "r") as af_handle:
|
||||
allowed_by_file = af_handle.read().replace("\r", "").split("\n")
|
||||
for allowed_ID in allowed_by_file:
|
||||
if len(allowed_ID) == (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2:
|
||||
allowed_identity_hashes.append(bytes.fromhex(allowed_ID))
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
exit(1)
|
||||
|
||||
if len(allowed_identity_hashes) < 1 and not disable_auth:
|
||||
print("Warning: No allowed identities configured, rncx will not accept any commands!")
|
||||
|
||||
|
||||
+26
-9
@@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9,8 +9,16 @@
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -36,6 +44,7 @@ from .Link import Link, RequestReceipt
|
||||
from .Channel import MessageBase
|
||||
from .Buffer import Buffer, RawChannelReader, RawChannelWriter
|
||||
from .Transport import Transport
|
||||
from .Discovery import InterfaceAnnouncer
|
||||
from .Destination import Destination
|
||||
from .Packet import Packet
|
||||
from .Packet import PacketReceipt
|
||||
@@ -49,6 +58,11 @@ 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"))]))
|
||||
|
||||
import importlib.util
|
||||
if importlib.util.find_spec("cython"): import cython; compiled = cython.compiled
|
||||
else: compiled = False
|
||||
|
||||
LOG_NONE = -1
|
||||
LOG_CRITICAL = 0
|
||||
LOG_ERROR = 1
|
||||
LOG_WARNING = 2
|
||||
@@ -114,6 +128,7 @@ def precise_timestamp_str(time_s):
|
||||
return datetime.datetime.now().strftime(logtimefmt_p)[:-3]
|
||||
|
||||
def log(msg, level=3, _override_destination = False, pt=False):
|
||||
if loglevel == LOG_NONE: return
|
||||
global _always_override_destination, compact_log_fmt
|
||||
msg = str(msg)
|
||||
if loglevel >= level:
|
||||
@@ -123,11 +138,14 @@ def log(msg, level=3, _override_destination = False, pt=False):
|
||||
if not compact_log_fmt:
|
||||
logstring = "["+timestamp_str(time.time())+"] "+loglevelname(level)+" "+msg
|
||||
else:
|
||||
logstring = "["+timestamp_str(time.time())+" "+msg
|
||||
logstring = "["+timestamp_str(time.time())+"] "+msg
|
||||
|
||||
with logging_lock:
|
||||
if (logdest == LOG_STDOUT or _always_override_destination or _override_destination):
|
||||
print(logstring)
|
||||
if not threading.main_thread().is_alive(): return
|
||||
else:
|
||||
try: print(logstring)
|
||||
except: pass
|
||||
|
||||
elif (logdest == LOG_FILE and logfile != None):
|
||||
try:
|
||||
@@ -362,13 +380,12 @@ def panic():
|
||||
os._exit(255)
|
||||
|
||||
exit_called = False
|
||||
def exit():
|
||||
def exit(code=0):
|
||||
global exit_called
|
||||
if not exit_called:
|
||||
exit_called = True
|
||||
print("")
|
||||
Reticulum.exit_handler()
|
||||
os._exit(0)
|
||||
os._exit(code)
|
||||
|
||||
class Profiler:
|
||||
_ran = False
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "0.9.2"
|
||||
__version__ = "1.1.0"
|
||||
|
||||
Vendored
+25
-34
@@ -19,8 +19,7 @@ import sys
|
||||
|
||||
from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE
|
||||
|
||||
import RNS.vendor.six as six
|
||||
__version__ = '5.0.6'
|
||||
__version__ = '5.0.9'
|
||||
|
||||
# imported lazily to avoid startup performance hit if it isn't used
|
||||
compiler = None
|
||||
@@ -121,10 +120,6 @@ OPTION_DEFAULTS = {
|
||||
'write_empty_values': False,
|
||||
}
|
||||
|
||||
# this could be replaced if six is used for compatibility, or there are no
|
||||
# more assertions about items being a string
|
||||
|
||||
|
||||
def getObj(s):
|
||||
global compiler
|
||||
if compiler is None:
|
||||
@@ -553,11 +548,11 @@ class Section(dict):
|
||||
"""Fetch the item and do string interpolation."""
|
||||
val = dict.__getitem__(self, key)
|
||||
if self.main.interpolation:
|
||||
if isinstance(val, six.string_types):
|
||||
if isinstance(val, str):
|
||||
return self._interpolate(key, val)
|
||||
if isinstance(val, list):
|
||||
def _check(entry):
|
||||
if isinstance(entry, six.string_types):
|
||||
if isinstance(entry, str):
|
||||
return self._interpolate(key, entry)
|
||||
return entry
|
||||
new = [_check(entry) for entry in val]
|
||||
@@ -580,7 +575,7 @@ class Section(dict):
|
||||
``unrepr`` must be set when setting a value to a dictionary, without
|
||||
creating a new sub-section.
|
||||
"""
|
||||
if not isinstance(key, six.string_types):
|
||||
if not isinstance(key, str):
|
||||
raise ValueError('The key "%s" is not a string.' % key)
|
||||
|
||||
# add the comment
|
||||
@@ -614,11 +609,11 @@ class Section(dict):
|
||||
if key not in self:
|
||||
self.scalars.append(key)
|
||||
if not self.main.stringify:
|
||||
if isinstance(value, six.string_types):
|
||||
if isinstance(value, str):
|
||||
pass
|
||||
elif isinstance(value, (list, tuple)):
|
||||
for entry in value:
|
||||
if not isinstance(entry, six.string_types):
|
||||
if not isinstance(entry, str):
|
||||
raise TypeError('Value is not a string "%s".' % entry)
|
||||
else:
|
||||
raise TypeError('Value is not a string "%s".' % value)
|
||||
@@ -959,7 +954,7 @@ class Section(dict):
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
if not isinstance(val, six.string_types):
|
||||
if not isinstance(val, str):
|
||||
# TODO: Why do we raise a KeyError here?
|
||||
raise KeyError()
|
||||
else:
|
||||
@@ -1230,7 +1225,7 @@ class ConfigObj(Section):
|
||||
|
||||
|
||||
def _load(self, infile, configspec):
|
||||
if isinstance(infile, six.string_types):
|
||||
if isinstance(infile, str):
|
||||
self.filename = infile
|
||||
if os.path.isfile(infile):
|
||||
with open(infile, 'rb') as h:
|
||||
@@ -1298,7 +1293,7 @@ class ConfigObj(Section):
|
||||
break
|
||||
break
|
||||
|
||||
assert all(isinstance(line, six.string_types) for line in content), repr(content)
|
||||
assert all(isinstance(line, str) for line in content), repr(content)
|
||||
content = [line.rstrip('\r\n') for line in content]
|
||||
|
||||
self._parse(content)
|
||||
@@ -1403,7 +1398,7 @@ class ConfigObj(Section):
|
||||
else:
|
||||
line = infile
|
||||
|
||||
if isinstance(line, six.text_type):
|
||||
if isinstance(line, str):
|
||||
# it's already decoded and there's no need to do anything
|
||||
# else, just use the _decode utility method to handle
|
||||
# listifying appropriately
|
||||
@@ -1448,7 +1443,7 @@ class ConfigObj(Section):
|
||||
|
||||
# No encoding specified - so we need to check for UTF8/UTF16
|
||||
for BOM, (encoding, final_encoding) in list(BOMS.items()):
|
||||
if not isinstance(line, six.binary_type) or not line.startswith(BOM):
|
||||
if not isinstance(line, bytes) or not line.startswith(BOM):
|
||||
# didn't specify a BOM, or it's not a bytestring
|
||||
continue
|
||||
else:
|
||||
@@ -1464,9 +1459,9 @@ class ConfigObj(Section):
|
||||
else:
|
||||
infile = newline
|
||||
# UTF-8
|
||||
if isinstance(infile, six.text_type):
|
||||
if isinstance(infile, str):
|
||||
return infile.splitlines(True)
|
||||
elif isinstance(infile, six.binary_type):
|
||||
elif isinstance(infile, bytes):
|
||||
return infile.decode('utf-8').splitlines(True)
|
||||
else:
|
||||
return self._decode(infile, 'utf-8')
|
||||
@@ -1474,12 +1469,8 @@ class ConfigObj(Section):
|
||||
return self._decode(infile, encoding)
|
||||
|
||||
|
||||
if six.PY2 and isinstance(line, str):
|
||||
# don't actually do any decoding, since we're on python 2 and
|
||||
# returning a bytestring is fine
|
||||
return self._decode(infile, None)
|
||||
# No BOM discovered and no encoding specified, default to UTF-8
|
||||
if isinstance(infile, six.binary_type):
|
||||
if isinstance(infile, bytes):
|
||||
return infile.decode('utf-8').splitlines(True)
|
||||
else:
|
||||
return self._decode(infile, 'utf-8')
|
||||
@@ -1487,7 +1478,7 @@ class ConfigObj(Section):
|
||||
|
||||
def _a_to_u(self, aString):
|
||||
"""Decode ASCII strings to unicode if a self.encoding is specified."""
|
||||
if isinstance(aString, six.binary_type) and self.encoding:
|
||||
if isinstance(aString, bytes) and self.encoding:
|
||||
return aString.decode(self.encoding)
|
||||
else:
|
||||
return aString
|
||||
@@ -1499,9 +1490,9 @@ class ConfigObj(Section):
|
||||
|
||||
if is a string, it also needs converting to a list.
|
||||
"""
|
||||
if isinstance(infile, six.string_types):
|
||||
if isinstance(infile, str):
|
||||
return infile.splitlines(True)
|
||||
if isinstance(infile, six.binary_type):
|
||||
if isinstance(infile, bytes):
|
||||
# NOTE: Could raise a ``UnicodeDecodeError``
|
||||
if encoding:
|
||||
return infile.decode(encoding).splitlines(True)
|
||||
@@ -1510,7 +1501,7 @@ class ConfigObj(Section):
|
||||
|
||||
if encoding:
|
||||
for i, line in enumerate(infile):
|
||||
if isinstance(line, six.binary_type):
|
||||
if isinstance(line, bytes):
|
||||
# NOTE: The isinstance test here handles mixed lists of unicode/string
|
||||
# NOTE: But the decode will break on any non-string values
|
||||
# NOTE: Or could raise a ``UnicodeDecodeError``
|
||||
@@ -1520,7 +1511,7 @@ class ConfigObj(Section):
|
||||
|
||||
def _decode_element(self, line):
|
||||
"""Decode element to unicode if necessary."""
|
||||
if isinstance(line, six.binary_type) and self.default_encoding:
|
||||
if isinstance(line, bytes) and self.default_encoding:
|
||||
return line.decode(self.default_encoding)
|
||||
else:
|
||||
return line
|
||||
@@ -1532,7 +1523,7 @@ class ConfigObj(Section):
|
||||
Used by ``stringify`` within validate, to turn non-string values
|
||||
into strings.
|
||||
"""
|
||||
if not isinstance(value, six.string_types):
|
||||
if not isinstance(value, str):
|
||||
# intentially 'str' because it's just whatever the "normal"
|
||||
# string type is for the python version we're dealing with
|
||||
return str(value)
|
||||
@@ -1786,7 +1777,7 @@ class ConfigObj(Section):
|
||||
return self._quote(value[0], multiline=False) + ','
|
||||
return ', '.join([self._quote(val, multiline=False)
|
||||
for val in value])
|
||||
if not isinstance(value, six.string_types):
|
||||
if not isinstance(value, str):
|
||||
if self.stringify:
|
||||
# intentially 'str' because it's just whatever the "normal"
|
||||
# string type is for the python version we're dealing with
|
||||
@@ -2111,7 +2102,7 @@ class ConfigObj(Section):
|
||||
if not output.endswith(newline):
|
||||
output += newline
|
||||
|
||||
if isinstance(output, six.binary_type):
|
||||
if isinstance(output, bytes):
|
||||
output_bytes = output
|
||||
else:
|
||||
output_bytes = output.encode(self.encoding or
|
||||
@@ -2170,7 +2161,7 @@ class ConfigObj(Section):
|
||||
if preserve_errors:
|
||||
# We do this once to remove a top level dependency on the validate module
|
||||
# Which makes importing configobj faster
|
||||
from validate import VdtMissingValue
|
||||
from configobj.validate import VdtMissingValue
|
||||
self._vdtMissingValue = VdtMissingValue
|
||||
|
||||
section = self
|
||||
@@ -2353,7 +2344,7 @@ class ConfigObj(Section):
|
||||
This method raises a ``ReloadError`` if the ConfigObj doesn't have
|
||||
a filename attribute pointing to a file.
|
||||
"""
|
||||
if not isinstance(self.filename, six.string_types):
|
||||
if not isinstance(self.filename, str):
|
||||
raise ReloadError()
|
||||
|
||||
filename = self.filename
|
||||
@@ -2480,4 +2471,4 @@ def get_extra_values(conf, _prepend=()):
|
||||
return out
|
||||
|
||||
|
||||
"""*A programming language is a medium of expression.* - Paul Graham"""
|
||||
"""*A programming language is a medium of expression.* - Paul Graham"""
|
||||
Vendored
-33
@@ -1,33 +0,0 @@
|
||||
# Copyright (c) 2014 Stefan C. Mueller
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from RNS.vendor.ifaddr._shared import Adapter, IP
|
||||
|
||||
if os.name == "nt":
|
||||
from RNS.vendor.ifaddr._win32 import get_adapters
|
||||
elif os.name == "posix":
|
||||
from RNS.vendor.ifaddr._posix import get_adapters
|
||||
else:
|
||||
raise RuntimeError("Unsupported Operating System: %s" % os.name)
|
||||
|
||||
__all__ = ['Adapter', 'IP', 'get_adapters']
|
||||
Vendored
-93
@@ -1,93 +0,0 @@
|
||||
# Copyright (c) 2014 Stefan C. Mueller
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
|
||||
import os
|
||||
import ctypes.util
|
||||
import ipaddress
|
||||
import collections
|
||||
import socket
|
||||
|
||||
from typing import Iterable, Optional
|
||||
|
||||
import RNS.vendor.ifaddr._shared as shared
|
||||
|
||||
class ifaddrs(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
ifaddrs._fields_ = [
|
||||
('ifa_next', ctypes.POINTER(ifaddrs)),
|
||||
('ifa_name', ctypes.c_char_p),
|
||||
('ifa_flags', ctypes.c_uint),
|
||||
('ifa_addr', ctypes.POINTER(shared.sockaddr)),
|
||||
('ifa_netmask', ctypes.POINTER(shared.sockaddr)),
|
||||
]
|
||||
|
||||
libc = ctypes.CDLL(ctypes.util.find_library("socket" if os.uname()[0] == "SunOS" else "c"), use_errno=True) # type: ignore
|
||||
|
||||
|
||||
def get_adapters(include_unconfigured: bool = False) -> Iterable[shared.Adapter]:
|
||||
|
||||
addr0 = addr = ctypes.POINTER(ifaddrs)()
|
||||
retval = libc.getifaddrs(ctypes.byref(addr))
|
||||
if retval != 0:
|
||||
eno = ctypes.get_errno()
|
||||
raise OSError(eno, os.strerror(eno))
|
||||
|
||||
ips = collections.OrderedDict()
|
||||
|
||||
def add_ip(adapter_name: str, ip: Optional[shared.IP]) -> None:
|
||||
if adapter_name not in ips:
|
||||
index = None # type: Optional[int]
|
||||
try:
|
||||
# Mypy errors on this when the Windows CI runs:
|
||||
# error: Module has no attribute "if_nametoindex"
|
||||
index = socket.if_nametoindex(adapter_name) # type: ignore
|
||||
except (OSError, AttributeError):
|
||||
pass
|
||||
ips[adapter_name] = shared.Adapter(adapter_name, adapter_name, [], index=index)
|
||||
if ip is not None:
|
||||
ips[adapter_name].ips.append(ip)
|
||||
|
||||
while addr:
|
||||
name = addr[0].ifa_name.decode(encoding='UTF-8')
|
||||
ip_addr = shared.sockaddr_to_ip(addr[0].ifa_addr)
|
||||
if ip_addr:
|
||||
if addr[0].ifa_netmask and not addr[0].ifa_netmask[0].sa_familiy:
|
||||
addr[0].ifa_netmask[0].sa_familiy = addr[0].ifa_addr[0].sa_familiy
|
||||
netmask = shared.sockaddr_to_ip(addr[0].ifa_netmask)
|
||||
if isinstance(netmask, tuple):
|
||||
netmaskStr = str(netmask[0])
|
||||
prefixlen = shared.ipv6_prefixlength(ipaddress.IPv6Address(netmaskStr))
|
||||
else:
|
||||
assert netmask is not None, f'sockaddr_to_ip({addr[0].ifa_netmask}) returned None'
|
||||
netmaskStr = str('0.0.0.0/' + netmask)
|
||||
prefixlen = ipaddress.IPv4Network(netmaskStr).prefixlen
|
||||
ip = shared.IP(ip_addr, prefixlen, name)
|
||||
add_ip(name, ip)
|
||||
else:
|
||||
if include_unconfigured:
|
||||
add_ip(name, None)
|
||||
addr = addr[0].ifa_next
|
||||
|
||||
libc.freeifaddrs(addr0)
|
||||
|
||||
return ips.values()
|
||||
Vendored
-198
@@ -1,198 +0,0 @@
|
||||
# Copyright (c) 2014 Stefan C. Mueller
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
|
||||
import ctypes
|
||||
import socket
|
||||
import ipaddress
|
||||
import platform
|
||||
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
class Adapter(object):
|
||||
"""
|
||||
Represents a network interface device controller (NIC), such as a
|
||||
network card. An adapter can have multiple IPs.
|
||||
|
||||
On Linux aliasing (multiple IPs per physical NIC) is implemented
|
||||
by creating 'virtual' adapters, each represented by an instance
|
||||
of this class. Each of those 'virtual' adapters can have both
|
||||
a IPv4 and an IPv6 IP address.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, nice_name: str, ips: List['IP'], index: Optional[int] = None) -> None:
|
||||
|
||||
#: Unique name that identifies the adapter in the system.
|
||||
#: On Linux this is of the form of `eth0` or `eth0:1`, on
|
||||
#: Windows it is a UUID in string representation, such as
|
||||
#: `{846EE342-7039-11DE-9D20-806E6F6E6963}`.
|
||||
self.name = name
|
||||
|
||||
#: Human readable name of the adpater. On Linux this
|
||||
#: is currently the same as :attr:`name`. On Windows
|
||||
#: this is the name of the device.
|
||||
self.nice_name = nice_name
|
||||
|
||||
#: List of :class:`ifaddr.IP` instances in the order they were
|
||||
#: reported by the system.
|
||||
self.ips = ips
|
||||
|
||||
#: Adapter index as used by some API (e.g. IPv6 multicast group join).
|
||||
self.index = index
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Adapter(name={name}, nice_name={nice_name}, ips={ips}, index={index})".format(
|
||||
name=repr(self.name), nice_name=repr(self.nice_name), ips=repr(self.ips), index=repr(self.index)
|
||||
)
|
||||
|
||||
|
||||
# Type of an IPv4 address (a string in "xxx.xxx.xxx.xxx" format)
|
||||
_IPv4Address = str
|
||||
|
||||
# Type of an IPv6 address (a three-tuple `(ip, flowinfo, scope_id)`)
|
||||
_IPv6Address = Tuple[str, int, int]
|
||||
|
||||
|
||||
class IP(object):
|
||||
"""
|
||||
Represents an IP address of an adapter.
|
||||
"""
|
||||
|
||||
def __init__(self, ip: Union[_IPv4Address, _IPv6Address], network_prefix: int, nice_name: str) -> None:
|
||||
|
||||
#: IP address. For IPv4 addresses this is a string in
|
||||
#: "xxx.xxx.xxx.xxx" format. For IPv6 addresses this
|
||||
#: is a three-tuple `(ip, flowinfo, scope_id)`, where
|
||||
#: `ip` is a string in the usual collon separated
|
||||
#: hex format.
|
||||
self.ip = ip
|
||||
|
||||
#: Number of bits of the IP that represent the
|
||||
#: network. For a `255.255.255.0` netmask, this
|
||||
#: number would be `24`.
|
||||
self.network_prefix = network_prefix
|
||||
|
||||
#: Human readable name for this IP.
|
||||
#: On Linux is this currently the same as the adapter name.
|
||||
#: On Windows this is the name of the network connection
|
||||
#: as configured in the system control panel.
|
||||
self.nice_name = nice_name
|
||||
|
||||
@property
|
||||
def is_IPv4(self) -> bool:
|
||||
"""
|
||||
Returns `True` if this IP is an IPv4 address and `False`
|
||||
if it is an IPv6 address.
|
||||
"""
|
||||
return not isinstance(self.ip, tuple)
|
||||
|
||||
@property
|
||||
def is_IPv6(self) -> bool:
|
||||
"""
|
||||
Returns `True` if this IP is an IPv6 address and `False`
|
||||
if it is an IPv4 address.
|
||||
"""
|
||||
return isinstance(self.ip, tuple)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "IP(ip={ip}, network_prefix={network_prefix}, nice_name={nice_name})".format(
|
||||
ip=repr(self.ip), network_prefix=repr(self.network_prefix), nice_name=repr(self.nice_name)
|
||||
)
|
||||
|
||||
|
||||
if platform.system() == "Darwin" or "BSD" in platform.system():
|
||||
|
||||
# BSD derived systems use marginally different structures
|
||||
# than either Linux or Windows.
|
||||
# I still keep it in `shared` since we can use
|
||||
# both structures equally.
|
||||
|
||||
class sockaddr(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('sa_len', ctypes.c_uint8),
|
||||
('sa_familiy', ctypes.c_uint8),
|
||||
('sa_data', ctypes.c_uint8 * 14),
|
||||
]
|
||||
|
||||
class sockaddr_in(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('sa_len', ctypes.c_uint8),
|
||||
('sa_familiy', ctypes.c_uint8),
|
||||
('sin_port', ctypes.c_uint16),
|
||||
('sin_addr', ctypes.c_uint8 * 4),
|
||||
('sin_zero', ctypes.c_uint8 * 8),
|
||||
]
|
||||
|
||||
class sockaddr_in6(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('sa_len', ctypes.c_uint8),
|
||||
('sa_familiy', ctypes.c_uint8),
|
||||
('sin6_port', ctypes.c_uint16),
|
||||
('sin6_flowinfo', ctypes.c_uint32),
|
||||
('sin6_addr', ctypes.c_uint8 * 16),
|
||||
('sin6_scope_id', ctypes.c_uint32),
|
||||
]
|
||||
|
||||
else:
|
||||
|
||||
class sockaddr(ctypes.Structure): # type: ignore
|
||||
_fields_ = [('sa_familiy', ctypes.c_uint16), ('sa_data', ctypes.c_uint8 * 14)]
|
||||
|
||||
class sockaddr_in(ctypes.Structure): # type: ignore
|
||||
_fields_ = [
|
||||
('sin_familiy', ctypes.c_uint16),
|
||||
('sin_port', ctypes.c_uint16),
|
||||
('sin_addr', ctypes.c_uint8 * 4),
|
||||
('sin_zero', ctypes.c_uint8 * 8),
|
||||
]
|
||||
|
||||
class sockaddr_in6(ctypes.Structure): # type: ignore
|
||||
_fields_ = [
|
||||
('sin6_familiy', ctypes.c_uint16),
|
||||
('sin6_port', ctypes.c_uint16),
|
||||
('sin6_flowinfo', ctypes.c_uint32),
|
||||
('sin6_addr', ctypes.c_uint8 * 16),
|
||||
('sin6_scope_id', ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
def sockaddr_to_ip(sockaddr_ptr: 'ctypes.pointer[sockaddr]') -> Optional[Union[_IPv4Address, _IPv6Address]]:
|
||||
if sockaddr_ptr:
|
||||
if sockaddr_ptr[0].sa_familiy == socket.AF_INET:
|
||||
ipv4 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in))
|
||||
ippacked = bytes(bytearray(ipv4[0].sin_addr))
|
||||
ip = str(ipaddress.ip_address(ippacked))
|
||||
return ip
|
||||
elif sockaddr_ptr[0].sa_familiy == socket.AF_INET6:
|
||||
ipv6 = ctypes.cast(sockaddr_ptr, ctypes.POINTER(sockaddr_in6))
|
||||
flowinfo = ipv6[0].sin6_flowinfo
|
||||
ippacked = bytes(bytearray(ipv6[0].sin6_addr))
|
||||
ip = str(ipaddress.ip_address(ippacked))
|
||||
scope_id = ipv6[0].sin6_scope_id
|
||||
return (ip, flowinfo, scope_id)
|
||||
return None
|
||||
|
||||
|
||||
def ipv6_prefixlength(address: ipaddress.IPv6Address) -> int:
|
||||
prefix_length = 0
|
||||
for i in range(address.max_prefixlen):
|
||||
if int(address) >> i & 1:
|
||||
prefix_length = prefix_length + 1
|
||||
return prefix_length
|
||||
Vendored
-145
@@ -1,145 +0,0 @@
|
||||
# Copyright (c) 2014 Stefan C. Mueller
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
from typing import Iterable, List
|
||||
|
||||
import RNS.vendor.ifaddr._shared as shared
|
||||
|
||||
NO_ERROR = 0
|
||||
ERROR_BUFFER_OVERFLOW = 111
|
||||
MAX_ADAPTER_NAME_LENGTH = 256
|
||||
MAX_ADAPTER_DESCRIPTION_LENGTH = 128
|
||||
MAX_ADAPTER_ADDRESS_LENGTH = 8
|
||||
AF_UNSPEC = 0
|
||||
|
||||
|
||||
class SOCKET_ADDRESS(ctypes.Structure):
|
||||
_fields_ = [('lpSockaddr', ctypes.POINTER(shared.sockaddr)), ('iSockaddrLength', wintypes.INT)]
|
||||
|
||||
|
||||
class IP_ADAPTER_UNICAST_ADDRESS(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
IP_ADAPTER_UNICAST_ADDRESS._fields_ = [
|
||||
('Length', wintypes.ULONG),
|
||||
('Flags', wintypes.DWORD),
|
||||
('Next', ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)),
|
||||
('Address', SOCKET_ADDRESS),
|
||||
('PrefixOrigin', ctypes.c_uint),
|
||||
('SuffixOrigin', ctypes.c_uint),
|
||||
('DadState', ctypes.c_uint),
|
||||
('ValidLifetime', wintypes.ULONG),
|
||||
('PreferredLifetime', wintypes.ULONG),
|
||||
('LeaseLifetime', wintypes.ULONG),
|
||||
('OnLinkPrefixLength', ctypes.c_uint8),
|
||||
]
|
||||
|
||||
|
||||
class IP_ADAPTER_ADDRESSES(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
IP_ADAPTER_ADDRESSES._fields_ = [
|
||||
('Length', wintypes.ULONG),
|
||||
('IfIndex', wintypes.DWORD),
|
||||
('Next', ctypes.POINTER(IP_ADAPTER_ADDRESSES)),
|
||||
('AdapterName', ctypes.c_char_p),
|
||||
('FirstUnicastAddress', ctypes.POINTER(IP_ADAPTER_UNICAST_ADDRESS)),
|
||||
('FirstAnycastAddress', ctypes.c_void_p),
|
||||
('FirstMulticastAddress', ctypes.c_void_p),
|
||||
('FirstDnsServerAddress', ctypes.c_void_p),
|
||||
('DnsSuffix', ctypes.c_wchar_p),
|
||||
('Description', ctypes.c_wchar_p),
|
||||
('FriendlyName', ctypes.c_wchar_p),
|
||||
]
|
||||
|
||||
|
||||
iphlpapi = ctypes.windll.LoadLibrary("Iphlpapi") # type: ignore
|
||||
|
||||
|
||||
def enumerate_interfaces_of_adapter(
|
||||
nice_name: str, address: IP_ADAPTER_UNICAST_ADDRESS
|
||||
) -> Iterable[shared.IP]:
|
||||
|
||||
# Iterate through linked list and fill list
|
||||
addresses = [] # type: List[IP_ADAPTER_UNICAST_ADDRESS]
|
||||
while True:
|
||||
addresses.append(address)
|
||||
if not address.Next:
|
||||
break
|
||||
address = address.Next[0]
|
||||
|
||||
for address in addresses:
|
||||
ip = shared.sockaddr_to_ip(address.Address.lpSockaddr)
|
||||
assert ip is not None, f'sockaddr_to_ip({address.Address.lpSockaddr}) returned None'
|
||||
network_prefix = address.OnLinkPrefixLength
|
||||
yield shared.IP(ip, network_prefix, nice_name)
|
||||
|
||||
|
||||
def get_adapters(include_unconfigured: bool = False) -> Iterable[shared.Adapter]:
|
||||
|
||||
# Call GetAdaptersAddresses() with error and buffer size handling
|
||||
|
||||
addressbuffersize = wintypes.ULONG(15 * 1024)
|
||||
retval = ERROR_BUFFER_OVERFLOW
|
||||
while retval == ERROR_BUFFER_OVERFLOW:
|
||||
addressbuffer = ctypes.create_string_buffer(addressbuffersize.value)
|
||||
retval = iphlpapi.GetAdaptersAddresses(
|
||||
wintypes.ULONG(AF_UNSPEC),
|
||||
wintypes.ULONG(0),
|
||||
None,
|
||||
ctypes.byref(addressbuffer),
|
||||
ctypes.byref(addressbuffersize),
|
||||
)
|
||||
if retval != NO_ERROR:
|
||||
raise ctypes.WinError() # type: ignore
|
||||
|
||||
# Iterate through adapters fill array
|
||||
address_infos = [] # type: List[IP_ADAPTER_ADDRESSES]
|
||||
address_info = IP_ADAPTER_ADDRESSES.from_buffer(addressbuffer)
|
||||
while True:
|
||||
address_infos.append(address_info)
|
||||
if not address_info.Next:
|
||||
break
|
||||
address_info = address_info.Next[0]
|
||||
|
||||
# Iterate through unicast addresses
|
||||
result = [] # type: List[shared.Adapter]
|
||||
for adapter_info in address_infos:
|
||||
|
||||
# We don't expect non-ascii characters here, so encoding shouldn't matter
|
||||
name = adapter_info.AdapterName.decode()
|
||||
nice_name = adapter_info.Description
|
||||
index = adapter_info.IfIndex
|
||||
|
||||
if adapter_info.FirstUnicastAddress:
|
||||
ips = enumerate_interfaces_of_adapter(
|
||||
adapter_info.FriendlyName, adapter_info.FirstUnicastAddress[0]
|
||||
)
|
||||
ips = list(ips)
|
||||
result.append(shared.Adapter(name, nice_name, ips, index=index))
|
||||
elif include_unconfigured:
|
||||
result.append(shared.Adapter(name, nice_name, [], index=index))
|
||||
|
||||
return result
|
||||
Vendored
-57
@@ -1,57 +0,0 @@
|
||||
import ipaddress
|
||||
import RNS.vendor.ifaddr
|
||||
import socket
|
||||
|
||||
from typing import List
|
||||
|
||||
AF_INET6 = socket.AF_INET6.value
|
||||
AF_INET = socket.AF_INET.value
|
||||
|
||||
def interfaces() -> List[str]:
|
||||
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
|
||||
return [a.name for a in adapters]
|
||||
|
||||
def interface_names_to_indexes() -> dict:
|
||||
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
|
||||
results = {}
|
||||
for adapter in adapters:
|
||||
results[adapter.name] = adapter.index
|
||||
return results
|
||||
|
||||
def interface_name_to_nice_name(ifname) -> str:
|
||||
try:
|
||||
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
|
||||
for adapter in adapters:
|
||||
if adapter.name == ifname:
|
||||
if hasattr(adapter, "nice_name"):
|
||||
return adapter.nice_name
|
||||
except:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def ifaddresses(ifname) -> dict:
|
||||
adapters = RNS.vendor.ifaddr.get_adapters(include_unconfigured=True)
|
||||
ifa = {}
|
||||
for a in adapters:
|
||||
if a.name == ifname:
|
||||
ipv4s = []
|
||||
ipv6s = []
|
||||
for ip in a.ips:
|
||||
t = {}
|
||||
if ip.is_IPv4:
|
||||
net = ipaddress.ip_network(str(ip.ip)+"/"+str(ip.network_prefix), strict=False)
|
||||
t["addr"] = ip.ip
|
||||
t["prefix"] = ip.network_prefix
|
||||
t["broadcast"] = str(net.broadcast_address)
|
||||
ipv4s.append(t)
|
||||
if ip.is_IPv6:
|
||||
t["addr"] = ip.ip[0]
|
||||
ipv6s.append(t)
|
||||
|
||||
if len(ipv4s) > 0:
|
||||
ifa[AF_INET] = ipv4s
|
||||
if len(ipv6s) > 0:
|
||||
ifa[AF_INET6] = ipv6s
|
||||
|
||||
return ifa
|
||||
Vendored
Vendored
+51
-26
@@ -1,42 +1,69 @@
|
||||
# Reticulum License
|
||||
#
|
||||
# Copyright (c) 2016-2025 Mark Qvist
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# - The Software shall not be used in any kind of system which includes amongst
|
||||
# its functions the ability to purposefully do harm to human beings.
|
||||
#
|
||||
# - The Software shall not be used, directly or indirectly, in the creation of
|
||||
# an artificial intelligence, machine learning or language model training
|
||||
# dataset, including but not limited to any use that contributes to the
|
||||
# training or development of such a model or algorithm.
|
||||
#
|
||||
# - The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
def get_platform():
|
||||
from os import environ
|
||||
if "ANDROID_ARGUMENT" in environ:
|
||||
return "android"
|
||||
elif "ANDROID_ROOT" in environ:
|
||||
return "android"
|
||||
if "ANDROID_ARGUMENT" in environ: return "android"
|
||||
elif "ANDROID_ROOT" in environ: return "android"
|
||||
else:
|
||||
import sys
|
||||
return sys.platform
|
||||
|
||||
def is_linux():
|
||||
if get_platform() == "linux":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
if get_platform() == "linux": return True
|
||||
else: return False
|
||||
|
||||
def is_darwin():
|
||||
if get_platform() == "darwin":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
if get_platform() == "darwin": return True
|
||||
else: return False
|
||||
|
||||
def is_android():
|
||||
if get_platform() == "android":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
if get_platform() == "android": return True
|
||||
else: return False
|
||||
|
||||
def is_windows():
|
||||
if str(get_platform()).startswith("win"):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
if str(get_platform()).startswith("win"): return True
|
||||
else: return False
|
||||
|
||||
def use_epoll():
|
||||
if is_linux() or is_android(): return True
|
||||
else: return False
|
||||
|
||||
def use_af_unix():
|
||||
if is_linux() or is_android(): return True
|
||||
else: return False
|
||||
|
||||
def platform_checks():
|
||||
if is_windows():
|
||||
import sys
|
||||
if sys.version_info.major >= 3 and sys.version_info.minor >= 8:
|
||||
pass
|
||||
if sys.version_info.major >= 3 and sys.version_info.minor >= 8: pass
|
||||
else:
|
||||
import RNS
|
||||
RNS.log("On Windows, Reticulum requires Python 3.8 or higher.", RNS.LOG_ERROR)
|
||||
@@ -45,7 +72,5 @@ def platform_checks():
|
||||
|
||||
def cryptography_old_api():
|
||||
import cryptography
|
||||
if cryptography.__version__ == "2.8":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
if cryptography.__version__ == "2.8": return True
|
||||
else: return False
|
||||
|
||||
Vendored
-998
@@ -1,998 +0,0 @@
|
||||
# Copyright (c) 2010-2020 Benjamin Peterson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
"""Utilities for writing code that runs on Python 2 and 3"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
import operator
|
||||
import sys
|
||||
import types
|
||||
|
||||
__author__ = "Benjamin Peterson <benjamin@python.org>"
|
||||
__version__ = "1.16.0"
|
||||
|
||||
|
||||
# Useful for very coarse version differentiation.
|
||||
PY2 = sys.version_info[0] == 2
|
||||
PY3 = sys.version_info[0] == 3
|
||||
PY34 = sys.version_info[0:2] >= (3, 4)
|
||||
|
||||
if PY3:
|
||||
string_types = str,
|
||||
integer_types = int,
|
||||
class_types = type,
|
||||
text_type = str
|
||||
binary_type = bytes
|
||||
|
||||
MAXSIZE = sys.maxsize
|
||||
else:
|
||||
string_types = basestring,
|
||||
integer_types = (int, long)
|
||||
class_types = (type, types.ClassType)
|
||||
text_type = unicode
|
||||
binary_type = str
|
||||
|
||||
if sys.platform.startswith("java"):
|
||||
# Jython always uses 32 bits.
|
||||
MAXSIZE = int((1 << 31) - 1)
|
||||
else:
|
||||
# It's possible to have sizeof(long) != sizeof(Py_ssize_t).
|
||||
class X(object):
|
||||
|
||||
def __len__(self):
|
||||
return 1 << 31
|
||||
try:
|
||||
len(X())
|
||||
except OverflowError:
|
||||
# 32-bit
|
||||
MAXSIZE = int((1 << 31) - 1)
|
||||
else:
|
||||
# 64-bit
|
||||
MAXSIZE = int((1 << 63) - 1)
|
||||
del X
|
||||
|
||||
if PY34:
|
||||
from importlib.util import spec_from_loader
|
||||
else:
|
||||
spec_from_loader = None
|
||||
|
||||
|
||||
def _add_doc(func, doc):
|
||||
"""Add documentation to a function."""
|
||||
func.__doc__ = doc
|
||||
|
||||
|
||||
def _import_module(name):
|
||||
"""Import module, returning the module after the last dot."""
|
||||
__import__(name)
|
||||
return sys.modules[name]
|
||||
|
||||
|
||||
class _LazyDescr(object):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj, tp):
|
||||
result = self._resolve()
|
||||
setattr(obj, self.name, result) # Invokes __set__.
|
||||
try:
|
||||
# This is a bit ugly, but it avoids running this again by
|
||||
# removing this descriptor.
|
||||
delattr(obj.__class__, self.name)
|
||||
except AttributeError:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
class MovedModule(_LazyDescr):
|
||||
|
||||
def __init__(self, name, old, new=None):
|
||||
super(MovedModule, self).__init__(name)
|
||||
if PY3:
|
||||
if new is None:
|
||||
new = name
|
||||
self.mod = new
|
||||
else:
|
||||
self.mod = old
|
||||
|
||||
def _resolve(self):
|
||||
return _import_module(self.mod)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
_module = self._resolve()
|
||||
value = getattr(_module, attr)
|
||||
setattr(self, attr, value)
|
||||
return value
|
||||
|
||||
|
||||
class _LazyModule(types.ModuleType):
|
||||
|
||||
def __init__(self, name):
|
||||
super(_LazyModule, self).__init__(name)
|
||||
self.__doc__ = self.__class__.__doc__
|
||||
|
||||
def __dir__(self):
|
||||
attrs = ["__doc__", "__name__"]
|
||||
attrs += [attr.name for attr in self._moved_attributes]
|
||||
return attrs
|
||||
|
||||
# Subclasses should override this
|
||||
_moved_attributes = []
|
||||
|
||||
|
||||
class MovedAttribute(_LazyDescr):
|
||||
|
||||
def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
|
||||
super(MovedAttribute, self).__init__(name)
|
||||
if PY3:
|
||||
if new_mod is None:
|
||||
new_mod = name
|
||||
self.mod = new_mod
|
||||
if new_attr is None:
|
||||
if old_attr is None:
|
||||
new_attr = name
|
||||
else:
|
||||
new_attr = old_attr
|
||||
self.attr = new_attr
|
||||
else:
|
||||
self.mod = old_mod
|
||||
if old_attr is None:
|
||||
old_attr = name
|
||||
self.attr = old_attr
|
||||
|
||||
def _resolve(self):
|
||||
module = _import_module(self.mod)
|
||||
return getattr(module, self.attr)
|
||||
|
||||
|
||||
class _SixMetaPathImporter(object):
|
||||
|
||||
"""
|
||||
A meta path importer to import six.moves and its submodules.
|
||||
|
||||
This class implements a PEP302 finder and loader. It should be compatible
|
||||
with Python 2.5 and all existing versions of Python3
|
||||
"""
|
||||
|
||||
def __init__(self, six_module_name):
|
||||
self.name = six_module_name
|
||||
self.known_modules = {}
|
||||
|
||||
def _add_module(self, mod, *fullnames):
|
||||
for fullname in fullnames:
|
||||
self.known_modules[self.name + "." + fullname] = mod
|
||||
|
||||
def _get_module(self, fullname):
|
||||
return self.known_modules[self.name + "." + fullname]
|
||||
|
||||
def find_module(self, fullname, path=None):
|
||||
if fullname in self.known_modules:
|
||||
return self
|
||||
return None
|
||||
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if fullname in self.known_modules:
|
||||
return spec_from_loader(fullname, self)
|
||||
return None
|
||||
|
||||
def __get_module(self, fullname):
|
||||
try:
|
||||
return self.known_modules[fullname]
|
||||
except KeyError:
|
||||
raise ImportError("This loader does not know module " + fullname)
|
||||
|
||||
def load_module(self, fullname):
|
||||
try:
|
||||
# in case of a reload
|
||||
return sys.modules[fullname]
|
||||
except KeyError:
|
||||
pass
|
||||
mod = self.__get_module(fullname)
|
||||
if isinstance(mod, MovedModule):
|
||||
mod = mod._resolve()
|
||||
else:
|
||||
mod.__loader__ = self
|
||||
sys.modules[fullname] = mod
|
||||
return mod
|
||||
|
||||
def is_package(self, fullname):
|
||||
"""
|
||||
Return true, if the named module is a package.
|
||||
|
||||
We need this method to get correct spec objects with
|
||||
Python 3.4 (see PEP451)
|
||||
"""
|
||||
return hasattr(self.__get_module(fullname), "__path__")
|
||||
|
||||
def get_code(self, fullname):
|
||||
"""Return None
|
||||
|
||||
Required, if is_package is implemented"""
|
||||
self.__get_module(fullname) # eventually raises ImportError
|
||||
return None
|
||||
get_source = get_code # same as get_code
|
||||
|
||||
def create_module(self, spec):
|
||||
return self.load_module(spec.name)
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
_importer = _SixMetaPathImporter(__name__)
|
||||
|
||||
|
||||
class _MovedItems(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects"""
|
||||
__path__ = [] # mark as package
|
||||
|
||||
|
||||
_moved_attributes = [
|
||||
MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
|
||||
MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
|
||||
MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
|
||||
MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
|
||||
MovedAttribute("intern", "__builtin__", "sys"),
|
||||
MovedAttribute("map", "itertools", "builtins", "imap", "map"),
|
||||
MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
|
||||
MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
|
||||
MovedAttribute("getoutput", "commands", "subprocess"),
|
||||
MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
|
||||
MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
|
||||
MovedAttribute("reduce", "__builtin__", "functools"),
|
||||
MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
|
||||
MovedAttribute("StringIO", "StringIO", "io"),
|
||||
MovedAttribute("UserDict", "UserDict", "collections"),
|
||||
MovedAttribute("UserList", "UserList", "collections"),
|
||||
MovedAttribute("UserString", "UserString", "collections"),
|
||||
MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
|
||||
MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
|
||||
MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
|
||||
MovedModule("builtins", "__builtin__"),
|
||||
MovedModule("configparser", "ConfigParser"),
|
||||
MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"),
|
||||
MovedModule("copyreg", "copy_reg"),
|
||||
MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
|
||||
MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"),
|
||||
MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"),
|
||||
MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
|
||||
MovedModule("http_cookies", "Cookie", "http.cookies"),
|
||||
MovedModule("html_entities", "htmlentitydefs", "html.entities"),
|
||||
MovedModule("html_parser", "HTMLParser", "html.parser"),
|
||||
MovedModule("http_client", "httplib", "http.client"),
|
||||
MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
|
||||
MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"),
|
||||
MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
|
||||
MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
|
||||
MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
|
||||
MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
|
||||
MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
|
||||
MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
|
||||
MovedModule("cPickle", "cPickle", "pickle"),
|
||||
MovedModule("queue", "Queue"),
|
||||
MovedModule("reprlib", "repr"),
|
||||
MovedModule("socketserver", "SocketServer"),
|
||||
MovedModule("_thread", "thread", "_thread"),
|
||||
MovedModule("tkinter", "Tkinter"),
|
||||
MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
|
||||
MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
|
||||
MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
|
||||
MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
|
||||
MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
|
||||
MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
|
||||
MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
|
||||
MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
|
||||
MovedModule("tkinter_colorchooser", "tkColorChooser",
|
||||
"tkinter.colorchooser"),
|
||||
MovedModule("tkinter_commondialog", "tkCommonDialog",
|
||||
"tkinter.commondialog"),
|
||||
MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
|
||||
MovedModule("tkinter_font", "tkFont", "tkinter.font"),
|
||||
MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
|
||||
MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
|
||||
"tkinter.simpledialog"),
|
||||
MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
|
||||
MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
|
||||
MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
|
||||
MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
|
||||
MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
|
||||
MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
|
||||
]
|
||||
# Add windows specific modules.
|
||||
if sys.platform == "win32":
|
||||
_moved_attributes += [
|
||||
MovedModule("winreg", "_winreg"),
|
||||
]
|
||||
|
||||
for attr in _moved_attributes:
|
||||
setattr(_MovedItems, attr.name, attr)
|
||||
if isinstance(attr, MovedModule):
|
||||
_importer._add_module(attr, "moves." + attr.name)
|
||||
del attr
|
||||
|
||||
_MovedItems._moved_attributes = _moved_attributes
|
||||
|
||||
moves = _MovedItems(__name__ + ".moves")
|
||||
_importer._add_module(moves, "moves")
|
||||
|
||||
|
||||
class Module_six_moves_urllib_parse(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects in six.moves.urllib_parse"""
|
||||
|
||||
|
||||
_urllib_parse_moved_attributes = [
|
||||
MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urljoin", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urlparse", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("quote", "urllib", "urllib.parse"),
|
||||
MovedAttribute("quote_plus", "urllib", "urllib.parse"),
|
||||
MovedAttribute("unquote", "urllib", "urllib.parse"),
|
||||
MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
|
||||
MovedAttribute("unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes"),
|
||||
MovedAttribute("urlencode", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splitquery", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splittag", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splituser", "urllib", "urllib.parse"),
|
||||
MovedAttribute("splitvalue", "urllib", "urllib.parse"),
|
||||
MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("uses_params", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("uses_query", "urlparse", "urllib.parse"),
|
||||
MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
|
||||
]
|
||||
for attr in _urllib_parse_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_parse, attr.name, attr)
|
||||
del attr
|
||||
|
||||
Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
|
||||
"moves.urllib_parse", "moves.urllib.parse")
|
||||
|
||||
|
||||
class Module_six_moves_urllib_error(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects in six.moves.urllib_error"""
|
||||
|
||||
|
||||
_urllib_error_moved_attributes = [
|
||||
MovedAttribute("URLError", "urllib2", "urllib.error"),
|
||||
MovedAttribute("HTTPError", "urllib2", "urllib.error"),
|
||||
MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
|
||||
]
|
||||
for attr in _urllib_error_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_error, attr.name, attr)
|
||||
del attr
|
||||
|
||||
Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
|
||||
"moves.urllib_error", "moves.urllib.error")
|
||||
|
||||
|
||||
class Module_six_moves_urllib_request(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects in six.moves.urllib_request"""
|
||||
|
||||
|
||||
_urllib_request_moved_attributes = [
|
||||
MovedAttribute("urlopen", "urllib2", "urllib.request"),
|
||||
MovedAttribute("install_opener", "urllib2", "urllib.request"),
|
||||
MovedAttribute("build_opener", "urllib2", "urllib.request"),
|
||||
MovedAttribute("pathname2url", "urllib", "urllib.request"),
|
||||
MovedAttribute("url2pathname", "urllib", "urllib.request"),
|
||||
MovedAttribute("getproxies", "urllib", "urllib.request"),
|
||||
MovedAttribute("Request", "urllib2", "urllib.request"),
|
||||
MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
|
||||
MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
|
||||
MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("FileHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
|
||||
MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
|
||||
MovedAttribute("urlretrieve", "urllib", "urllib.request"),
|
||||
MovedAttribute("urlcleanup", "urllib", "urllib.request"),
|
||||
MovedAttribute("URLopener", "urllib", "urllib.request"),
|
||||
MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
|
||||
MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
|
||||
MovedAttribute("parse_http_list", "urllib2", "urllib.request"),
|
||||
MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"),
|
||||
]
|
||||
for attr in _urllib_request_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_request, attr.name, attr)
|
||||
del attr
|
||||
|
||||
Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
|
||||
"moves.urllib_request", "moves.urllib.request")
|
||||
|
||||
|
||||
class Module_six_moves_urllib_response(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects in six.moves.urllib_response"""
|
||||
|
||||
|
||||
_urllib_response_moved_attributes = [
|
||||
MovedAttribute("addbase", "urllib", "urllib.response"),
|
||||
MovedAttribute("addclosehook", "urllib", "urllib.response"),
|
||||
MovedAttribute("addinfo", "urllib", "urllib.response"),
|
||||
MovedAttribute("addinfourl", "urllib", "urllib.response"),
|
||||
]
|
||||
for attr in _urllib_response_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_response, attr.name, attr)
|
||||
del attr
|
||||
|
||||
Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
|
||||
"moves.urllib_response", "moves.urllib.response")
|
||||
|
||||
|
||||
class Module_six_moves_urllib_robotparser(_LazyModule):
|
||||
|
||||
"""Lazy loading of moved objects in six.moves.urllib_robotparser"""
|
||||
|
||||
|
||||
_urllib_robotparser_moved_attributes = [
|
||||
MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
|
||||
]
|
||||
for attr in _urllib_robotparser_moved_attributes:
|
||||
setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
|
||||
del attr
|
||||
|
||||
Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
|
||||
"moves.urllib_robotparser", "moves.urllib.robotparser")
|
||||
|
||||
|
||||
class Module_six_moves_urllib(types.ModuleType):
|
||||
|
||||
"""Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
|
||||
__path__ = [] # mark as package
|
||||
parse = _importer._get_module("moves.urllib_parse")
|
||||
error = _importer._get_module("moves.urllib_error")
|
||||
request = _importer._get_module("moves.urllib_request")
|
||||
response = _importer._get_module("moves.urllib_response")
|
||||
robotparser = _importer._get_module("moves.urllib_robotparser")
|
||||
|
||||
def __dir__(self):
|
||||
return ['parse', 'error', 'request', 'response', 'robotparser']
|
||||
|
||||
_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
|
||||
"moves.urllib")
|
||||
|
||||
|
||||
def add_move(move):
|
||||
"""Add an item to six.moves."""
|
||||
setattr(_MovedItems, move.name, move)
|
||||
|
||||
|
||||
def remove_move(name):
|
||||
"""Remove item from six.moves."""
|
||||
try:
|
||||
delattr(_MovedItems, name)
|
||||
except AttributeError:
|
||||
try:
|
||||
del moves.__dict__[name]
|
||||
except KeyError:
|
||||
raise AttributeError("no such move, %r" % (name,))
|
||||
|
||||
|
||||
if PY3:
|
||||
_meth_func = "__func__"
|
||||
_meth_self = "__self__"
|
||||
|
||||
_func_closure = "__closure__"
|
||||
_func_code = "__code__"
|
||||
_func_defaults = "__defaults__"
|
||||
_func_globals = "__globals__"
|
||||
else:
|
||||
_meth_func = "im_func"
|
||||
_meth_self = "im_self"
|
||||
|
||||
_func_closure = "func_closure"
|
||||
_func_code = "func_code"
|
||||
_func_defaults = "func_defaults"
|
||||
_func_globals = "func_globals"
|
||||
|
||||
|
||||
try:
|
||||
advance_iterator = next
|
||||
except NameError:
|
||||
def advance_iterator(it):
|
||||
return it.next()
|
||||
next = advance_iterator
|
||||
|
||||
|
||||
try:
|
||||
callable = callable
|
||||
except NameError:
|
||||
def callable(obj):
|
||||
return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
|
||||
|
||||
|
||||
if PY3:
|
||||
def get_unbound_function(unbound):
|
||||
return unbound
|
||||
|
||||
create_bound_method = types.MethodType
|
||||
|
||||
def create_unbound_method(func, cls):
|
||||
return func
|
||||
|
||||
Iterator = object
|
||||
else:
|
||||
def get_unbound_function(unbound):
|
||||
return unbound.im_func
|
||||
|
||||
def create_bound_method(func, obj):
|
||||
return types.MethodType(func, obj, obj.__class__)
|
||||
|
||||
def create_unbound_method(func, cls):
|
||||
return types.MethodType(func, None, cls)
|
||||
|
||||
class Iterator(object):
|
||||
|
||||
def next(self):
|
||||
return type(self).__next__(self)
|
||||
|
||||
callable = callable
|
||||
_add_doc(get_unbound_function,
|
||||
"""Get the function out of a possibly unbound function""")
|
||||
|
||||
|
||||
get_method_function = operator.attrgetter(_meth_func)
|
||||
get_method_self = operator.attrgetter(_meth_self)
|
||||
get_function_closure = operator.attrgetter(_func_closure)
|
||||
get_function_code = operator.attrgetter(_func_code)
|
||||
get_function_defaults = operator.attrgetter(_func_defaults)
|
||||
get_function_globals = operator.attrgetter(_func_globals)
|
||||
|
||||
|
||||
if PY3:
|
||||
def iterkeys(d, **kw):
|
||||
return iter(d.keys(**kw))
|
||||
|
||||
def itervalues(d, **kw):
|
||||
return iter(d.values(**kw))
|
||||
|
||||
def iteritems(d, **kw):
|
||||
return iter(d.items(**kw))
|
||||
|
||||
def iterlists(d, **kw):
|
||||
return iter(d.lists(**kw))
|
||||
|
||||
viewkeys = operator.methodcaller("keys")
|
||||
|
||||
viewvalues = operator.methodcaller("values")
|
||||
|
||||
viewitems = operator.methodcaller("items")
|
||||
else:
|
||||
def iterkeys(d, **kw):
|
||||
return d.iterkeys(**kw)
|
||||
|
||||
def itervalues(d, **kw):
|
||||
return d.itervalues(**kw)
|
||||
|
||||
def iteritems(d, **kw):
|
||||
return d.iteritems(**kw)
|
||||
|
||||
def iterlists(d, **kw):
|
||||
return d.iterlists(**kw)
|
||||
|
||||
viewkeys = operator.methodcaller("viewkeys")
|
||||
|
||||
viewvalues = operator.methodcaller("viewvalues")
|
||||
|
||||
viewitems = operator.methodcaller("viewitems")
|
||||
|
||||
_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
|
||||
_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
|
||||
_add_doc(iteritems,
|
||||
"Return an iterator over the (key, value) pairs of a dictionary.")
|
||||
_add_doc(iterlists,
|
||||
"Return an iterator over the (key, [values]) pairs of a dictionary.")
|
||||
|
||||
|
||||
if PY3:
|
||||
def b(s):
|
||||
return s.encode("latin-1")
|
||||
|
||||
def u(s):
|
||||
return s
|
||||
unichr = chr
|
||||
import struct
|
||||
int2byte = struct.Struct(">B").pack
|
||||
del struct
|
||||
byte2int = operator.itemgetter(0)
|
||||
indexbytes = operator.getitem
|
||||
iterbytes = iter
|
||||
import io
|
||||
StringIO = io.StringIO
|
||||
BytesIO = io.BytesIO
|
||||
del io
|
||||
_assertCountEqual = "assertCountEqual"
|
||||
if sys.version_info[1] <= 1:
|
||||
_assertRaisesRegex = "assertRaisesRegexp"
|
||||
_assertRegex = "assertRegexpMatches"
|
||||
_assertNotRegex = "assertNotRegexpMatches"
|
||||
else:
|
||||
_assertRaisesRegex = "assertRaisesRegex"
|
||||
_assertRegex = "assertRegex"
|
||||
_assertNotRegex = "assertNotRegex"
|
||||
else:
|
||||
def b(s):
|
||||
return s
|
||||
# Workaround for standalone backslash
|
||||
|
||||
def u(s):
|
||||
return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
|
||||
unichr = unichr
|
||||
int2byte = chr
|
||||
|
||||
def byte2int(bs):
|
||||
return ord(bs[0])
|
||||
|
||||
def indexbytes(buf, i):
|
||||
return ord(buf[i])
|
||||
iterbytes = functools.partial(itertools.imap, ord)
|
||||
import StringIO
|
||||
StringIO = BytesIO = StringIO.StringIO
|
||||
_assertCountEqual = "assertItemsEqual"
|
||||
_assertRaisesRegex = "assertRaisesRegexp"
|
||||
_assertRegex = "assertRegexpMatches"
|
||||
_assertNotRegex = "assertNotRegexpMatches"
|
||||
_add_doc(b, """Byte literal""")
|
||||
_add_doc(u, """Text literal""")
|
||||
|
||||
|
||||
def assertCountEqual(self, *args, **kwargs):
|
||||
return getattr(self, _assertCountEqual)(*args, **kwargs)
|
||||
|
||||
|
||||
def assertRaisesRegex(self, *args, **kwargs):
|
||||
return getattr(self, _assertRaisesRegex)(*args, **kwargs)
|
||||
|
||||
|
||||
def assertRegex(self, *args, **kwargs):
|
||||
return getattr(self, _assertRegex)(*args, **kwargs)
|
||||
|
||||
|
||||
def assertNotRegex(self, *args, **kwargs):
|
||||
return getattr(self, _assertNotRegex)(*args, **kwargs)
|
||||
|
||||
|
||||
if PY3:
|
||||
exec_ = getattr(moves.builtins, "exec")
|
||||
|
||||
def reraise(tp, value, tb=None):
|
||||
try:
|
||||
if value is None:
|
||||
value = tp()
|
||||
if value.__traceback__ is not tb:
|
||||
raise value.with_traceback(tb)
|
||||
raise value
|
||||
finally:
|
||||
value = None
|
||||
tb = None
|
||||
|
||||
else:
|
||||
def exec_(_code_, _globs_=None, _locs_=None):
|
||||
"""Execute code in a namespace."""
|
||||
if _globs_ is None:
|
||||
frame = sys._getframe(1)
|
||||
_globs_ = frame.f_globals
|
||||
if _locs_ is None:
|
||||
_locs_ = frame.f_locals
|
||||
del frame
|
||||
elif _locs_ is None:
|
||||
_locs_ = _globs_
|
||||
exec("""exec _code_ in _globs_, _locs_""")
|
||||
|
||||
exec_("""def reraise(tp, value, tb=None):
|
||||
try:
|
||||
raise tp, value, tb
|
||||
finally:
|
||||
tb = None
|
||||
""")
|
||||
|
||||
|
||||
if sys.version_info[:2] > (3,):
|
||||
exec_("""def raise_from(value, from_value):
|
||||
try:
|
||||
raise value from from_value
|
||||
finally:
|
||||
value = None
|
||||
""")
|
||||
else:
|
||||
def raise_from(value, from_value):
|
||||
raise value
|
||||
|
||||
|
||||
print_ = getattr(moves.builtins, "print", None)
|
||||
if print_ is None:
|
||||
def print_(*args, **kwargs):
|
||||
"""The new-style print function for Python 2.4 and 2.5."""
|
||||
fp = kwargs.pop("file", sys.stdout)
|
||||
if fp is None:
|
||||
return
|
||||
|
||||
def write(data):
|
||||
if not isinstance(data, basestring):
|
||||
data = str(data)
|
||||
# If the file has an encoding, encode unicode with it.
|
||||
if (isinstance(fp, file) and
|
||||
isinstance(data, unicode) and
|
||||
fp.encoding is not None):
|
||||
errors = getattr(fp, "errors", None)
|
||||
if errors is None:
|
||||
errors = "strict"
|
||||
data = data.encode(fp.encoding, errors)
|
||||
fp.write(data)
|
||||
want_unicode = False
|
||||
sep = kwargs.pop("sep", None)
|
||||
if sep is not None:
|
||||
if isinstance(sep, unicode):
|
||||
want_unicode = True
|
||||
elif not isinstance(sep, str):
|
||||
raise TypeError("sep must be None or a string")
|
||||
end = kwargs.pop("end", None)
|
||||
if end is not None:
|
||||
if isinstance(end, unicode):
|
||||
want_unicode = True
|
||||
elif not isinstance(end, str):
|
||||
raise TypeError("end must be None or a string")
|
||||
if kwargs:
|
||||
raise TypeError("invalid keyword arguments to print()")
|
||||
if not want_unicode:
|
||||
for arg in args:
|
||||
if isinstance(arg, unicode):
|
||||
want_unicode = True
|
||||
break
|
||||
if want_unicode:
|
||||
newline = unicode("\n")
|
||||
space = unicode(" ")
|
||||
else:
|
||||
newline = "\n"
|
||||
space = " "
|
||||
if sep is None:
|
||||
sep = space
|
||||
if end is None:
|
||||
end = newline
|
||||
for i, arg in enumerate(args):
|
||||
if i:
|
||||
write(sep)
|
||||
write(arg)
|
||||
write(end)
|
||||
if sys.version_info[:2] < (3, 3):
|
||||
_print = print_
|
||||
|
||||
def print_(*args, **kwargs):
|
||||
fp = kwargs.get("file", sys.stdout)
|
||||
flush = kwargs.pop("flush", False)
|
||||
_print(*args, **kwargs)
|
||||
if flush and fp is not None:
|
||||
fp.flush()
|
||||
|
||||
_add_doc(reraise, """Reraise an exception.""")
|
||||
|
||||
if sys.version_info[0:2] < (3, 4):
|
||||
# This does exactly the same what the :func:`py3:functools.update_wrapper`
|
||||
# function does on Python versions after 3.2. It sets the ``__wrapped__``
|
||||
# attribute on ``wrapper`` object and it doesn't raise an error if any of
|
||||
# the attributes mentioned in ``assigned`` and ``updated`` are missing on
|
||||
# ``wrapped`` object.
|
||||
def _update_wrapper(wrapper, wrapped,
|
||||
assigned=functools.WRAPPER_ASSIGNMENTS,
|
||||
updated=functools.WRAPPER_UPDATES):
|
||||
for attr in assigned:
|
||||
try:
|
||||
value = getattr(wrapped, attr)
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
setattr(wrapper, attr, value)
|
||||
for attr in updated:
|
||||
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
|
||||
wrapper.__wrapped__ = wrapped
|
||||
return wrapper
|
||||
_update_wrapper.__doc__ = functools.update_wrapper.__doc__
|
||||
|
||||
def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
|
||||
updated=functools.WRAPPER_UPDATES):
|
||||
return functools.partial(_update_wrapper, wrapped=wrapped,
|
||||
assigned=assigned, updated=updated)
|
||||
wraps.__doc__ = functools.wraps.__doc__
|
||||
|
||||
else:
|
||||
wraps = functools.wraps
|
||||
|
||||
|
||||
def with_metaclass(meta, *bases):
|
||||
"""Create a base class with a metaclass."""
|
||||
# This requires a bit of explanation: the basic idea is to make a dummy
|
||||
# metaclass for one level of class instantiation that replaces itself with
|
||||
# the actual metaclass.
|
||||
class metaclass(type):
|
||||
|
||||
def __new__(cls, name, this_bases, d):
|
||||
if sys.version_info[:2] >= (3, 7):
|
||||
# This version introduced PEP 560 that requires a bit
|
||||
# of extra care (we mimic what is done by __build_class__).
|
||||
resolved_bases = types.resolve_bases(bases)
|
||||
if resolved_bases is not bases:
|
||||
d['__orig_bases__'] = bases
|
||||
else:
|
||||
resolved_bases = bases
|
||||
return meta(name, resolved_bases, d)
|
||||
|
||||
@classmethod
|
||||
def __prepare__(cls, name, this_bases):
|
||||
return meta.__prepare__(name, bases)
|
||||
return type.__new__(metaclass, 'temporary_class', (), {})
|
||||
|
||||
|
||||
def add_metaclass(metaclass):
|
||||
"""Class decorator for creating a class with a metaclass."""
|
||||
def wrapper(cls):
|
||||
orig_vars = cls.__dict__.copy()
|
||||
slots = orig_vars.get('__slots__')
|
||||
if slots is not None:
|
||||
if isinstance(slots, str):
|
||||
slots = [slots]
|
||||
for slots_var in slots:
|
||||
orig_vars.pop(slots_var)
|
||||
orig_vars.pop('__dict__', None)
|
||||
orig_vars.pop('__weakref__', None)
|
||||
if hasattr(cls, '__qualname__'):
|
||||
orig_vars['__qualname__'] = cls.__qualname__
|
||||
return metaclass(cls.__name__, cls.__bases__, orig_vars)
|
||||
return wrapper
|
||||
|
||||
|
||||
def ensure_binary(s, encoding='utf-8', errors='strict'):
|
||||
"""Coerce **s** to six.binary_type.
|
||||
|
||||
For Python 2:
|
||||
- `unicode` -> encoded to `str`
|
||||
- `str` -> `str`
|
||||
|
||||
For Python 3:
|
||||
- `str` -> encoded to `bytes`
|
||||
- `bytes` -> `bytes`
|
||||
"""
|
||||
if isinstance(s, binary_type):
|
||||
return s
|
||||
if isinstance(s, text_type):
|
||||
return s.encode(encoding, errors)
|
||||
raise TypeError("not expecting type '%s'" % type(s))
|
||||
|
||||
|
||||
def ensure_str(s, encoding='utf-8', errors='strict'):
|
||||
"""Coerce *s* to `str`.
|
||||
|
||||
For Python 2:
|
||||
- `unicode` -> encoded to `str`
|
||||
- `str` -> `str`
|
||||
|
||||
For Python 3:
|
||||
- `str` -> `str`
|
||||
- `bytes` -> decoded to `str`
|
||||
"""
|
||||
# Optimization: Fast return for the common case.
|
||||
if type(s) is str:
|
||||
return s
|
||||
if PY2 and isinstance(s, text_type):
|
||||
return s.encode(encoding, errors)
|
||||
elif PY3 and isinstance(s, binary_type):
|
||||
return s.decode(encoding, errors)
|
||||
elif not isinstance(s, (text_type, binary_type)):
|
||||
raise TypeError("not expecting type '%s'" % type(s))
|
||||
return s
|
||||
|
||||
|
||||
def ensure_text(s, encoding='utf-8', errors='strict'):
|
||||
"""Coerce *s* to six.text_type.
|
||||
|
||||
For Python 2:
|
||||
- `unicode` -> `unicode`
|
||||
- `str` -> `unicode`
|
||||
|
||||
For Python 3:
|
||||
- `str` -> `str`
|
||||
- `bytes` -> decoded to `str`
|
||||
"""
|
||||
if isinstance(s, binary_type):
|
||||
return s.decode(encoding, errors)
|
||||
elif isinstance(s, text_type):
|
||||
return s
|
||||
else:
|
||||
raise TypeError("not expecting type '%s'" % type(s))
|
||||
|
||||
|
||||
def python_2_unicode_compatible(klass):
|
||||
"""
|
||||
A class decorator that defines __unicode__ and __str__ methods under Python 2.
|
||||
Under Python 3 it does nothing.
|
||||
|
||||
To support Python 2 and 3 with a single code base, define a __str__ method
|
||||
returning text and apply this decorator to the class.
|
||||
"""
|
||||
if PY2:
|
||||
if '__str__' not in klass.__dict__:
|
||||
raise ValueError("@python_2_unicode_compatible cannot be applied "
|
||||
"to %s because it doesn't define __str__()." %
|
||||
klass.__name__)
|
||||
klass.__unicode__ = klass.__str__
|
||||
klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
|
||||
return klass
|
||||
|
||||
|
||||
# Complete the moves implementation.
|
||||
# This code is at the end of this module to speed up module loading.
|
||||
# Turn this module into a package.
|
||||
__path__ = [] # required for PEP 302 and PEP 451
|
||||
__package__ = __name__ # see PEP 366 @ReservedAssignment
|
||||
if globals().get("__spec__") is not None:
|
||||
__spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
|
||||
# Remove other six meta path importers, since they cause problems. This can
|
||||
# happen if six is removed from sys.modules and then reloaded. (Setuptools does
|
||||
# this for some reason.)
|
||||
if sys.meta_path:
|
||||
for i, importer in enumerate(sys.meta_path):
|
||||
# Here's some real nastiness: Another "instance" of the six module might
|
||||
# be floating around. Therefore, we can't use isinstance() to check for
|
||||
# the six meta path importer, since the other six instance will have
|
||||
# inserted an importer with different class.
|
||||
if (type(importer).__name__ == "_SixMetaPathImporter" and
|
||||
importer.name == __name__):
|
||||
del sys.meta_path[i]
|
||||
break
|
||||
del i, importer
|
||||
# Finally, add the importer to the meta path import hook.
|
||||
sys.meta_path.append(_importer)
|
||||
+3
-18
@@ -14,18 +14,6 @@ This document outlines the currently established development roadmap for Reticul
|
||||
## Currently Active Work Areas
|
||||
For each release cycle of Reticulum, improvements and additions from the five [Primary Efforts](#primary-efforts) are selected as active work areas, and can be expected to be included in the upcoming releases within that cycle. While not entirely set in stone for each release cycle, they serve as a pointer of what to expect in the near future.
|
||||
|
||||
- The current `0.8.x` release cycle aims at completing
|
||||
- [ ] Hot-pluggable interface system
|
||||
- [ ] External interface plugins
|
||||
- [ ] Network-wide path balancing and multi-pathing
|
||||
- [ ] Expanded hardware support
|
||||
- [ ] Overhauling and updating the documentation
|
||||
- [ ] Distributed Destination Naming System
|
||||
- [ ] A standalone RNS Daemon app for Android
|
||||
- [ ] Addding automatic retries to all use cases of the `Request` API
|
||||
- [ ] Performance and memory optimisations of the Python reference implementation
|
||||
- [ ] Fixing bugs discovered while operating Reticulum systems and applications
|
||||
|
||||
## Primary Efforts
|
||||
The development path for Reticulum is currently laid out in five distinct areas: *Comprehensibility*, *Universality*, *Functionality*, *Usability & Utility* and *Interfaceability*. Conceptualising the development of Reticulum into these areas serves to advance the implementation and work towards the Foundational Goals & Values of Reticulum.
|
||||
|
||||
@@ -50,17 +38,14 @@ These efforts are aimed at improving the ease of which Reticulum is understood,
|
||||
### Universality
|
||||
These efforts seek to broaden the universality of the Reticulum software and hardware ecosystem by continously diversifying platform support, and by improving the overall availability and ease of deployment of the Reticulum stack.
|
||||
|
||||
- OpenWRT support
|
||||
- Create a standalone RNS Daemon app for Android
|
||||
- A lightweight and portable C implementation for microcontrollers, µRNS
|
||||
- A portable, high-performance Reticulum implementation in C/C++, see [#21](https://github.com/markqvist/Reticulum/discussions/21)
|
||||
- Performance and memory optimisations of the Python implementation
|
||||
- Bindings for other programming languages
|
||||
|
||||
### Functionality
|
||||
These efforts aim to expand and improve the core functionality and reliability of Reticulum.
|
||||
|
||||
- Add support for user-supplied external interface drivers
|
||||
- Add interface hot-plug and live up/down control to running instances
|
||||
- Add automatic retries to all use cases of the `Request` API
|
||||
- Network-wide path balancing
|
||||
@@ -70,11 +55,11 @@ These efforts aim to expand and improve the core functionality and reliability o
|
||||
- [Metric-based path selection and multiple paths](https://github.com/markqvist/Reticulum/discussions/86)
|
||||
|
||||
### Usability & Utility
|
||||
These effors seek to make Reticulum easier to use and operate, and to expand the utility of the stack on deployed systems.
|
||||
These efforts seek to make Reticulum easier to use and operate, and to expand the utility of the stack on deployed systems.
|
||||
|
||||
- Easy way to share interface configurations, see [#19](https://github.com/markqvist/Reticulum/discussions/19)
|
||||
- Transit traffic display in rnstatus
|
||||
- rnsconfig utility
|
||||
- Transit traffic display in `rnstatus`
|
||||
- `rnsconfig` utility
|
||||
|
||||
### Interfaceability
|
||||
These efforts aim to expand the types of physical and virtual interfaces that Reticulum can natively use to transport data.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
# Sphinx build info version 1
|
||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: 137f4f4730a0df8f9714240ecb465598
|
||||
# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||
config: e189c8207645fa72ffb805cde7491052
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 152 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 259 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -86,7 +86,7 @@ This example can also be found at `<https://github.com/markqvist/Reticulum/blob/
|
||||
Requests & Responses
|
||||
====================
|
||||
|
||||
The *Request* example explores sendig requests and receiving responses.
|
||||
The *Request* example explores sending requests and receiving responses.
|
||||
|
||||
.. literalinclude:: ../../Examples/Request.py
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ scenarios.
|
||||
|
||||
|
||||
Standalone Reticulum Installation
|
||||
=============================================
|
||||
=================================
|
||||
If you simply want to install Reticulum and related utilities on a system,
|
||||
the easiest way is via the ``pip`` package manager:
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
pip install rns
|
||||
|
||||
@@ -23,9 +23,18 @@ of your system with a command like ``sudo apt install python3-pip``,
|
||||
You can also dowload the Reticulum release wheels from GitHub, or other release channels,
|
||||
and install them offline using ``pip``:
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
pip install ./rns-0.5.1-py3-none-any.whl
|
||||
pip install ./rns-1.0.2-py3-none-any.whl
|
||||
|
||||
On platforms that limit user package installation via ``pip``, you may need to manually
|
||||
allow this using the ``--break-system-packages`` command line flag when installing. This
|
||||
will not actually break any packages, unless you have installed Reticulum directly via
|
||||
your operating system's package manager.
|
||||
|
||||
.. code:: shell
|
||||
|
||||
pip install rns --break-system-packages
|
||||
|
||||
For more detailed installation instructions, please see the
|
||||
:ref:`Platform-Specific Install Notes<install-guides>` section.
|
||||
@@ -39,7 +48,7 @@ On some platforms, there may not be binary packages available for all dependenci
|
||||
``pip`` installation may fail with an error message. In these cases, the issue can usually
|
||||
be resolved by installing the development essentials packages for your platform:
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
# Debian / Ubuntu / Derivatives
|
||||
sudo apt install build-essential
|
||||
@@ -125,7 +134,7 @@ Linux, macOS and Windows.
|
||||
:align: center
|
||||
:target: _images/sideband_devices.webp
|
||||
|
||||
.. only:: latexpdf
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/sideband_devices.png
|
||||
:align: center
|
||||
@@ -140,8 +149,8 @@ MeshChat
|
||||
^^^^^^^^
|
||||
|
||||
The `Reticulum MeshChat <https://github.com/liamcottle/reticulum-meshchat>`_ application
|
||||
is a user-friendly LXMF client for macOS and Windows, that also includes voice call
|
||||
functionality, and a range of other interesting functions.
|
||||
is a user-friendly LXMF client for Linux, macOS and Windows, that also includes a Nomad Network
|
||||
page browser and other interesting functionality.
|
||||
|
||||
.. only:: html
|
||||
|
||||
@@ -149,7 +158,7 @@ functionality, and a range of other interesting functions.
|
||||
:align: center
|
||||
:target: _images/meshchat_1.webp
|
||||
|
||||
.. only:: latexpdf
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/meshchat_1.png
|
||||
:align: center
|
||||
@@ -209,7 +218,7 @@ and :ref:`Interfaces<interfaces-main>` chapters of this manual.
|
||||
|
||||
Connecting Reticulum Instances Over the Internet
|
||||
================================================
|
||||
Reticulum currently offers two interfaces suitable for connecting instances over the Internet: :ref:`TCP<interfaces-tcps>`
|
||||
Reticulum currently offers three interfaces suitable for connecting instances over the Internet: :ref:`Backbone<interfaces-backbone>`, :ref:`TCP<interfaces-tcps>`
|
||||
and :ref:`I2P<interfaces-i2p>`. Each interface offers a different set of features, and Reticulum
|
||||
users should carefully choose the interface which best suites their needs.
|
||||
|
||||
@@ -217,6 +226,10 @@ The ``TCPServerInterface`` allows users to host an instance accessible over TCP/
|
||||
method is generally faster, lower latency, and more energy efficient than using ``I2PInterface``,
|
||||
however it also leaks more data about the server host.
|
||||
|
||||
The ``BackboneInterface`` is a very fast and efficient interface type available on POSIX operating
|
||||
systems, designed to handle many hundreds of connections simultaneously with low memory, processing
|
||||
and I/O overhead. It is fully compatible with the TCP-based interface types.
|
||||
|
||||
TCP connections reveal the IP address of both your instance and the server to anyone who can
|
||||
inspect the connection. Someone could use this information to determine your location or identity. Adversaries
|
||||
inspecting your packets may be able to record packet metadata like time of transmission and packet size.
|
||||
@@ -241,14 +254,96 @@ In general it is recommended to use an I2P node if you want to host a publicly a
|
||||
instance, while preserving anonymity. If you care more about performance, and a slightly
|
||||
easier setup, use TCP.
|
||||
|
||||
.. _bootstrapping-connectivity:
|
||||
|
||||
Bootstrapping Connectivity
|
||||
==========================
|
||||
|
||||
Reticulum is not a service you subscribe to, nor is it a single global network you "join". It is a *networking stack*; a toolkit for building communications systems that align with your specific values, requirements, and operational environment. The way you choose to connect to other Reticulum peers is entirely your own choice.
|
||||
|
||||
One of the most powerful aspects of Reticulum is that it provides a multitude of tools to establish, maintain, and optimize connectivity. You can use these tools in isolation or combine them in complex configurations to achieve a vast array of goals.
|
||||
|
||||
Whether your aim is to create a completely private, air-gapped network for your family; to build a resilient community mesh that survives infrastructure collapse; to connect far and wide to as many nodes as possible; or simply to maintain a reliable, encrypted link to a specific organization you care about, Reticulum provides the mechanisms to make it happen.
|
||||
|
||||
There is no "right" or "wrong" way to build a Reticulum network, and you don't need to be a network engineer just to get started. If the information flows in the way you intend, and your privacy and security requirements are met, your configuration is a success. Reticulum is designed to make the most challenging and difficult scenarios attainable, even when other networking technologies fail.
|
||||
|
||||
|
||||
Finding Your Way
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
When you first start using Reticulum, you need a way to obtain connectivity with the peers you want to communicate with. This is the process of **bootstrapping**.
|
||||
|
||||
A common mistake in modern networking is the reliance on a few centralized, hard-coded entrypoints. If every user simply connects to the same list of public IP addresses found on a website, the network becomes brittle, centralized, and ultimately fails to deliver on the promise of decentralization.
|
||||
|
||||
Reticulum encourages the approach of *organic growth*. Instead of relying on permanent static connections to distant servers, you can use temporary bootstrap connections to *discover* better, more relevant or local infrastructure. Once discovered, your system can automatically form stronger, more direct links to these peers, and discard the temporary bootstrap links. This results in a web of connections that are geographically relevant, resilient and efficient.
|
||||
|
||||
It *is* possible to simply add a few public entrypoints to the ``[interfaces]`` section of your Reticulum configuration and be connected, but a better option is to enable :ref:`interface discovery<using-interface_discovery>` and either manually select relevant, local interfaces, or enable discovered interface auto-connection.
|
||||
|
||||
A relevant option in this context is the :ref:`bootstrap only<interfaces-options>` interface option. This is an automated tool for better distributing connectivity. By enabling interface discovery and auto-connection, and marking an interface as ``bootstrap_only``, you tell Reticulum to use that interface primarliy to find connectivity options, and then disconnect it once sufficient entrypoints have been discovered. This helps create a network topology that favors locality and resilience over the simple centralization caused by using only a few static entrypoints.
|
||||
|
||||
A good place to find interface definitions for bootstrapping connectivity is `rmap.world <https://rmap.world/>`_.
|
||||
|
||||
|
||||
Building Personal Infrastructure
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You do not need a datacenter to be a meaningful part of the Reticulum ecosystem. In fact, the most important nodes in the network are often the smallest ones.
|
||||
|
||||
We strongly encourage everyone, even home users, to think in terms of building **personal infrastructure**. Don't connect every phone, tablet, and computer in your house directly to a public internet gateway. Instead, repurpose an old computer, a Raspberry Pi, or a supported router to act as your own, personal **Transport Node**:
|
||||
|
||||
* Your local Transport Node sits in your home, connected to your WiFi and perhaps a radio interface (like an RNode).
|
||||
* You configure this node with a ``bootstrap_only`` interface (perhaps a TCP tunnel to a wider network) and enable interface discovery.
|
||||
* While you sleep, work, or cook, your node listens to the network. It discovers other local community members, validates their Network Identities, and automatically establishes direct links.
|
||||
* Your personal devices now connect to your *local* node, which is integrated into a living, breathing local mesh. Your traffic flows through local paths provided by other real people in the community rather than bouncing off a distant server.
|
||||
|
||||
**Don't wait for others to build the networks you want to see**. Every network is important, perhaps even most so those that support individual families and persons. Once enough of this personal, local infrastructure exist, connecting them directly to each other, without traversing the public Internet, becomes inevitable.
|
||||
|
||||
|
||||
Mixing Strategies
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
There is no requirement to commit to a single strategy. The most robust setups often mix static, dynamic, and discovered interfaces.
|
||||
|
||||
* **Static Interfaces:** You maintain a permanent interface to a trusted friend or organization using a static configuration.
|
||||
* **Bootstrap Links:** You connect a ``bootstrap_only`` interface to a public gateway on the Internet to scan for new connectable peers or to regain connectivity if your other interfaces fail.
|
||||
* **Local Wide-Area Connectivity:** You run a ``RNodeInterface`` on a shared frequency, giving you completely self-sovereign and private wide-area access to both your own network and other Reticulum peers globally, without any "service providers" being able to control or monitor how you interact with people.
|
||||
|
||||
By combining these methods, you create a system that is secure against single points of failure, adaptable to changing network conditions, and better integrated into your physical and social reality.
|
||||
|
||||
|
||||
Network Health & Responsibility
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
As you participate in the wider networks you discover and build, you will inevitably encounter peers that are misconfigured, malicious, or simply broken. To protect your resources and those of your local peers, you can utilize the :ref:`Blackhole Management<using-blackhole_management>` system.
|
||||
|
||||
Whether you manually block a spamming identity or subscribe to a blackhole list maintained by a trusted Network Identity, these tools help ensure that *your* transport capacity is used for what *you* consider legitimate communication. This keeps your local segment efficient and contributes to the health of the wider network.
|
||||
|
||||
|
||||
Contributing to the Global Ret
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have the means to host a stable node with a public IP address, consider becoming a :ref:`Public Entrypoint<hosting-entrypoints>`. By :ref:`publishing your interface as discoverable<interfaces-discoverable>`, you provide a potential connection point for others, helping the network grow and reach new areas.
|
||||
|
||||
For guidelines on how to properly configure and secure a public gateway, refer to the :ref:`Hosting Public Entrypoints<hosting-entrypoints>` section.
|
||||
|
||||
Connect to the Public Testnet
|
||||
===========================================
|
||||
=============================
|
||||
|
||||
An experimental public testnet has been made accessible over both I2P and TCP. You can join it
|
||||
by adding one of the following interfaces to your ``.reticulum/config`` file:
|
||||
An experimental public testnet has been made accessible by volunteers in the community. You
|
||||
can find interface definitions for adding to your ``.reticulum/config`` file on the
|
||||
`Reticulum Website <https://reticulum.network/connect.html>`_, or the
|
||||
`Community Wiki <https://github.com/markqvist/Reticulum/wiki/Community-Node-List>`_.
|
||||
|
||||
.. code::
|
||||
As development of Reticulum has transitioned away from the public Internet, and is now happening exclusively over Reticulum itself, the lists on the `Reticulum Website <https://reticulum.network/connect.html>`_ and the
|
||||
`Community Wiki <https://github.com/markqvist/Reticulum/wiki/Community-Node-List>`_ are no longer actively maintained, and any up-to-date connectivity information will have to be found elsewhere.
|
||||
|
||||
For a while, these resources will likely still be a usable way to find bootstrap connections, that will allow you to discover other entrypoints to connect to, but it is highly recommended to also check community run projects like `rmap.world <https://rmap.world/>`_.
|
||||
|
||||
You can connect your devices or instances to one or more of these to gain access to any
|
||||
Reticulum networks they are physically connected to. Simply add one or more interface
|
||||
snippets to your config file in the ``[interface]`` section, like in the example below:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
# TCP/IP interface to the RNS Amsterdam Hub
|
||||
[[RNS Testnet Amsterdam]]
|
||||
@@ -257,34 +352,76 @@ by adding one of the following interfaces to your ``.reticulum/config`` file:
|
||||
target_host = amsterdam.connect.reticulum.network
|
||||
target_port = 4965
|
||||
|
||||
# TCP/IP interface to the BetweenTheBorders Hub (community-provided)
|
||||
[[RNS Testnet BetweenTheBorders]]
|
||||
type = TCPClientInterface
|
||||
|
||||
.. tip::
|
||||
Don't rely on a single connection to a testnet entrypoint for everyday use. The testnet is often used for development and failure testing scenarios. Instead, read the :ref:`Bootstrapping Connectivity<bootstrapping-connectivity>` section.
|
||||
|
||||
As the amount of global Reticulum nodes and entrypoints have grown to a substantial quantity, the public Amsterdam Testnet entrypoint is slated for de-commisioning in the first quarter of 2026. If your own instances rely on this entrypoint for connectivity, it is high time to start configuring alternatives. Read the :ref:`Bootstrapping Connectivity<bootstrapping-connectivity>` section for pointers.
|
||||
|
||||
.. warning::
|
||||
It probably goes without saying, but *don't use the testnet entry-points as
|
||||
hardcoded or default interfaces in any applications you ship to users*. When
|
||||
shipping applications, the best practice is to provide your own default
|
||||
connectivity solutions, if needed and applicable, or in most cases, simply
|
||||
leave it up to the user which networks to connect to, and how.
|
||||
|
||||
.. _hosting-entrypoints:
|
||||
|
||||
Hosting Public Entrypoints
|
||||
==========================
|
||||
|
||||
If you want to host a public (or private) entry-point to a Reticulum network over the
|
||||
Internet, this section offers some helpful pointers. Once you have set up your public entrypoint, it is a great idea to :ref:`make it discoverable over Reticulum<interfaces-discoverable>`.
|
||||
|
||||
You will need a machine, physical or
|
||||
virtual with a public IP address, that can be reached by other devices on the Internet.
|
||||
|
||||
The most efficient and performant way to host a connectable entry-point supporting many
|
||||
users is to use the ``BackboneInterface``. This interface type is fully compatible with
|
||||
the ``TCPClientInterface`` and ``TCPServerInterface`` types, but much faster and uses
|
||||
less system resources, allowing your device to handle thousands of connections even on
|
||||
small systems.
|
||||
|
||||
It is also important to set your connectable interface to ``gateway`` mode, since this
|
||||
will greatly improve network convergence time and path resolution for anyone connecting
|
||||
to your entry-point.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
# This example demonstrates a backbone interface
|
||||
# configured for acting as a gateway for users to
|
||||
# connect to either a public or private network
|
||||
|
||||
[[Public Gateway]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
target_host = reticulum.betweentheborders.com
|
||||
target_port = 4242
|
||||
mode = gateway
|
||||
listen_on = 0.0.0.0
|
||||
port = 4242
|
||||
|
||||
# Interface to Testnet I2P Hub
|
||||
[[RNS Testnet I2P Hub]]
|
||||
type = I2PInterface
|
||||
If instead you want to make a private entry-point from the Internet, you can use the
|
||||
:ref:`IFAC name and passphrase options<interfaces-options>` to secure your interface with a network name and passphrase.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
# A private entry-point requiring a pre-shared
|
||||
# network name and passphrase to connect to.
|
||||
|
||||
[[Private Gateway]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
peers = g3br23bvx3lq5uddcsjii74xgmn6y5q325ovrkq2zw2wbzbqgbuq.b32.i2p
|
||||
|
||||
Many other Reticulum instances are connecting to this testnet, and you can also join it
|
||||
via other entry points if you know them. There is absolutely no control over the network
|
||||
topography, usage or what types of instances connect. It will also occasionally be used
|
||||
to test various failure scenarios, and there are no availability or service guarantees.
|
||||
Expect weird things to happen on this network, as people experiment and try out things.
|
||||
|
||||
It probably goes without saying, but *don't use the testnet entry-points as
|
||||
hardcoded or default interfaces in any applications you ship to users*. When
|
||||
shipping applications, the best practice is to provide your own default
|
||||
connectivity solutions, if needed and applicable, or in most cases, simply
|
||||
leave it up to the user which networks to connect to, and how.
|
||||
mode = gateway
|
||||
listen_on = 0.0.0.0
|
||||
port = 4242
|
||||
network_name = private_ret
|
||||
passphrase = 2owjajquafIanPecAc
|
||||
|
||||
If you are hosting an entry-point on an operating system that does not support
|
||||
``BackboneInterface``, you can use ``TCPServerInterface`` instead, although it will
|
||||
not be as performant.
|
||||
|
||||
Adding Radio Interfaces
|
||||
==============================================
|
||||
=======================
|
||||
Once you have Reticulum installed and working, you can add radio interfaces with
|
||||
any compatible hardware you have available. Reticulum supports a wide range of radio
|
||||
hardware, and if you already have any available, it is very likely that it will
|
||||
@@ -313,7 +450,7 @@ and propose adding an interface for the hardware.
|
||||
|
||||
|
||||
Creating and Using Custom Interfaces
|
||||
===========================================
|
||||
====================================
|
||||
|
||||
While Reticulum includes a flexible and broad range of built-in interfaces, these
|
||||
will not cover every conceivable type of communications hardware that Reticulum
|
||||
@@ -340,54 +477,10 @@ ready to import and use RNS in your own programs. The next step will most
|
||||
likely be to look at some :ref:`Example Programs<examples-main>`.
|
||||
|
||||
The entire Reticulum API is documented in the :ref:`API Reference<api-main>`
|
||||
chapter of this manual.
|
||||
chapter of this manual. Before diving in, it's probably a good idea to read
|
||||
this manual in full, but at least start with the :ref:`Understanding Reticulum<understanding-main>` chapter.
|
||||
|
||||
|
||||
Participate in Reticulum Development
|
||||
==============================================
|
||||
If you want to participate in the development of Reticulum and associated
|
||||
utilities, you'll want to get the latest source from GitHub. In that case,
|
||||
don't use pip, but try this recipe:
|
||||
|
||||
.. code::
|
||||
|
||||
# Install dependencies
|
||||
pip install cryptography pyserial
|
||||
|
||||
# Clone repository
|
||||
git clone https://github.com/markqvist/Reticulum.git
|
||||
|
||||
# Move into Reticulum folder and symlink library to examples folder
|
||||
cd Reticulum
|
||||
ln -s ../RNS ./Examples/
|
||||
|
||||
# Run an example
|
||||
python Examples/Echo.py -s
|
||||
|
||||
# Unless you've manually created a config file, Reticulum will do so now,
|
||||
# and immediately exit. Make any necessary changes to the file:
|
||||
nano ~/.reticulum/config
|
||||
|
||||
# ... and launch the example again.
|
||||
python Examples/Echo.py -s
|
||||
|
||||
# You can now repeat the process on another computer,
|
||||
# and run the same example with -h to get command line options.
|
||||
python Examples/Echo.py -h
|
||||
|
||||
# Run the example in client mode to "ping" the server.
|
||||
# Replace the hash below with the actual destination hash of your server.
|
||||
python Examples/Echo.py 174a64852a75682259ad8b921b8bf416
|
||||
|
||||
# Have a look at another example
|
||||
python Examples/Filetransfer.py -h
|
||||
|
||||
When you have experimented with the basic examples, it's time to go read the
|
||||
:ref:`Understanding Reticulum<understanding-main>` chapter. Before submitting
|
||||
your first pull request, it is probably a good idea to introduce yourself on
|
||||
the `disucssion forum on GitHub <https://github.com/markqvist/Reticulum/discussions>`_,
|
||||
or ask one of the developers or maintainers for a good place to start.
|
||||
|
||||
.. _install-guides:
|
||||
|
||||
Platform-Specific Install Notes
|
||||
@@ -415,7 +508,7 @@ build into Termux. After that, you can use ``pip`` to install Reticulum.
|
||||
|
||||
From within Termux, execute the following:
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
# First, make sure indexes and packages are up to date.
|
||||
pkg update
|
||||
@@ -434,7 +527,7 @@ If for some reason the ``python-cryptography`` package is not available for
|
||||
your platform via the Termux package manager, you can attempt to build it
|
||||
locally on your device using the following command:
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
# First, make sure indexes and packages are up to date.
|
||||
pkg update
|
||||
@@ -470,7 +563,7 @@ On some architectures, including ARM64, not all dependencies have precompiled
|
||||
binaries. On such systems, you may need to install ``python3-dev`` (or similar) before
|
||||
installing Reticulum or programs that depend on Reticulum.
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
# Install Python and development packages
|
||||
sudo apt update
|
||||
@@ -491,7 +584,7 @@ use the replacement ``pipx`` command instead, which places installed packages in
|
||||
isolated environment. This should not negatively affect Reticulum, but will not work
|
||||
for including and using Reticulum in your own scripts and programs.
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
# Install pipx
|
||||
sudo apt install pipx
|
||||
@@ -506,7 +599,7 @@ Alternatively, you can restore normal behaviour to ``pip`` by creating or editin
|
||||
the configuration file located at ``~/.config/pip/pip.conf``, and adding the
|
||||
following section:
|
||||
|
||||
.. code:: text
|
||||
.. code:: ini
|
||||
|
||||
[global]
|
||||
break-system-packages = true
|
||||
@@ -514,7 +607,7 @@ following section:
|
||||
For a one-shot installation of Reticulum, without globally enabling the ``break-system-packages``
|
||||
option, you can use the following command:
|
||||
|
||||
.. code:: text
|
||||
.. code:: shell
|
||||
|
||||
pip install rns --break-system-packages
|
||||
|
||||
@@ -539,7 +632,7 @@ Python manually.
|
||||
When Python and ``pip`` is available on your system, simply open a terminal window
|
||||
and use one of the following commands:
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
# Install Reticulum and utilities with pip:
|
||||
pip3 install rns
|
||||
@@ -560,7 +653,7 @@ manually add your installed ``pip`` packages directory to your `PATH` environmen
|
||||
variable, before you can use installed commands in your terminal. Usually, adding
|
||||
the following line to your shell init script (for example ``~/.zshrc``) will be enough:
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
export PATH=$PATH:~/Library/Python/3.9/bin
|
||||
|
||||
@@ -583,7 +676,7 @@ Reticulum and related utilities using the `opkg` package manager and `pip`.
|
||||
To install Reticulum on OpenWRT, first log into a command line session, and
|
||||
then use the following instructions:
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
# Install dependencies
|
||||
opkg install python3 python3-pip python3-cryptography python3-pyserial
|
||||
@@ -620,7 +713,7 @@ don't always have packages available for some dependencies. If Python and the
|
||||
`pip` package manager is not already installed, do that first, and then
|
||||
install Reticulum using `pip`.
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
# Install dependencies
|
||||
sudo apt install python3 python3-pip python3-cryptography python3-pyserial
|
||||
@@ -646,7 +739,7 @@ On some architectures, including RISC-V, not all dependencies have precompiled
|
||||
binaries. On such systems, you may need to install ``python3-dev`` (or similar) before
|
||||
installing Reticulum or programs that depend on Reticulum.
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
# Install Python and development packages
|
||||
sudo apt update
|
||||
@@ -667,7 +760,7 @@ use the replacement ``pipx`` command instead, which places installed packages in
|
||||
isolated environment. This should not negatively affect Reticulum, but will not work
|
||||
for including and using Reticulum in your own scripts and programs.
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
# Install pipx
|
||||
sudo apt install pipx
|
||||
@@ -717,7 +810,7 @@ use the ``pip`` installer, or run the included Reticulum utility programs (such
|
||||
|
||||
After installing Python, open the command prompt or Windows Powershell, and type:
|
||||
|
||||
.. code::
|
||||
.. code:: shell
|
||||
|
||||
pip install rns
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ for example the :ref:`PipeInterface<interfaces-pipe>` or the :ref:`TCPClientInte
|
||||
in combination with code like `TCP KISS Server <https://github.com/simplyequipped/tcpkissserver>`_
|
||||
by `simplyequipped <https://github.com/simplyequipped>`_.
|
||||
|
||||
It is also very easy to write and load :ref:`custom interface modules<interfaces-custom>`
|
||||
into Reticulum, allowing you to communicate with more or less anything you can think of.
|
||||
|
||||
While this broad support and flexibility is very useful, an abundance of options
|
||||
can sometimes make it difficult to know where to begin, especially when you are
|
||||
starting from scratch.
|
||||
@@ -88,7 +91,8 @@ to the configuration.
|
||||
Supported Boards and Devices
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
To create one or more RNodes, you will need to obtain supported development
|
||||
boards. The following boards are supported by the auto-installer.
|
||||
boards or completed devices. The following boards and devices are supported
|
||||
by the auto-installer.
|
||||
|
||||
------------
|
||||
|
||||
@@ -98,7 +102,7 @@ boards. The following boards are supported by the auto-installer.
|
||||
|
||||
LilyGO T-Beam Supreme
|
||||
"""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262, SX1268
|
||||
- **Transceiver IC** Semtech SX1262 or SX1268
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
@@ -110,7 +114,7 @@ LilyGO T-Beam Supreme
|
||||
|
||||
LilyGO T-Beam
|
||||
"""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262, SX1268, SX1276 and SX1278
|
||||
- **Transceiver IC** Semtech SX1262, SX1268, SX1276 or SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
@@ -122,7 +126,7 @@ LilyGO T-Beam
|
||||
|
||||
LilyGO T3S3
|
||||
"""""""""""
|
||||
- **Transceiver IC** Semtech SX1262, SX1268, SX1276 and SX1278
|
||||
- **Transceiver IC** Semtech SX1262, SX1268, SX1276 or SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
@@ -134,19 +138,31 @@ LilyGO T3S3
|
||||
|
||||
RAK4631-based Boards
|
||||
""""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262, SX1268
|
||||
- **Transceiver IC** Semtech SX1262 or SX1268
|
||||
- **Device Platform** nRF52
|
||||
- **Manufacturer** `RAK Wireless <https://www.rakwireless.com>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_opencomxl.png
|
||||
:width: 45%
|
||||
:align: center
|
||||
|
||||
OpenCom XL
|
||||
""""""""""""""""""""
|
||||
- **Transceiver ICs** Semtech SX1262 and SX1280 (dual transceiver)
|
||||
- **Device Platform** nRF52
|
||||
- **Manufacturer** `Liberated Embedded Systems <https://liberatedsystems.co.uk/>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_rnodev2.png
|
||||
:width: 68%
|
||||
:align: center
|
||||
|
||||
Unsigned RNode v2.x
|
||||
"""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Transceiver IC** Semtech SX1276 or SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `unsigned.io <https://unsigned.io>`_
|
||||
|
||||
@@ -158,7 +174,7 @@ Unsigned RNode v2.x
|
||||
|
||||
LilyGO LoRa32 v2.1
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Transceiver IC** Semtech SX1276 or SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
@@ -170,7 +186,7 @@ LilyGO LoRa32 v2.1
|
||||
|
||||
LilyGO LoRa32 v2.0
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Transceiver IC** Semtech SX1276 or SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
@@ -182,7 +198,7 @@ LilyGO LoRa32 v2.0
|
||||
|
||||
LilyGO LoRa32 v1.0
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Transceiver IC** Semtech SX1276 or SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
@@ -194,19 +210,55 @@ LilyGO LoRa32 v1.0
|
||||
|
||||
LilyGO T-Deck
|
||||
"""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262, SX1268
|
||||
- **Transceiver IC** Semtech SX1262 or SX1268
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_techo.png
|
||||
:width: 45%
|
||||
:align: center
|
||||
|
||||
LilyGO T-Echo
|
||||
"""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262 or SX1268
|
||||
- **Device Platform** nRF52
|
||||
- **Manufacturer** `LilyGO <https://lilygo.cn>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_t114.png
|
||||
:width: 58%
|
||||
:align: center
|
||||
|
||||
Heltec T114
|
||||
"""""""""""
|
||||
- **Transceiver IC** Semtech SX1262 or SX1268
|
||||
- **Device Platform** nRF52
|
||||
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_heltec32v4.png
|
||||
:width: 58%
|
||||
:align: center
|
||||
|
||||
Heltec LoRa32 v4.0
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_heltec32v30.png
|
||||
:width: 58%
|
||||
:align: center
|
||||
|
||||
Heltec LoRa32 v3.0
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1262 and SX1268
|
||||
- **Transceiver IC** Semtech SX1262 or SX1268
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
|
||||
|
||||
@@ -218,24 +270,12 @@ Heltec LoRa32 v3.0
|
||||
|
||||
Heltec LoRa32 v2.0
|
||||
""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Transceiver IC** Semtech SX1276 or SX1278
|
||||
- **Device Platform** ESP32
|
||||
- **Manufacturer** `Heltec Automation <https://heltec.org>`_
|
||||
|
||||
------------
|
||||
|
||||
.. image:: graphics/board_rnode.png
|
||||
:width: 50%
|
||||
:align: center
|
||||
|
||||
Unsigned RNode v1.x
|
||||
"""""""""""""""""""
|
||||
- **Transceiver IC** Semtech SX1276 and SX1278
|
||||
- **Device Platform** AVR ATmega1284p
|
||||
- **Manufacturer** `unsigned.io <https://unsigned.io>`_
|
||||
|
||||
------------
|
||||
|
||||
.. _rnode-installation:
|
||||
|
||||
Installation
|
||||
|
||||
@@ -25,8 +25,8 @@ to participate in the development of Reticulum itself.
|
||||
hardware
|
||||
interfaces
|
||||
networks
|
||||
examples
|
||||
support
|
||||
examples
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
@@ -34,54 +34,67 @@ example for basic interface code to build upon.
|
||||
Auto Interface
|
||||
==============
|
||||
|
||||
The Auto Interface enables communication with other discoverable Reticulum
|
||||
nodes over autoconfigured IPv6 and UDP. It does not need any functional IP
|
||||
infrastructure like routers or DHCP servers, but will require at least some
|
||||
sort of switching medium between peers (a wired switch, a hub, a WiFi access
|
||||
point or similar), and that link-local IPv6 is enabled in your operating
|
||||
system, which should be enabled by default in almost all OSes.
|
||||
The ``AutoInterface`` enables communication with other discoverable Reticulum
|
||||
nodes over any kind of local Ethernet or WiFi-based medium. Even though it uses IPv6 for peer
|
||||
discovery, and UDP for packet transport, it **does not** need any functional IP
|
||||
infrastructure like routers or DHCP servers, on your physical network.
|
||||
|
||||
.. code::
|
||||
.. warning::
|
||||
If you have **firewall** software running on your computer, it may block traffic
|
||||
required for ``AutoInterface`` to work. If this is the case, you will have to
|
||||
allow UDP traffic on port ``29716`` and ``42671``.
|
||||
|
||||
As long as there is at least some sort of switching medium present between peers (a
|
||||
wired switch, a hub, a WiFi access point or similar, or simply two devices connected
|
||||
directly by Ethernet cable), it will work without any configuration, setup or intermediary devices.
|
||||
|
||||
For ``AutoInterface`` peer discovery to work, it's also required that link-local
|
||||
IPv6 support is available on your system, which it should be by default in all
|
||||
current operating systems, both desktop and mobile.
|
||||
|
||||
.. note::
|
||||
Almost all current Ethernet and WiFi hardware will work without any kind
|
||||
of configuration or setup with ``AutoInterface``, but a small subset of
|
||||
devices turn on options that limit device-to-device communication by default,
|
||||
resulting in ``AutoInterface`` peer discovery being blocked. This issue is
|
||||
most commonly seen on very cheap, ISP-supplied WiFi routers, and can sometimes
|
||||
be turned off in the router configuration.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
# This example demonstrates a bare-minimum setup
|
||||
# of an Auto Interface. It will allow communica-
|
||||
# tion with all other reachable devices on all
|
||||
# usable physical ethernet-based devices that
|
||||
# are available on the system.
|
||||
|
||||
[[Default Interface]]
|
||||
type = AutoInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# This example demonstrates an more specifically
|
||||
# configured Auto Interface, that only uses spe-
|
||||
# cific physical interfaces, and has a number of
|
||||
# other configuration options set.
|
||||
|
||||
[[Default Interface]]
|
||||
type = AutoInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# You can create multiple isolated Reticulum
|
||||
# networks on the same physical LAN by
|
||||
# specifying different Group IDs.
|
||||
|
||||
group_id = reticulum
|
||||
|
||||
# You can also choose the multicast address type:
|
||||
# temporary (default, Temporary Multicast Address)
|
||||
# or permanent (Permanent Multicast Address)
|
||||
|
||||
multicast_address_type = permanent
|
||||
|
||||
# You can also select specifically which
|
||||
# kernel networking devices to use.
|
||||
|
||||
devices = wlan0,eth1
|
||||
|
||||
# Or let AutoInterface use all suitable
|
||||
# devices except for a list of ignored ones.
|
||||
|
||||
ignored_devices = tun0,eth0
|
||||
|
||||
|
||||
@@ -91,11 +104,11 @@ autodiscover other Reticulum nodes within your selected Group ID. You can specif
|
||||
the discovery scope by setting it to one of ``link``, ``admin``, ``site``,
|
||||
``organisation`` or ``global``.
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
[[Default Interface]]
|
||||
type = AutoInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Configure global discovery
|
||||
|
||||
@@ -108,73 +121,114 @@ the discovery scope by setting it to one of ``link``, ``admin``, ``site``,
|
||||
data_port = 49555
|
||||
|
||||
|
||||
.. _interfaces-i2p:
|
||||
.. _interfaces-backbone:
|
||||
|
||||
I2P Interface
|
||||
=============
|
||||
Backbone Interface
|
||||
====================
|
||||
|
||||
The I2P interface lets you connect Reticulum instances over the
|
||||
`Invisible Internet Protocol <https://i2pd.website>`_. This can be
|
||||
especially useful in cases where you want to host a globally reachable
|
||||
Reticulum instance, but do not have access to any public IP addresses,
|
||||
have a frequently changing IP address, or have firewalls blocking
|
||||
inbound traffic.
|
||||
|
||||
Using the I2P interface, you will get a globally reachable, portable
|
||||
and persistent I2P address that your Reticulum instance can be reached
|
||||
at.
|
||||
|
||||
To use the I2P interface, you must have an I2P router running
|
||||
on your system. The easiest way to achieve this is to download and
|
||||
install the `latest release <https://github.com/PurpleI2P/i2pd/releases/latest>`_
|
||||
of the ``i2pd`` package. For more details about I2P, see the
|
||||
`geti2p.net website <https://geti2p.net/en/about/intro>`_.
|
||||
|
||||
When an I2P router is running on your system, you can simply add
|
||||
an I2P interface to Reticulum:
|
||||
|
||||
.. code::
|
||||
|
||||
[[I2P]]
|
||||
type = I2PInterface
|
||||
interface_enabled = yes
|
||||
connectable = yes
|
||||
|
||||
On the first start, Reticulum will generate a new I2P address for the
|
||||
interface and start listening for inbound traffic on it. This can take
|
||||
a while the first time, especially if your I2P router was also just
|
||||
started, and is not yet well-connected to the I2P network. When ready,
|
||||
you should see I2P base32 address printed to your log file. You can
|
||||
also inspect the status of the interface using the ``rnstatus`` utility.
|
||||
|
||||
To connect to other Reticulum instances over I2P, just add a comma-separated
|
||||
list of I2P base32 addresses to the ``peers`` option of the interface:
|
||||
|
||||
.. code::
|
||||
|
||||
[[I2P]]
|
||||
type = I2PInterface
|
||||
interface_enabled = yes
|
||||
connectable = yes
|
||||
peers = 5urvjicpzi7q3ybztsef4i5ow2aq4soktfj7zedz53s47r54jnqq.b32.i2p
|
||||
|
||||
It can take anywhere from a few seconds to a few minutes to establish
|
||||
I2P connections to the desired peers, so Reticulum handles the process
|
||||
in the background, and will output relevant events to the log.
|
||||
The Backbone interface is a very fast and resource efficient interface type, primarily
|
||||
intended for interconnecting Reticulum instances over many different types of mediums.
|
||||
It uses a kernel-event I/O backend, and can handle thousands of interfaces and/or clients
|
||||
with relatively low system resource utilisation. **This interface type is currently only
|
||||
supported on Linux and Android**.
|
||||
|
||||
.. note::
|
||||
While the I2P interface is the simplest way to use
|
||||
Reticulum over I2P, it is also possible to tunnel the TCP server and
|
||||
client interfaces over I2P manually. This can be useful in situations
|
||||
where more control is needed, but requires manual tunnel setup through
|
||||
the I2P daemon configuration.
|
||||
The Backbone Interface is fully compatible with the ``TCPServerInterface`` and ``TCPClientInterface``
|
||||
types, and they can be used interchangably, and cross-connect with each other. On systems that support
|
||||
``BackboneInterface``, it is generally recommended to use it, unless you need specific options or
|
||||
features that the TCP server and client interfaces provide.
|
||||
|
||||
It is important to note that the two methods are *interchangably compatible*.
|
||||
You can use the I2PInterface to connect to a TCPServerInterface that
|
||||
was manually tunneled over I2P, for example. This offers a high degree
|
||||
of flexibility in network setup, while retaining ease of use in simpler
|
||||
use-cases.
|
||||
While the goal is to support *all* socket types and I/O devices provided by the underlying
|
||||
operating system, the initial release only provides support for TCP connections over IPv4
|
||||
and IPv6.
|
||||
|
||||
For all types of connections over a ``BackboneInterface``, Reticulum will gracefully
|
||||
handle intermittency, link loss, and connections that come and go.
|
||||
|
||||
Listeners
|
||||
---------
|
||||
|
||||
The following examples illustrates various ways to set up ``BackboneInterface`` listeners.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
# This example demonstrates a backbone interface
|
||||
# that listens for incoming connections on the
|
||||
# specified IP address and port number.
|
||||
[[Backbone Listener]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
listen_on = 0.0.0.0
|
||||
port = 4242
|
||||
|
||||
# Alternatively you can bind to a specific IP
|
||||
[[Backbone Listener]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
listen_on = 10.0.0.88
|
||||
port = 4242
|
||||
|
||||
# Or a specific network device
|
||||
[[Backbone Listener]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
device = eth0
|
||||
port = 4242
|
||||
|
||||
If you are using the interface on a device which has both IPv4 and IPv6 addresses available,
|
||||
you can use the ``prefer_ipv6`` option to bind to the IPv6 address:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
# This example demonstrates a backbone interface
|
||||
# listening on the IPv6 address of a specified
|
||||
# kernel networking device.
|
||||
[[Backbone Listener]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
prefer_ipv6 = yes
|
||||
device = eth0
|
||||
port = 4242
|
||||
|
||||
To use the ``BackboneInterface`` over `Yggdrasil <https://yggdrasil-network.github.io/>`_, you
|
||||
can simply specify the Yggdrasil ``tun`` device and a listening port, like so:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
# This example demonstrates a backbone interface
|
||||
# listening for connections over Yggdrasil.
|
||||
[[Yggdrasil Backbone Interface]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
device = tun0
|
||||
port = 4343
|
||||
|
||||
Connecting Remotes
|
||||
------------------
|
||||
The following examples illustrates various ways to connect to remote ``BackboneInterface`` listeners.
|
||||
As noted above, ``BackboneInterface`` interfaces can also connect to remote ``TCPServerInterface``,
|
||||
and as such these interface types can be used interchangably.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
# Here's an example of a backbone interface that
|
||||
# connects to a remote listener.
|
||||
[[Backbone Remote]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
remote = amsterdam.connect.reticulum.network
|
||||
target_port = 4251
|
||||
|
||||
To connect to remotes over `Yggdrasil <https://yggdrasil-network.github.io/>`_, simply
|
||||
specify the target Yggdrasil IPv6 address and port, like so:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[Yggdrasil Remote]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5
|
||||
target_port = 4343
|
||||
|
||||
.. _interfaces-tcps:
|
||||
|
||||
@@ -185,36 +239,35 @@ The TCP Server interface is suitable for allowing other peers to connect over
|
||||
the Internet or private IPv4 and IPv6 networks. When a TCP server interface has been
|
||||
configured, other Reticulum peers can connect to it with a TCP Client interface.
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
# This example demonstrates a TCP server interface.
|
||||
# It will listen for incoming connections on the
|
||||
# specified IP address and port number.
|
||||
|
||||
# It will listen for incoming connections on all IP
|
||||
# interfaces on port 4242.
|
||||
[[TCP Server Interface]]
|
||||
type = TCPServerInterface
|
||||
interface_enabled = True
|
||||
|
||||
# This configuration will listen on all IP
|
||||
# interfaces on port 4242
|
||||
|
||||
enabled = yes
|
||||
listen_ip = 0.0.0.0
|
||||
listen_port = 4242
|
||||
|
||||
# Alternatively you can bind to a specific IP
|
||||
|
||||
# listen_ip = 10.0.0.88
|
||||
# listen_port = 4242
|
||||
# Alternatively you can bind to a specific IP
|
||||
[[TCP Server Interface]]
|
||||
type = TCPServerInterface
|
||||
enabled = yes
|
||||
listen_ip = 10.0.0.88
|
||||
listen_port = 4242
|
||||
|
||||
# Or a specific network device
|
||||
|
||||
# device = eth0
|
||||
# port = 4242
|
||||
# Or a specific network device
|
||||
[[TCP Server Interface]]
|
||||
type = TCPServerInterface
|
||||
enabled = yes
|
||||
device = eth0
|
||||
listen_port = 4242
|
||||
|
||||
If you are using the interface on a device which has both IPv4 and IPv6 addresses available,
|
||||
you can use the ``prefer_ipv6`` option to bind to the IPv6 address:
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
# This example demonstrates a TCP server interface.
|
||||
# It will listen for incoming connections on the
|
||||
@@ -222,22 +275,21 @@ you can use the ``prefer_ipv6`` option to bind to the IPv6 address:
|
||||
|
||||
[[TCP Server Interface]]
|
||||
type = TCPServerInterface
|
||||
interface_enabled = True
|
||||
|
||||
enabled = yes
|
||||
prefer_ipv6 = True
|
||||
device = eth0
|
||||
port = 4242
|
||||
prefer_ipv6 = True
|
||||
|
||||
To use the TCP Server Interface over `Yggdrasil <https://yggdrasil-network.github.io/>`_, you
|
||||
can simply specify the Yggdrasil ``tun`` device and a listening port, like so:
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
[[Yggdrasil TCP Server Interface]]
|
||||
type = TCPServerInterface
|
||||
interface_enabled = yes
|
||||
device = tun0
|
||||
listen_port = 4343
|
||||
type = TCPServerInterface
|
||||
enabled = yes
|
||||
device = tun0
|
||||
listen_port = 4343
|
||||
|
||||
.. note::
|
||||
The TCP interfaces support tunneling over I2P, but to do so reliably,
|
||||
@@ -246,11 +298,11 @@ can simply specify the Yggdrasil ``tun`` device and a listening port, like so:
|
||||
.. code::
|
||||
|
||||
[[TCP Server on I2P]]
|
||||
type = TCPServerInterface
|
||||
interface_enabled = yes
|
||||
listen_ip = 127.0.0.1
|
||||
listen_port = 5001
|
||||
i2p_tunneled = yes
|
||||
type = TCPServerInterface
|
||||
enabled = yes
|
||||
listen_ip = 127.0.0.1
|
||||
listen_port = 5001
|
||||
i2p_tunneled = yes
|
||||
|
||||
In almost all cases, it is easier to use the dedicated ``I2PInterface``, but for complete
|
||||
control, and using I2P routers running on external systems, this option also exists.
|
||||
@@ -260,7 +312,7 @@ control, and using I2P routers running on external systems, this option also exi
|
||||
TCP Client Interface
|
||||
====================
|
||||
|
||||
To connect to a TCP server interface, you would naturally use the TCP client
|
||||
To connect to a TCP server interface, you can use the TCP client
|
||||
interface. Many TCP Client interfaces from different peers can connect to the
|
||||
same TCP Server interface at the same time.
|
||||
|
||||
@@ -268,25 +320,24 @@ The TCP interface types can also tolerate intermittency in the IP link layer.
|
||||
This means that Reticulum will gracefully handle IP links that go up and down,
|
||||
and restore connectivity after a failure, once the other end of a TCP interface reappears.
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
# Here's an example of a TCP Client interface. The
|
||||
# target_host can be a hostname or an IPv4 or IPv6 address.
|
||||
|
||||
[[TCP Client Interface]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
target_host = 127.0.0.1
|
||||
target_port = 4242
|
||||
|
||||
To use the TCP Client Interface over `Yggdrasil <https://yggdrasil-network.github.io/>`_, simply
|
||||
specify the target Yggdrasil IPv6 address and port, like so:
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
[[Yggdrasil TCP Client Interface]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = yes
|
||||
enabled = yes
|
||||
target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5
|
||||
target_port = 4343
|
||||
|
||||
@@ -294,17 +345,18 @@ It is also possible to use this interface type to connect via other programs
|
||||
or hardware devices that expose a KISS interface on a TCP port, for example
|
||||
software-based soundmodems. To do this, use the ``kiss_framing`` option:
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
# Here's an example of a TCP Client interface that connects
|
||||
# to a software TNC soundmodem on a KISS over TCP port.
|
||||
|
||||
[[TCP KISS Interface]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
kiss_framing = True
|
||||
target_host = 127.0.0.1
|
||||
target_port = 8001
|
||||
fixed_mtu = 500
|
||||
|
||||
**Caution!** Only use the KISS framing option when connecting to external devices
|
||||
and programs like soundmodems and similar over TCP. When using the
|
||||
@@ -313,15 +365,18 @@ never enable ``kiss_framing``, since this will disable internal reliability and
|
||||
recovery mechanisms that greatly improves performance over unreliable and
|
||||
intermittent TCP links.
|
||||
|
||||
For KISS devices that need only supports a particular MTU, you can use the
|
||||
``fixed_mtu`` option.
|
||||
|
||||
.. note::
|
||||
The TCP interfaces support tunneling over I2P, but to do so reliably,
|
||||
you must use the i2p_tunneled option:
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
[[TCP Client over I2P]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = yes
|
||||
enabled = yes
|
||||
target_host = 127.0.0.1
|
||||
target_port = 5001
|
||||
i2p_tunneled = yes
|
||||
@@ -341,17 +396,17 @@ with all other peers on a local area network.
|
||||
Using broadcast UDP traffic has performance implications,
|
||||
especially on WiFi. If your goal is simply to enable easy communication
|
||||
with all peers in your local Ethernet broadcast domain, the
|
||||
:ref:`Auto Interface<interfaces-auto>` performs better, and is even
|
||||
:ref:`Auto Interface<interfaces-auto>` performs *much* better, and is even
|
||||
easier to use.
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
# This example enables communication with other
|
||||
# local Reticulum peers over UDP.
|
||||
|
||||
[[UDP Interface]]
|
||||
type = UDPInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
listen_ip = 0.0.0.0
|
||||
listen_port = 4242
|
||||
@@ -389,6 +444,74 @@ with all other peers on a local area network.
|
||||
# forward_port = 4242
|
||||
|
||||
|
||||
.. _interfaces-i2p:
|
||||
|
||||
I2P Interface
|
||||
=============
|
||||
|
||||
The I2P interface lets you connect Reticulum instances over the
|
||||
`Invisible Internet Protocol <https://i2pd.website>`_. This can be
|
||||
especially useful in cases where you want to host a globally reachable
|
||||
Reticulum instance, but do not have access to any public IP addresses,
|
||||
have a frequently changing IP address, or have firewalls blocking
|
||||
inbound traffic.
|
||||
|
||||
Using the I2P interface, you will get a globally reachable, portable
|
||||
and persistent I2P address that your Reticulum instance can be reached
|
||||
at.
|
||||
|
||||
To use the I2P interface, you must have an I2P router running
|
||||
on your system. The easiest way to achieve this is to download and
|
||||
install the `latest release <https://github.com/PurpleI2P/i2pd/releases/latest>`_
|
||||
of the ``i2pd`` package. For more details about I2P, see the
|
||||
`geti2p.net website <https://geti2p.net/en/about/intro>`_.
|
||||
|
||||
When an I2P router is running on your system, you can simply add
|
||||
an I2P interface to Reticulum:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[I2P]]
|
||||
type = I2PInterface
|
||||
enabled = yes
|
||||
connectable = yes
|
||||
|
||||
On the first start, Reticulum will generate a new I2P address for the
|
||||
interface and start listening for inbound traffic on it. This can take
|
||||
a while the first time, especially if your I2P router was also just
|
||||
started, and is not yet well-connected to the I2P network. When ready,
|
||||
you should see I2P base32 address printed to your log file. You can
|
||||
also inspect the status of the interface using the ``rnstatus`` utility.
|
||||
|
||||
To connect to other Reticulum instances over I2P, just add a comma-separated
|
||||
list of I2P base32 addresses to the ``peers`` option of the interface:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[I2P]]
|
||||
type = I2PInterface
|
||||
enabled = yes
|
||||
connectable = yes
|
||||
peers = 5urvjicpzi7q3ybztsef4i5ow2aq4soktfj7zedz53s47r54jnqq.b32.i2p
|
||||
|
||||
It can take anywhere from a few seconds to a few minutes to establish
|
||||
I2P connections to the desired peers, so Reticulum handles the process
|
||||
in the background, and will output relevant events to the log.
|
||||
|
||||
.. note::
|
||||
While the I2P interface is the simplest way to use
|
||||
Reticulum over I2P, it is also possible to tunnel the TCP server and
|
||||
client interfaces over I2P manually. This can be useful in situations
|
||||
where more control is needed, but requires manual tunnel setup through
|
||||
the I2P daemon configuration.
|
||||
|
||||
It is important to note that the two methods are *interchangably compatible*.
|
||||
You can use the I2PInterface to connect to a TCPServerInterface that
|
||||
was manually tunneled over I2P, for example. This offers a high degree
|
||||
of flexibility in network setup, while retaining ease of use in simpler
|
||||
use-cases.
|
||||
|
||||
|
||||
.. _interfaces-rnode:
|
||||
|
||||
RNode LoRa Interface
|
||||
@@ -402,7 +525,7 @@ can be used, and offers full control over LoRa parameters.
|
||||
varies widely around the world. It is your responsibility to be aware of any
|
||||
relevant regulation for your location, and to make decisions accordingly.
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
# Here's an example of how to add a LoRa interface
|
||||
# using the RNode LoRa transceiver.
|
||||
@@ -411,11 +534,20 @@ can be used, and offers full control over LoRa parameters.
|
||||
type = RNodeInterface
|
||||
|
||||
# Enable interface if you want use it!
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB0
|
||||
|
||||
# You can connect wirelessly to the
|
||||
# RNode device if it supports WiFi.
|
||||
|
||||
# Connect by IP address
|
||||
# port = tcp://10.0.0.1
|
||||
|
||||
# Or, connect by hostname
|
||||
# port = tcp://rnodef3b9.local
|
||||
|
||||
# It is also possible to use BLE devices
|
||||
# instead of wired serial ports. The
|
||||
# target RNode must be paired with the
|
||||
@@ -494,7 +626,7 @@ Multi interface can be used to configure sub-interfaces individually.
|
||||
varies widely around the world. It is your responsibility to be aware of any
|
||||
relevant regulation for your location, and to make decisions accordingly.
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
# Here's an example of how to add an RNode Multi interface
|
||||
# using the RNode LoRa transceiver.
|
||||
@@ -503,7 +635,7 @@ Multi interface can be used to configure sub-interfaces individually.
|
||||
type = RNodeMultiInterface
|
||||
|
||||
# Enable interface if you want to use it!
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyACM0
|
||||
@@ -519,7 +651,7 @@ Multi interface can be used to configure sub-interfaces individually.
|
||||
# A subinterface
|
||||
[[[High Datarate]]]
|
||||
# Subinterfaces can be enabled and disabled in of themselves
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Set frequency to 2.4GHz
|
||||
frequency = 2400000000
|
||||
@@ -561,7 +693,7 @@ Multi interface can be used to configure sub-interfaces individually.
|
||||
|
||||
[[[Low Datarate]]]
|
||||
# Subinterfaces can be enabled and disabled in of themselves
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Set frequency to 865.6 MHz
|
||||
frequency = 865600000
|
||||
@@ -610,11 +742,11 @@ Reticulum can be used over serial ports directly, or over any device with a
|
||||
serial port, that will transparently pass data. Useful for communicating
|
||||
directly over a wire-pair, or for using devices such as data radios and lasers.
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
[[Serial Interface]]
|
||||
type = SerialInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB0
|
||||
@@ -635,11 +767,11 @@ Using this interface, Reticulum can use any program as an interface via `stdin`
|
||||
`stdout`. This can be used to easily create virtual interfaces, or to interface with
|
||||
custom hardware or other systems.
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
[[Pipe Interface]]
|
||||
type = PipeInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# External command to execute
|
||||
command = netcat -l 5757
|
||||
@@ -666,11 +798,11 @@ for station identification purposes.
|
||||
varies widely around the world. It is your responsibility to be aware of any
|
||||
relevant regulation for your location, and to make decisions accordingly.
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
[[Packet Radio KISS Interface]]
|
||||
type = KISSInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB1
|
||||
@@ -734,7 +866,7 @@ beaconing functionality described above.
|
||||
varies widely around the world. It is your responsibility to be aware of any
|
||||
relevant regulation for your location, and to make decisions accordingly.
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
[[Packet Radio AX.25 KISS Interface]]
|
||||
type = AX25KISSInterface
|
||||
@@ -744,7 +876,7 @@ beaconing functionality described above.
|
||||
ssid = 0
|
||||
|
||||
# Enable interface if you want use it!
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB2
|
||||
@@ -779,6 +911,213 @@ beaconing functionality described above.
|
||||
# small internal packet buffer.
|
||||
flow_control = false
|
||||
|
||||
.. _interfaces-discoverable:
|
||||
|
||||
Discoverable Interfaces
|
||||
=======================
|
||||
|
||||
Reticulum includes a powerful system for publishing your local interfaces to the wider network, allowing other peers to :ref:`discover, validate, and automatically connect to them<using-interface_discovery>`. This feature is particularly useful for creating decentralized networks where peers can dynamically find entrypoints, such as public Internet gateways or local radio access points, without relying on static configuration files or centralized directories.
|
||||
|
||||
When an interface is made **discoverable**, your Reticulum instance will periodically broadcast an announce packet containing the connection details and parameters required for other peers to establish a connection. These announces are propagated over the network using the standard Reticulum announce mechanism using the ``rnstransport.discovery.interface`` destination type.
|
||||
|
||||
.. note::
|
||||
To use the interface discovery functionality, the ``LXMF`` module must be installed in your Python environment. You can install it using pip:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
pip install lxmf
|
||||
|
||||
|
||||
Enabling Discovery
|
||||
------------------
|
||||
|
||||
Interface discovery is enabled on a per-interface basis. To make a specific interface discoverable, you must add the ``discoverable`` option to that interface's configuration block and set it to ``yes``.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[My Public Gateway]]
|
||||
type = BackboneInterface
|
||||
...
|
||||
discoverable = yes
|
||||
|
||||
Once enabled, Reticulum will automatically handle the generation, signing, stamping, and broadcasting of the discovery announces. It is not *required* to enable Transport to publish interface discovery information, but for most use cases where you want others to connect to you, you will likely want ``enable_transport`` set to ``yes`` in the ``[reticulum]`` section of your configuration.
|
||||
|
||||
|
||||
Discovery Parameters
|
||||
--------------------
|
||||
|
||||
When ``discoverable`` is enabled, a variety of additional options become available to control how the interface is presented to the network. These parameters allow you to fine-tune the metadata, security requirements, and visibility of your interface.
|
||||
|
||||
**Basic Metadata**
|
||||
|
||||
``discovery_name``
|
||||
A human-readable name for the interface. This name will be displayed to users on remote systems when they list discovered interfaces. If not specified, the interface name (the section header) will be used.
|
||||
|
||||
``announce_interval``
|
||||
The interval in minutes between successive discovery announces for this interface. Default is 360 minutes (6 hours). For stable, long-running infrastructure, higher intervals (12 to 22 hours) are usually sufficient and reduce network load. Minimum allowed value is 5 minutes (but expect to have your announces throttled if using intervals below one hour).
|
||||
|
||||
**Connectivity Specification**
|
||||
|
||||
``reachable_on``
|
||||
Specifies the address that remote peers should use to connect to this interface.
|
||||
|
||||
* For TCP and Backbone interfaces, this is typically the public IP address or hostname. Do not include the port, this is fetched automatically from the interface.
|
||||
* For I2P interfaces, this is usually the I2P ``b32`` address. This value is fetched automatically from the ``I2PInterface`` once it is up and connected to the I2P network, so you should not set this manually, unless you absolutely know what you're doing.
|
||||
|
||||
**Dynamic Resolution:** This option also accepts a path to an external executable script or binary. If a path is provided, Reticulum will execute the script and use its ``stdout`` as the reachability address. This is useful for devices behind dynamic DNS, NATs, or complex cloud environments where the external IP is not known locally. The script must simply print the address to stdout and exit.
|
||||
|
||||
.. note::
|
||||
When using an executable script for ``reachable_on``, Reticulum expects the script to output only the IP address or hostname to ``stdout``, followed by a newline character. Any additional output or errors may cause the resolution to fail. Ensure the script has executable permissions and is robust against temporary network failures.
|
||||
|
||||
A minimal example of a script that resolves the externally available, public IP of an internet-connected system could look like this:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
#!/bin/bash
|
||||
curl -s ip.me
|
||||
exit $?
|
||||
|
||||
On a real system, you should make the script robust enough to deal with intermittent Internet or service failures, such that the script *always* returns a sensible value, or if not possible at least exits with a non-zero exit return code, so Reticulum knows the output is invalid.
|
||||
|
||||
**Security & Cost**
|
||||
|
||||
``discovery_stamp_value``
|
||||
Defines the proof-of-work difficulty for the cryptographic stamp included in the announce. This value acts as a cost barrier to prevent network flooding. The default value is ``14``. Increasing this value makes it computationally more expensive to generate an announce, which can be useful to prevent spam on very large networks, but it also increases CPU load on your system when generating announces. Stamps are cached, and only generated if interface information changes, or at instance restart. If you have the computational resources, it is generally advisable to use as high a stamp value as possible.
|
||||
|
||||
**Privacy & Encryption**
|
||||
|
||||
``discovery_encrypt``
|
||||
If set to ``yes``, the discovery announce payload will be encrypted. To decrypt the announce, remote peers must possess the *network identity* configured for your instance (see ``network_identity`` in the ``[reticulum]`` section). This allows you to publish private interfaces that are only discoverable to specific trusted networks.
|
||||
|
||||
.. important::
|
||||
If you enable ``discovery_encrypt`` but do not configure a valid ``network_identity`` in the ``[reticulum]`` section of your configuration, Reticulum will abort the interface discovery announce. Encryption requires a valid network identity key to function.
|
||||
|
||||
``publish_ifac``
|
||||
If set to ``yes``, the Interface Access Code (IFAC) name and passphrase for this interface will be included in the discovery announce. This allows peers to automatically configure the correct authentication parameters when connecting to the interface.
|
||||
|
||||
**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.
|
||||
|
||||
**Radio Parameters**
|
||||
|
||||
For physical radio interfaces like ``RNodeInterface`` or ``KISSInterface``, the following optional parameters allow you to broadcast the operating frequency and characteristics, allowing clients to verify compatibility before connecting:
|
||||
|
||||
``discovery_frequency``
|
||||
The operating frequency in Hz. Auto-configured on RNode interfaces. Necessary on KISS-based radio interfaces and ``TCPClientInterfaces`` connecting to radio modems.
|
||||
|
||||
``discovery_bandwidth``
|
||||
The signal bandwidth in Hz. Auto-configured on RNode interfaces. Useful on KISS-based radio interfaces and ``TCPClientInterfaces`` connecting to radio modems.
|
||||
|
||||
``discovery_modulation``
|
||||
The modulation type or scheme. Auto-configured on RNode interfaces, but highly advisable to include on other radio-based interfaces.
|
||||
|
||||
|
||||
Interface Modes
|
||||
---------------
|
||||
|
||||
When you enable discovery on an interface, Reticulum enforces certain interface modes to ensure the interface is actually useful for remote peers.
|
||||
|
||||
If an interface is configured as ``discoverable``, but its mode is not explicitly set to ``gateway`` (for server-style interfaces like ``BackboneInterface`` or ``TCPServerInterface``) or ``access_point`` (for radio interfaces like ``RNodeInterface``), Reticulum will automatically configure the appropriate mode and log a notice.
|
||||
|
||||
For example, if you enable discovery on a ``RNodeInterface`` without specifying the mode, Reticulum will automatically set it to ``access_point`` mode.
|
||||
|
||||
Security Considerations
|
||||
-----------------------
|
||||
|
||||
When making interfaces discoverable, you are effectively broadcasting an invitation to connect to your system. It is important to understand the security implications of the configuration options you choose.
|
||||
|
||||
**Publishing Credentials**
|
||||
|
||||
If you enable ``publish_ifac = yes``, your interface's authentication passphrase will be included in the announce. If you are operating a public network and want anyone to connect, this is acceptable. However, if you wish to restrict access to a specific group of users, you **must** enable ``discovery_encrypt = yes``. This ensures that only peers possessing the correct ``network_identity`` can decode the passphrase.
|
||||
|
||||
**Topology Exposure**
|
||||
|
||||
A discoverable interface announces its presence, location (if configured), and capabilities to the network. Even if the connection details are encrypted, the *fact* that a connectable node exists within a certain network becomes public information. In high-security or scenarios requiring operational secrecy, consider the implications of advertising your infrastructure's existence.
|
||||
|
||||
Example Configuration
|
||||
---------------------
|
||||
|
||||
Below is an example configuration for a public backbone gateway. This configuration publishes a high-value, publicly discoverable interface, that anyone can connect to.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[My Public Gateway]]
|
||||
type = BackboneInterface
|
||||
mode = gateway
|
||||
listen_on = 0.0.0.0
|
||||
port = 4242
|
||||
|
||||
# Enable Discovery
|
||||
discoverable = yes
|
||||
|
||||
# Interface Details
|
||||
discovery_name = Region A Public Entrypoint
|
||||
announce_interval = 720
|
||||
|
||||
# Use external script to resolve dynamic IP
|
||||
reachable_on = /usr/local/bin/get_external_ip.sh
|
||||
|
||||
# Generate high stamp value
|
||||
discovery_stamp_value = 24
|
||||
|
||||
# Optional location data
|
||||
latitude = 51.99714
|
||||
longitude = -0.74195
|
||||
height = 15
|
||||
|
||||
The next example create an encrypted discovery-enabled interface, requiring a specific network identity to decode, and includes IFAC credentials for seamless authentication.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[[My Private Gateway]]
|
||||
type = BackboneInterface
|
||||
mode = gateway
|
||||
listen_on = 0.0.0.0
|
||||
port = 5858
|
||||
network_name = internal_1
|
||||
passphrase = Mevpekyafshak5Wr
|
||||
|
||||
# Enable Discovery
|
||||
discoverable = yes
|
||||
|
||||
# Interface Details
|
||||
discovery_name = Region A Private Backbone
|
||||
announce_interval = 720
|
||||
|
||||
# Use external script to resolve dynamic IP
|
||||
reachable_on = /usr/local/bin/get_external_ip.sh
|
||||
|
||||
# Target stamp value
|
||||
discovery_stamp_value = 22
|
||||
|
||||
# Encrypt announces for our network only
|
||||
discovery_encrypt = yes
|
||||
|
||||
# Include credentials so trusted
|
||||
# peers can connect automatically
|
||||
publish_ifac = yes
|
||||
|
||||
# Optional location data
|
||||
latitude = 34.06915
|
||||
longitude = -118.44318
|
||||
height = 15
|
||||
|
||||
In the ``[reticulum]`` section of your configuration, you would define the network identity used for encryption as follows:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
# The identity used to sign/encrypt discovery announces
|
||||
network_identity = ~/.reticulum/storage/identities/my_network_identity
|
||||
...
|
||||
|
||||
With these configuration options applied, your Reticulum instance will actively participate in the network's discovery ecosystem. Other peers running Reticulum with discovery enabled will be able to see your interface, validate its cryptographic stamp, and (depending on their configuration) automatically connect to it.
|
||||
|
||||
For information on how to use these discovered interfaces and configure your system to auto-connect to them, refer to the :ref:`Discovering Interfaces<using-interface_discovery>` chapter.
|
||||
|
||||
.. _interfaces-options:
|
||||
|
||||
Common Interface Options
|
||||
@@ -859,6 +1198,15 @@ These can be used to control various aspects of interface behaviour.
|
||||
option, to set the interface speed in *bits per second*.
|
||||
|
||||
|
||||
* | The ``bootstrap_only`` option designates an interface as a temporary
|
||||
bridge for initial connectivity. If this option is enabled, the
|
||||
interface will be monitored and automatically detached once the
|
||||
number of auto-connected interfaces reaches the limit configured by
|
||||
``autoconnect_discovered_interfaces``. This is particularly useful
|
||||
for using a slow or expensive connection (such as a single LoRa
|
||||
link or a remote TCP tunnel) solely to discover better local
|
||||
infrastructure, which then supersedes the bootstrap interface.
|
||||
|
||||
.. _interfaces-modes:
|
||||
|
||||
Interface Modes
|
||||
|
||||
@@ -4,17 +4,47 @@
|
||||
Building Networks
|
||||
*****************
|
||||
|
||||
This chapter will provide you with the knowledge needed to build networks with
|
||||
Reticulum, which can often be easier than using traditional stacks, since you
|
||||
don't have to worry about coordinating addresses, subnets and routing for an
|
||||
This chapter will provide you with the high-level knowledge needed to build networks with
|
||||
Reticulum. It will not, however tell you all you need to know to succesfully
|
||||
design and configure every kind of network you can imagine. For this, you will
|
||||
most likely need to read this manual in its entirity, invest significant time
|
||||
into experimenting with the stack, and learning functionality intuitively.
|
||||
|
||||
Still, after reading this chapter, you should be well equipped to *start* that
|
||||
journey. While Reticulum is **fundamentally different** compared to other
|
||||
networking technologies, it can often be easier than using traditional stacks.
|
||||
If you've built networks before, you will probably have to forget, or at least
|
||||
temporarily ignore, a lot of things at this point. It will all makes sense in
|
||||
the end though. Hopefully.
|
||||
|
||||
If you're used to protocols like IP, let's at least start with some relief:
|
||||
You don't have to worry about coordinating addresses, subnets and routing for an
|
||||
entire network that you might not know how will evolve in the future. With
|
||||
Reticulum, you can simply add more segments to your network when it becomes
|
||||
necessary, and Reticulum will handle the convergence of the entire network
|
||||
automatically.
|
||||
automatically. There's plenty more neat aspects like that to Reticulum, but
|
||||
we're getting ahead of ourselves. Let's cover the basics first.
|
||||
|
||||
Concepts & Overview
|
||||
--------------------
|
||||
|
||||
Before you start building your own networks, it's important to understand the
|
||||
fundamental principles that distinguish Reticulum networks from traditional
|
||||
networking approaches. These principles shape how you design your network,
|
||||
what trade-offs you encounter, and what capabilities you can rely on.
|
||||
|
||||
Reticulum is not a single network you "join", it is a toolkit for *creating* networks.
|
||||
You decide what mediums to use, how nodes connect, what trust boundaries exist,
|
||||
and what the network's purpose is. Reticulum provides the cryptographic foundation,
|
||||
the transport mechanisms, and the convergence algorithms that make your design
|
||||
workable. You provide the intent and the structure.
|
||||
|
||||
This approach offers tremendous flexibility, but it requires thinking in terms of
|
||||
different abstractions than those used in conventional networking.
|
||||
|
||||
Introductory Considerations
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There are important points that need to be kept in mind when building networks
|
||||
with Reticulum:
|
||||
|
||||
@@ -31,6 +61,11 @@ with Reticulum:
|
||||
interconnect with much larger and higher bandwidth networks without issue.
|
||||
Reticulum automatically manages the flow of information to and from various
|
||||
network segments, and when bandwidth is limited, local traffic is prioritised.
|
||||
You will, however, need to configure your interfaces correctly. If you tell
|
||||
Reticulum to pass all announce traffic from a gigabit link to a LoRa interfaces,
|
||||
it will try as best as possible to comply with this, while still respecting
|
||||
bandwidth limits, but you *will* waste a lot of precious bandwidth and airtime,
|
||||
and your LoRa network will not work very well.
|
||||
|
||||
* | Reticulum provides sender/initiator anonymity by default. There is no way
|
||||
to filter traffic or discriminate it based on the source of the traffic.
|
||||
@@ -89,81 +124,227 @@ Any number of interfaces can be configured, and Reticulum will automatically
|
||||
decide which are suitable to use in any given situation, depending on where
|
||||
traffic needs to flow.
|
||||
|
||||
Example Scenarios
|
||||
-----------------
|
||||
Destinations, Not Addresses
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
This section illustrates a few example scenarios, and how they would, in general
|
||||
terms, be planned, implemented and configured.
|
||||
In traditional networking, addresses are allocated from a managed space. If you want to
|
||||
communicate with another node, you need to know its address, and that address
|
||||
must be unique within the network segment. This requires coordination, either
|
||||
through manual assignment, DHCP servers, or other allocation mechanisms.
|
||||
|
||||
Interconnected LoRa Sites
|
||||
=========================
|
||||
Reticulum replaces addresses with **destinations**. A destination is identified by a 16-byte
|
||||
hash (128 bits) derived from a SHA-256 hash of the destination's identifying
|
||||
characteristics. This hash serves as the address on the network. On the network, it
|
||||
is represented in binary, but when displayed to human users, it will usually look something like
|
||||
this ``<13425ec15b621c1d928589718000d814>``.
|
||||
|
||||
An organisation wants to provide communication and information services to it's
|
||||
members, which are located mainly in three separate areas. Three suitable hill-top
|
||||
locations are found, where the organisation can install equipment: Site A, B and C.
|
||||
The critical difference is that *any node can generate as many destinations as it
|
||||
needs, without coordination*. A destination's uniqueness is guaranteed by the
|
||||
collision resistance of SHA-256 and the inclusion of the node's public key in the
|
||||
hash calculation. Two nodes can both use the destination name
|
||||
``messenger.user.inbox``, but they will have different destination hashes because
|
||||
their public keys differ. Both can coexist on the same network without conflict.
|
||||
|
||||
Since the amount of data that needs to be exchanged between users is mainly text-
|
||||
based, the bandwidth requirements are low, and LoRa radios are chosen to connect
|
||||
users to the network.
|
||||
This has profound implications for network design:
|
||||
|
||||
Due to the hill-top locations found, there is radio line-of-sight between site A
|
||||
and B, and also between site B and C. Because of this, the organisation does not
|
||||
need to use the Internet to interconnect the sites, but purchases four Point-to-Point
|
||||
WiFi based radios for interconnecting the sites.
|
||||
* **No address allocation planning:** You never need to reserve address ranges,
|
||||
plan subnets, or coordinate with other network operators. Nodes simply generate
|
||||
destinations and announce them.
|
||||
|
||||
At each site, a Raspberry Pi is installed to function as a gateway. A LoRa radio
|
||||
is connected to the Pi with a USB cable, and the WiFi radio is connected to the
|
||||
Ethernet port of the Pi. At site B, two WiFi radios are needed to be able to reach
|
||||
both site A and site C, so an extra Ethernet adapter is connected to the Pi in
|
||||
this location.
|
||||
* **Global portability:** A destination is not tied to a physical location or
|
||||
network segment. A node can move its destinations across interfaces, mediums,
|
||||
or even between entirely separate Reticulum networks simply by sending an
|
||||
announce on the new medium.
|
||||
|
||||
Once the hardware has been installed, Reticulum is installed on all the Pis, and at
|
||||
site A and C, one interface is added for the LoRa radio, as well as one for the WiFi
|
||||
radio. At site B, an interface for the LoRa radio, and one interface for each WiFi
|
||||
radio is added to the Reticulum configuration file. The transport node option is
|
||||
enabled in the configuration of all three gateways.
|
||||
* **Implicit authentication:** Because destinations are bound to public keys,
|
||||
communication to a destination is inherently cryptographically authenticated.
|
||||
Only the holder of the corresponding private key can decrypt and respond to
|
||||
traffic addressed to that destination. This also makes application-level
|
||||
authentication *much* simpler, since it can directly use the foundational
|
||||
identity verification built into the core networking layer.
|
||||
|
||||
The network is now operational, and ready to serve users across all three areas.
|
||||
The organisation prepares a LoRa radio that is supplied to the end users, along
|
||||
with a Reticulum configuration file, that contains the right parameters for
|
||||
communicating with the LoRa radios installed at the gateway sites.
|
||||
* **Identity abstraction:** A single Reticulum Identity can create multiple
|
||||
destinations. This allows a single entity (a person, a device, a service) to
|
||||
present multiple endpoints without needing multiple cryptographic keypairs.
|
||||
|
||||
Once users connect to the network, anyone will be able to communicate with anyone
|
||||
else across all three sites.
|
||||
|
||||
Bridging Over the Internet
|
||||
==========================
|
||||
Transport Nodes and Instances
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
As the organisation grows, several new communities form in places too far away
|
||||
from the core network to be reachable over WiFi links. New gateways similar to those
|
||||
previously installed are set up for the new communities at the new sites D and E, but
|
||||
they are islanded from the core network, and only serve the local users.
|
||||
Reticulum distinguishes between two types of nodes: **Instances**
|
||||
and **Transport Nodes**. Every node running Reticulum is an Instance, but not
|
||||
every Instance is a Transport Node.
|
||||
|
||||
After investigating the options, it is found that it is possible to install an
|
||||
Internet connection at site A, and an interface on the Internet connection is
|
||||
configured for Reticulum on the Raspberry Pi at site A.
|
||||
A **Reticulum Instance** is any system running the Reticulum stack. It can create
|
||||
destinations, send and receive packets, establish links, and communicate with
|
||||
other nodes. It can also host destinations that are connectable for *anyone* else
|
||||
in the network. This means you can easily host globally available services from
|
||||
any location, including your home or office. Network-wide, global connectivity
|
||||
for all destinations is guaranteed, as long as there is *some* physical way to
|
||||
actually transport the packets. Instances are the default state and are appropriate for most end-user devices,
|
||||
such as phones, laptops, sensors, or any device that primarily consumes network services.
|
||||
|
||||
A member of the organisation at site D, named Dori, is willing to help by sharing
|
||||
the Internet connection she already has in her home, and is able to leave a Raspberry
|
||||
Pi running. A new Reticulum interface is configured on her Pi, connecting to the newly
|
||||
enabled Internet interface on the gateway at site A. Dori is now connected to both
|
||||
all the nodes at her own local site (through the hill-top LoRa gateway), and all the
|
||||
combined users of sites A, B and C. She then enables transport on her node, and
|
||||
traffic from site D can now reach everyone at site A, B and C, and vice versa.
|
||||
A **Transport Node** is an Instance that has been explicitly configured to
|
||||
participate in network-wide transport. Transport nodes forward packets across
|
||||
hops, propagate announces, maintain path tables, and serve path requests on
|
||||
behalf of other nodes. When a destination sends an announce, Transport Nodes
|
||||
receive it, remember the path, and rebroadcast it to other interfaces. When a node
|
||||
needs to reach a destination it doesn't have a path for, Transport Nodes help
|
||||
resolve the path through the network.
|
||||
|
||||
Growth and Convergence
|
||||
======================
|
||||
Even devices hosting services or serving content should probably just be configured
|
||||
as instances, and themselves connect to wider networks via a Transport Node.
|
||||
In some situations, this may not be practical though, and as an example, it is
|
||||
entirely viable to host a personal Transport Node on a Raspberry Pi, while it
|
||||
is at the same time running an LXMF propagation node, and hosting your personal
|
||||
site or files over Reticulum.
|
||||
|
||||
As the organisation grows, more gateways are added to keep up with the growing user
|
||||
base. Some local gateways even add VHF radios and packet modems to reach outlying users
|
||||
and communities that are out of reach for the LoRa radios and WiFi backhauls.
|
||||
The distinction is important. **Not** every node should be a Transport Node:
|
||||
|
||||
As more sites, gateways and users are connected, the amount of coordination required
|
||||
is kept to a minimum. If one community wants to add connectivity to the next one
|
||||
over, it can simply be done without having to involve everyone or coordinate address
|
||||
space or routing tables.
|
||||
* **Resource consumption:** Transport nodes maintain path tables, process
|
||||
announces, and forward traffic. This requires memory and CPU resources that
|
||||
may be limited on low-powered devices.
|
||||
|
||||
With the added geographical coverage, the operators at site A one day find that
|
||||
the original internet bridged interfaces are no longer utilised. The network has
|
||||
converged to be completely self-connected, and the sites that were once poorly
|
||||
connected outliers are now an integral part of the network.
|
||||
* **Stability requirements:** Transport nodes contribute to network convergence.
|
||||
If Transport Nodes frequently go offline, path tables become stale and
|
||||
convergence suffers. Stable, always-on nodes make better Transport Nodes.
|
||||
|
||||
* **Bandwidth considerations:** Transport nodes process and rebroadcast network
|
||||
maintenance traffic. On very low-bandwidth mediums, having too many Transport
|
||||
Nodes will consume capacity that should be used for actual data.
|
||||
|
||||
In practice, a network typically has a relatively small number of Transport Nodes
|
||||
strategically placed to provide coverage and connectivity. End-user devices run
|
||||
as Instances, connecting through nearby Transport Nodes to reach the wider network.
|
||||
This pattern mirrors traditional networking where routers forward traffic while
|
||||
end hosts simply consume connectivity, but with the crucial difference that any
|
||||
node *can* become a router if needed, and the decision is yours to make based on
|
||||
your network's requirements.
|
||||
|
||||
Transport nodes also function as distributed cryptographic keystores. When a
|
||||
destination announces itself, Transport Nodes cache the public key and destination
|
||||
information. Other nodes can request unknown public keys from the network, and
|
||||
Transport Nodes respond with the cached information. This eliminates the need for
|
||||
a central directory service while ensuring that public keys remain available
|
||||
throughout the network.
|
||||
|
||||
Trustless Networking
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Traditional network security models assume high levels of trust at
|
||||
specific layers. You might trust your ISP to deliver packets without inspection,
|
||||
or trust your VPN provider to handle your traffic, or trust the network
|
||||
administrator to configure firewalls appropriately. These trust relationships
|
||||
create vulnerabilities and dependencies.
|
||||
|
||||
Reticulum is designed to function in **open, trustless environments**. This
|
||||
means the protocol makes no assumptions about the trustworthiness of the network
|
||||
infrastructure, the other participants, or the transport mediums. Every aspect
|
||||
of communication is secured cryptographically:
|
||||
|
||||
* **Traffic encryption:** All traffic to single destinations is encrypted using
|
||||
ephemeral keys.
|
||||
|
||||
* **Source anonymity:** Reticulum packets do not include source addresses.
|
||||
An observer intercepting a packet cannot determine who sent it, only who it is
|
||||
addressed to (unless IFAC is enabled, in which case nothing can be determined).
|
||||
This provides initiator anonymity by default.
|
||||
|
||||
* **Path verification:** The announce mechanism includes cryptographic signatures that
|
||||
prove the authenticity of destination announcements.
|
||||
|
||||
* **Unforgeable delivery confirmations:** When a destination proves receipt of a
|
||||
packet, the proof is signed with the destination's identity key. This prevents
|
||||
false acknowledgments and ensures reliable delivery verification.
|
||||
|
||||
* **Interface authentication:** When using Interface Access Codes (IFAC), packets
|
||||
on authenticated interfaces carry signatures derived from a shared secret. Only
|
||||
nodes with the correct network name and passphrase can generate valid packets, allowing creation
|
||||
of virtual private networks on shared mediums.
|
||||
|
||||
The trustless design has important consequences for network design:
|
||||
|
||||
* **Open-access networks are viable:** You can build networks that anyone can
|
||||
join without pre-approval. Because traffic is encrypted and authenticated end-
|
||||
to-end, participants cannot interfere with each other's private communication,
|
||||
even if they share the same transport infrastructure.
|
||||
|
||||
* **No traffic inspection or prioritization:** Because traffic contents and
|
||||
sources are opaque to intermediate nodes, there is no mechanism for filtering,
|
||||
prioritizing, or throttling traffic based on its type or origin. All traffic
|
||||
is treated equally. From a neutrality perspective, this is a feature.
|
||||
|
||||
* **Adversarial resilience:** The network can operate even if some nodes are
|
||||
malicious or controlled by adversaries. While a malicious Transport Node could
|
||||
refuse to forward certain traffic or drop packets, it cannot decrypt, modify,
|
||||
or impersonate legitimate traffic. Redundant paths and multiple Transport Nodes
|
||||
mitigate the impact of malicious nodes.
|
||||
|
||||
Of course, you can also create closed networks. Interface Access
|
||||
Codes allow you to restrict participation on specific interfaces. Network
|
||||
Identities enable you to verify that discovered interfaces belong to trusted
|
||||
operators. Blackhole management lets you block malicious identities. Reticulum
|
||||
provides both the tools for open networks and the controls for closed ones. The
|
||||
choice is yours based on your requirements.
|
||||
|
||||
|
||||
Heterogeneous Connectivity
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In conventional networking, mixing different transport mediums typically requires
|
||||
gateways, translation layers, and careful configuration. A WiFi network doesn't
|
||||
natively interoperate with a packet radio network without additional infrastructure,
|
||||
and you can't just download a car over a serial port, or send an encrypted message
|
||||
in a QR code.
|
||||
|
||||
Reticulum treats **heterogeneity as a core premise**. The protocol is designed
|
||||
to seamlessly mix mediums with vastly different characteristics:
|
||||
|
||||
* **Bandwidth:** LoRa links operating at a few hundred bits per second can
|
||||
interconnect with gigabit Ethernet backbones. Reticulum automatically manages
|
||||
the flow of information, prioritizing local traffic on slow segments while
|
||||
allowing global convergence.
|
||||
|
||||
* **Latency:** Satellite links with multi-second latency can coexist with local
|
||||
links measured in milliseconds. The transport system handles timing, asynchronous
|
||||
delivery and retransmissions transparently.
|
||||
|
||||
* **Topology:** Point-to-point microwave links, broadcast radio networks,
|
||||
switched Ethernet fabrics, and virtual tunnels over the Internet can all be
|
||||
part of the same Reticulum network.
|
||||
|
||||
* **Reliability:** Intermittent connections that come and go (such as mobile
|
||||
devices or opportunistic radio contacts) can participate alongside always-on
|
||||
infrastructure. Reticulum gracefully handles link loss and reconnection.
|
||||
|
||||
This heterogeneity is achieved through several design elements:
|
||||
|
||||
* **Expandable, medium-agnostic interface system:** Reticulum communicates with the physical
|
||||
world through interface modules. Adding support for a new medium is a matter
|
||||
of implementing an interface class. The protocol itself remains unchanged.
|
||||
|
||||
* **Interface modes:** Different modes (``full``, ``gateway``, ``access_point``,
|
||||
``roaming``, ``boundary``) allow you to configure how interfaces interact with
|
||||
the wider network based on their characteristics and role.
|
||||
|
||||
* **Announce propagation rules:** Announces are forwarded between interfaces
|
||||
according to rules that account for bandwidth limitations and interface modes.
|
||||
Slow segments are not overwhelmed by traffic from fast segments.
|
||||
|
||||
* **Local traffic prioritization:** When bandwidth is constrained, Reticulum
|
||||
prioritizes announces for nearby destinations. This ensures that local
|
||||
connectivity remains functional even when global convergence is incomplete.
|
||||
|
||||
For network designers, this means you are free to use whatever mediums are
|
||||
available, affordable, or appropriate for your situation. You might use LoRa for
|
||||
wide-area low-bandwidth coverage, WiFi for local high-capacity links, I2P for
|
||||
anonymous Internet connectivity, and Ethernet for infrastructure backhauls, all
|
||||
within the same network. Reticulum handles the translation and coordination
|
||||
automatically.
|
||||
|
||||
The key design consideration is not whether different mediums can work together
|
||||
(they can), but **how** they should work together based on your goals. A node
|
||||
with multiple interfaces spanning heterogeneous mediums needs to be configured
|
||||
with appropriate interface modes so that traffic flows efficiently. A gateway
|
||||
connecting a slow LoRa segment to a fast Internet backbone should be configured
|
||||
differently than a mobile device roaming between radio cells.
|
||||
@@ -16,11 +16,14 @@ Donations are gratefully accepted via the following channels:
|
||||
Monero:
|
||||
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
||||
|
||||
Ethereum:
|
||||
0x81F7B979fEa6134bA9FD5c701b3501A2e61E897a
|
||||
|
||||
Bitcoin:
|
||||
3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq
|
||||
bc1pgqgu8h8xvj4jtafslq396v7ju7hkgymyrzyqft4llfslz5vp99psqfk3a6
|
||||
|
||||
Ethereum:
|
||||
0x91C421DdfB8a30a49A71d63447ddb54cEBe3465E
|
||||
|
||||
Liberapay:
|
||||
https://liberapay.com/Reticulum/
|
||||
|
||||
Ko-Fi:
|
||||
https://ko-fi.com/markqvist
|
||||
@@ -30,15 +33,28 @@ organisation? Make them a reality quickly by sponsoring their implementation.
|
||||
|
||||
Provide Feedback
|
||||
================
|
||||
All feedback on the usage, functioning and potential dysfunctioning of any and
|
||||
Feedback on the usage, functioning and potential dysfunctioning of any and
|
||||
all components of the system is very valuable to the continued development and
|
||||
improvement of Reticulum.
|
||||
improvement of Reticulum. But...
|
||||
|
||||
.. warning::
|
||||
|
||||
**Think before you speak**. As time has shown, over 80% of the "feedback",
|
||||
"bug reports" and "advice" the Reticulum project has received has been
|
||||
irrelevant noise, stemming from erroneous assumptions, misunderstanding the
|
||||
foundational functionality or philosophy behind the system, or simply
|
||||
the malinformed (but overly opinionated) personal preferences of individual
|
||||
drive-by architects. This wastes the time of everyone involved.
|
||||
|
||||
The Reticulum project is not a public teahouse for serving the attention
|
||||
needs of random bypassers, but a highly complex system engineered and
|
||||
refined over more than a decade, designed to provide communication and
|
||||
connectivity guarantees in highly adversarial environments.
|
||||
|
||||
If you want to voice your opinion, it better be well-informed, and we
|
||||
expect you to have a comprehensive and solid foundation for your points
|
||||
of view. Everything else will be ignored.
|
||||
|
||||
Absolutely no automated analytics, telemetry, error
|
||||
reporting or statistics is collected and reported by Reticulum under any
|
||||
circumstances, so we rely on old-fashioned human feedback.
|
||||
|
||||
Contribute Code
|
||||
===============
|
||||
Join us on `the GitHub repository <https://github.com/markqvist/reticulum>`_ to
|
||||
report issues, suggest functionality and contribute code to Reticulum.
|
||||
|
||||
@@ -13,9 +13,8 @@ reference implementation and API reference. That being said, this chapter is an
|
||||
understanding how Reticulum works from a high-level perspective, along with the general principles of
|
||||
Reticulum, and how to apply them when creating your own networks or software.
|
||||
|
||||
After reading this document, you should be well-equipped to understand how a Reticulum network
|
||||
operates, what it can achieve, and how you can use it yourself. If you want to help out with the
|
||||
development, this is also the place to start, since it will provide a pretty clear overview of the
|
||||
After reading this chapter, you should be well-equipped to understand how a Reticulum network
|
||||
operates, what it can achieve, and how you can use it yourself. This chapter also seeks to provide an overview of the
|
||||
sentiments and the philosophy behind Reticulum, what problems it seeks to solve, and how it
|
||||
approaches those solutions.
|
||||
|
||||
@@ -117,7 +116,7 @@ Reticulum uses the singular concept of *destinations*. Any application using Ret
|
||||
networking stack will need to create one or more destinations to receive data, and know the
|
||||
destinations it needs to send data to.
|
||||
|
||||
All destinations in Reticulum are _represented_ as a 16 byte hash. This hash is derived from truncating a full
|
||||
All destinations in Reticulum are *represented* as a 16 byte hash. This hash is derived from truncating a full
|
||||
SHA-256 hash of identifying characteristics of the destination. To users, the destination addresses
|
||||
will be displayed as 16 hexadecimal bytes, like this example: ``<13425ec15b621c1d928589718000d814>``.
|
||||
|
||||
@@ -141,7 +140,7 @@ ratchets on a per-destination basis. The multi-hop transport, coordination, veri
|
||||
layers are fully autonomous and also based on elliptic curve cryptography.
|
||||
|
||||
Reticulum also offers symmetric key encryption for group-oriented communications, as well as
|
||||
unencrypted packets for local broadcast purposes.
|
||||
unencrypted packets (for local broadcast purposes **only**).
|
||||
|
||||
Reticulum can connect to a variety of interfaces such as radio modems, data radios and serial ports,
|
||||
and offers the possibility to easily tunnel Reticulum traffic over IP links such as the Internet or
|
||||
@@ -401,11 +400,10 @@ any transport node receiving it, but according to some specific rules:
|
||||
to be transmitted, the newest announce is discarded. If the newest announce contains different
|
||||
application specific data, it will replace the old announce.
|
||||
|
||||
Once an announce has reached a node in the network, any other node in direct contact with that
|
||||
node will be able to reach the destination the announce originated from, simply by sending a packet
|
||||
addressed to that destination. Any node with knowledge of the announce will be able to direct the
|
||||
packet towards the destination by looking up the next node with the shortest amount of hops to the
|
||||
destination.
|
||||
Once an announce has reached a transport node in the network, any other node in direct contact with that
|
||||
transport node will be able to reach the destination the announce originated from, simply by sending a packet
|
||||
addressed to that destination. Any transport node with knowledge of the announce will be able to direct the
|
||||
packet towards the destination by looking up the most efficient next node to the destination.
|
||||
|
||||
According to these rules, an announce will propagate throughout the network in a predictable way,
|
||||
and make the announced destination reachable in a short amount of time. Fast networks that have the
|
||||
@@ -414,6 +412,17 @@ new destinations. Slower segments of such networks might take a bit longer to ga
|
||||
the wide and fast networks they are connected to, but can still do so over time, while prioritising full
|
||||
and quickly converging end-to-end connectivity for their local, slower segments.
|
||||
|
||||
.. tip::
|
||||
|
||||
Even very slow networks, that simply don't have the capacity to ever reach *full* convergence
|
||||
will generally still be able to reach **any other destination on any connected segments**, since
|
||||
interconnecting transport nodes will prioritize announces into the slower segments that are
|
||||
actually requested by nodes on these.
|
||||
|
||||
This means that slow, low-capacity or low-resource segments **don't** need to have full network
|
||||
knowledge, since paths can always be recursively resolved from other segments that do have
|
||||
knowledge about them.
|
||||
|
||||
In general, even extremely complex networks, that utilize the maximum 128 hops will converge to full
|
||||
end-to-end connectivity in about one minute, given there is enough bandwidth available to process
|
||||
the required amount of announces.
|
||||
@@ -424,7 +433,7 @@ Reaching the Destination
|
||||
------------------------
|
||||
|
||||
In networks with changing topology and trustless connectivity, nodes need a way to establish
|
||||
*verified connectivity* with each other. Since the network is assumed to be trustless, Reticulum
|
||||
*verified connectivity* with each other. Since the underlying network mediums are assumed to be trustless, Reticulum
|
||||
must provide a way to guarantee that the peer you are communicating with is actually who you
|
||||
expect. Reticulum offers two ways to do this.
|
||||
|
||||
@@ -435,7 +444,7 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
|
||||
an ECDH key exchange with the destination's public key (or ratchet key, if available), and encrypt the information.
|
||||
|
||||
* | It is important to note that this key exchange does not require any network traffic. The sender already
|
||||
knows the public key of the destination from an earlier received *announce*, and can thus perform the ECDH
|
||||
knows the public key of the destination from an earlier received announce, and can thus perform the ECDH
|
||||
key exchange locally, before sending the packet.
|
||||
|
||||
* | The public part of the newly generated ephemeral key-pair is included with the encrypted token, and sent
|
||||
@@ -453,7 +462,7 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
|
||||
public signing key.
|
||||
|
||||
* | In case the packet is addressed to a *group* destination type, the packet will be encrypted with the
|
||||
pre-shared AES-128 key associated with the destination. In case the packet is addressed to a *plain*
|
||||
pre-shared AES-256 key associated with the destination. In case the packet is addressed to a *plain*
|
||||
destination type, the payload data will not be encrypted. Neither of these two destination types can offer
|
||||
forward secrecy. In general, it is recommended to always use the *single* destination type, unless it is
|
||||
strictly necessary to use one of the others.
|
||||
@@ -461,14 +470,14 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API
|
||||
|
||||
For exchanges of larger amounts of data, or when longer sessions of bidirectional communication is desired, Reticulum offers the *Link* API. To establish a *link*, the following process is employed:
|
||||
|
||||
* | First, the node that wishes to establish a link will send out a special packet, that
|
||||
* | First, the node that wishes to establish a link will send out a *link request* packet, that
|
||||
traverses the network and locates the desired destination. Along the way, the Transport Nodes that
|
||||
forward the packet will take note of this *link request*.
|
||||
forward the packet will take note of this *link request*, and mark it as pending.
|
||||
|
||||
* | Second, if the destination accepts the *link request* , it will send back a packet that proves the
|
||||
authenticity of its identity (and the receipt of the link request) to the initiating node. All
|
||||
nodes that initially forwarded the packet will also be able to verify this proof, and thus
|
||||
accept the validity of the *link* throughout the network.
|
||||
accept the validity of the *link* throughout the network. The link is now marked as *established*.
|
||||
|
||||
* | When the validity of the *link* has been accepted by forwarding nodes, these nodes will
|
||||
remember the *link* , and it can subsequently be used by referring to a hash representing it.
|
||||
@@ -560,9 +569,10 @@ an arbitrary number of hops, where information will be exchanged between two nod
|
||||
*link proof* to perform it's own Diffie Hellman Key Exchange and derive the symmetric key
|
||||
that is used to encrypt the channel. Information can now be exchanged reliably and securely.
|
||||
|
||||
.. note::
|
||||
|
||||
It’s important to note that this methodology ensures that the source of the request does not need to
|
||||
reveal any identifying information about itself. The link initiator remains completely anonymous.
|
||||
It’s important to note that this methodology ensures that the source of the request does not need to
|
||||
reveal any identifying information about itself. **The link initiator remains completely anonymous**.
|
||||
|
||||
When using *links*, Reticulum will automatically verify all data sent over the link, and can also
|
||||
automate retransmissions if *Resources* are used.
|
||||
@@ -585,6 +595,82 @@ the transfer, integrity verification and reassembling the data on the other end.
|
||||
of codes to reliably transfer any amount of data. They can be used to transfer data stored in memory,
|
||||
or stream data directly from files.
|
||||
|
||||
.. _understanding-network_identities:
|
||||
|
||||
Network Identities
|
||||
==================
|
||||
|
||||
In Reticulum, every peer and application utilizes a cryptographic **Identity** to verify authenticity and establish encrypted channels. While standard identities are typically used to represent a single user, device, or service, Reticulum introduces the concept of a **Network Identity** to represent a logical group of nodes or an entire community infrastructure.
|
||||
|
||||
A Network Identity is, at its core, a standard Reticulum Identity keyset. However, its purpose and usage differ from a personal identity. Instead of identifying a single entity, a Network Identity acts as a shared credential that federates multiple independent Transport Instances under a single, verifiable administrative domain.
|
||||
|
||||
|
||||
Conceptual Overview
|
||||
-------------------
|
||||
|
||||
You can think of a standard Reticulum Identity as a self-sovereign, privately created passport for a single person. A Network Identity, conversely, is akin to a cryptographic flag, or a charter that flies over a fleet of ships. It signifies that while the ships may operate independently and be physically distant, they belong to the same organization, follow the same protocols, and are expected to act in concert.
|
||||
|
||||
When you configure a Network Identity on one or more of your nodes, you are effectively declaring that these nodes constitute a specific "network" within a broader Reticulum mesh. This allows other peers to recognize interfaces not just as "a node named Alice", but as "a gateway belonging to The Eastern Ret Of Freedom".
|
||||
|
||||
|
||||
Current Usage
|
||||
-------------
|
||||
|
||||
At present, the primary function of a Network Identity is within the :ref:`Interface Discovery<using-interface_discovery>` system.
|
||||
|
||||
When a Transport Instance broadcasts a discovery announce for an interface, it can optionally sign that announce with a Network Identity, instead of just its local transport identity. Remote peers receiving the announce can then verify the signature. This provides functionality for two important distinctions:
|
||||
|
||||
1. **Authenticity:** It proves that the interface was published by an operator who possesses the private key for that Network Identity.
|
||||
2. **Trust Boundaries:** It allows users to configure their systems to only accept and connect to interfaces that belong to specific Network Identities, effectively creating "whitelisted" zones of trusted infrastructure.
|
||||
|
||||
.. note::
|
||||
If you enable encryption on your discovery announces, the Network Identity is used as the shared secret. Only peers who have been explicitly provided with the Network Identity's full keyset (and have it configured locally) will be able to decrypt and utilize the connection details.
|
||||
|
||||
This functionality will be expanded in the future, so that peers with delegated keys can be allowed to decrypt discovery announces without holding the root network key. Currently, the functionality is sufficient for sharing interface information privately where you control all nodes that must decrypt the discovered interfaces.
|
||||
|
||||
|
||||
Future Implications
|
||||
-------------------
|
||||
|
||||
While the current implementation focuses on interface discovery, the concept of Network Identities serves as the foundational building block for future Reticulum features designed to support large-scale, organic mesh formation.
|
||||
|
||||
As the ecosystem evolves, Network Identities will facilitate:
|
||||
|
||||
* **Distributed Name Resolution:** A system where networks can publish name-to-identity mappings, allowing human-readable names to resolve without centralized servers.
|
||||
* **Service Publishing:** Networks will be able to announce specific capabilities, services, or information endpoints available publicly or to their members.
|
||||
* **Inter-Network Federation:** Trust relationships between different networks, allowing for seamless but managed flow of traffic and information across distinct administrative boundaries.
|
||||
* **Distributed Blackhole Management:** A reputation-based system for blackhole list distribution, where trusted Network Identities can sign and publish lists of blackholed identities. This allows communities to collaboratively enforce security standards and filter spam or malicious identities across the parts of the wider mesh that they are responsible for.
|
||||
|
||||
By adopting the use of Network Identities now, you are preparing your infrastructure to be compatible with this future functionality.
|
||||
|
||||
|
||||
Creating and Using a Network Identity
|
||||
-------------------------------------
|
||||
|
||||
Since a Network Identity is simply a standard Reticulum Identity, you create one using the built-in tools.
|
||||
|
||||
1. **Generate the Identity:**
|
||||
Use the ``rnid`` utility to generate a new identity file that will serve as your Network Identity.
|
||||
|
||||
.. code:: sh
|
||||
|
||||
$ rnid -g ~/.reticulum/storage/identities/my_network
|
||||
|
||||
2. **Distribute the Public Key:**
|
||||
The public key must be distributed to any Transport Instance that needs to verify your network's announces and discovery information. By default, if your node is set up to use a network identity, this happens automatically (using the standard announce mechanism).
|
||||
|
||||
3. **Configure Instances:**
|
||||
In the ``[reticulum]`` section of the configuration file on every node within your network, point the ``network_identity`` option to the file you created.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
network_identity = ~/.reticulum/storage/identities/my_network
|
||||
...
|
||||
|
||||
Once configured, your instances will automatically utilize this identity for signing discovery announces (and potentially decrypting network-private information), presenting a unified front to the wider network.
|
||||
|
||||
.. _understanding-referencesystem:
|
||||
|
||||
Reference Setup
|
||||
@@ -624,18 +710,20 @@ into the future. The current Reference System Setup is as follows:
|
||||
* **Interface Device**
|
||||
A data radio consisting of a LoRa radio module, and a microcontroller with open source
|
||||
firmware, that can connect to host devices via USB. It operates in either the 430, 868 or 900
|
||||
MHz frequency bands. More details can be found on the `RNode Page <https://unsigned.io/rnode>`_.
|
||||
MHz frequency bands. More details can be found on the `RNode Page <https://github.com/markqvist/rnode_firmware>`_.
|
||||
* **Host Device**
|
||||
Any computer device running Linux and Python. A Raspberry Pi with a Debian based OS is
|
||||
recommended.
|
||||
a good place to start, but anything can be used.
|
||||
* **Software Stack**
|
||||
The most recently released Python Implementation of Reticulum, running on a Debian based
|
||||
The most recently released Python Implementation of Reticulum, running on a Linux-based
|
||||
operating system.
|
||||
|
||||
To avoid confusion, it is very important to note, that the reference interface device **does not**
|
||||
use the LoRaWAN standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will
|
||||
need a plain LoRa radio module connected to an controller with the correct firmware. Full details on how to
|
||||
get or make such a device is available on the `RNode Page <https://unsigned.io/rnode>`_.
|
||||
.. note::
|
||||
|
||||
To avoid confusion, it is very important to note, that the reference interface device **does not**
|
||||
use the LoRaWAN standard, but uses a custom MAC layer on top of the plain LoRa modulation! As such, you will
|
||||
need a plain LoRa radio module connected to a controller with the correct firmware. Full details on how to
|
||||
get or make such a device is available on the `RNode Page <https://github.com/markqvist/rnode_firmware>`_.
|
||||
|
||||
With the current reference setup, it should be possible to get on a Reticulum network for around 100$
|
||||
even if you have none of the hardware already, and need to purchase everything.
|
||||
@@ -649,16 +737,16 @@ Protocol Specifics
|
||||
==================
|
||||
|
||||
This chapter will detail protocol specific information that is essential to the implementation of
|
||||
Reticulum, but non critical in understanding how the protocol works on a general level. It should be
|
||||
Reticulum, but non-critical in understanding how the protocol works on a general level. It should be
|
||||
treated more as a reference than as essential reading.
|
||||
|
||||
|
||||
Packet Prioritisation
|
||||
---------------------
|
||||
|
||||
Currently, Reticulum is completely priority-agnostic regarding general traffic. All traffic is handled
|
||||
on a first-come, first-serve basis. Announce re-transmission are handled according to the re-transmission
|
||||
times and priorities described earlier in this chapter.
|
||||
Currently, Reticulum is completely priority-agnostic regarding *general* traffic. All traffic is handled
|
||||
on a first-come, first-serve basis. Announce re-transmission and other maintenance traffic is handled
|
||||
according to the re-transmission times and priorities described earlier in this chapter.
|
||||
|
||||
|
||||
Interface Access Codes
|
||||
@@ -666,8 +754,8 @@ Interface Access Codes
|
||||
|
||||
Reticulum can create named virtual networks, and networks that are only accessible by knowing a preshared
|
||||
passphrase. The configuration of this is detailed in the :ref:`Common Interface Options<interfaces-options>`
|
||||
section. To implement these feature, Reticulum uses the concept of Interface Access Codes, that are calculated
|
||||
and verified per packet.
|
||||
section. To implement this feature, Reticulum uses the concept of Interface Access Codes, that are calculated
|
||||
and verified per-packet.
|
||||
|
||||
An interface with a named virtual network or passphrase authentication enabled will derive a shared Ed25519
|
||||
signing identity, and for every outbound packet generate a signature of the entire packet. This signature is
|
||||
@@ -858,9 +946,17 @@ of the different interface modes, and how they are configured.
|
||||
Cryptographic Primitives
|
||||
------------------------
|
||||
|
||||
Reticulum has been designed to use a simple suite of efficient, strong and modern
|
||||
cryptographic primitives, with widely available implementations that can be used
|
||||
both on general-purpose CPUs and on microcontrollers. The necessary primitives are:
|
||||
Reticulum uses a simple suite of efficient, strong and well-tested cryptographic
|
||||
primitives, with widely available implementations that can be used both on
|
||||
general-purpose CPUs and on microcontrollers.
|
||||
|
||||
One of the primary considerations for choosing this particular set of primitives is
|
||||
that they can be implemented *safely* with relatively few pitfalls, on practically
|
||||
all current computing platforms.
|
||||
|
||||
The primitives listed here **are authoritative**. Anything claiming to be Reticulum,
|
||||
but not using these exact primitives **is not** Reticulum, and possibly an
|
||||
intentionally compromised or weakened clone. The utilised primitives are:
|
||||
|
||||
* Ed25519 for signatures
|
||||
|
||||
@@ -872,11 +968,11 @@ both on general-purpose CPUs and on microcontrollers. The necessary primitives a
|
||||
|
||||
* Ephemeral keys derived from an ECDH key exchange on Curve25519
|
||||
|
||||
* AES-128 in CBC mode with PKCS7 padding
|
||||
* AES-256 in CBC mode with PKCS7 padding
|
||||
|
||||
* HMAC using SHA256 for message authentication
|
||||
|
||||
* IVs are generated through os.urandom()
|
||||
* IVs must be generated through ``os.urandom()`` or better
|
||||
|
||||
* No Fernet version and timestamp metadata fields
|
||||
|
||||
@@ -884,7 +980,7 @@ both on general-purpose CPUs and on microcontrollers. The necessary primitives a
|
||||
|
||||
* SHA-512
|
||||
|
||||
In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES-128-CBC``
|
||||
In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES-256-CBC``
|
||||
primitives are provided by `OpenSSL <https://www.openssl.org/>`_ (via the `PyCA/cryptography <https://github.com/pyca/cryptography>`_
|
||||
package). The hashing functions ``SHA-256`` and ``SHA-512`` are provided by the standard
|
||||
Python `hashlib <https://docs.python.org/3/library/hashlib.html>`_. The ``HKDF``, ``HMAC``,
|
||||
@@ -904,6 +1000,11 @@ with the OpenSSL backend being *much* faster. The most important consequence how
|
||||
potential loss of security by using primitives that has not seen the same amount of scrutiny,
|
||||
testing and review as those from OpenSSL.
|
||||
|
||||
Using the normal RNS installation procedures, it is not possible to install Reticulum on a
|
||||
system without the required OpenSSL primitives being available, and if they are not, they will
|
||||
be resolved and installed as a dependency. It is only possible to use the pure-python primitives
|
||||
by manually specifying this, for example by using the ``rnspure`` package.
|
||||
|
||||
.. warning::
|
||||
If you want to use the internal pure-python primitives, it is **highly advisable** that you
|
||||
have a good understanding of the risks that this pose, and make an informed decision on whether
|
||||
|
||||
@@ -53,7 +53,7 @@ The entire configuration of Reticulum is found in the ``~/.reticulum/config``
|
||||
file. When Reticulum is first started on a new system, a basic, but fully functional
|
||||
configuration file is created. The default configuration looks like this:
|
||||
|
||||
.. code::
|
||||
.. code:: ini
|
||||
|
||||
# This is the default Reticulum config file.
|
||||
# You should probably edit it to include any additional,
|
||||
@@ -69,12 +69,12 @@ configuration file is created. The default configuration looks like this:
|
||||
|
||||
# If you enable Transport, your system will route traffic
|
||||
# for other peers, pass announces and serve path requests.
|
||||
# This should only be done for systems that are suited to
|
||||
# act as transport nodes, ie. if they are stationary and
|
||||
# This should be done for systems that are suited to act
|
||||
# as transport nodes, ie. if they are stationary and
|
||||
# always-on. This directive is optional and can be removed
|
||||
# for brevity.
|
||||
|
||||
enable_transport = False
|
||||
enable_transport = No
|
||||
|
||||
|
||||
# By default, the first program to launch the Reticulum
|
||||
@@ -91,12 +91,24 @@ configuration file is created. The default configuration looks like this:
|
||||
|
||||
# If you want to run multiple *different* shared instances
|
||||
# on the same system, you will need to specify different
|
||||
# shared instance ports for each. The defaults are given
|
||||
# below, and again, these options can be left out if you
|
||||
# don't need them.
|
||||
# instance names for each. On platforms supporting domain
|
||||
# sockets, this can be done with the instance_name option:
|
||||
|
||||
shared_instance_port = 37428
|
||||
instance_control_port = 37429
|
||||
instance_name = default
|
||||
|
||||
# Some platforms don't support domain sockets, and if that
|
||||
# is the case, you can isolate different instances by
|
||||
# specifying a unique set of ports for each:
|
||||
|
||||
# shared_instance_port = 37428
|
||||
# instance_control_port = 37429
|
||||
|
||||
|
||||
# If you want to explicitly use TCP for shared instance
|
||||
# communication, instead of domain sockets, this is also
|
||||
# possible, by using the following option:
|
||||
|
||||
# shared_instance_type = tcp
|
||||
|
||||
|
||||
# On systems where running instances may not have access
|
||||
@@ -110,13 +122,25 @@ configuration file is created. The default configuration looks like this:
|
||||
# rpc_key = e5c032d3ec4e64a6aca9927ba8ab73336780f6d71790
|
||||
|
||||
|
||||
# It is possible to allow remote management of Reticulum
|
||||
# systems using the various built-in utilities, such as
|
||||
# rnstatus and rnpath. You will need to specify one or
|
||||
# more Reticulum Identity hashes for authenticating the
|
||||
# queries from client programs. For this purpose, you can
|
||||
# use existing identity files, or generate new ones with
|
||||
# the rnid utility.
|
||||
|
||||
# enable_remote_management = yes
|
||||
# remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc
|
||||
|
||||
|
||||
# You can configure Reticulum to panic and forcibly close
|
||||
# if an unrecoverable interface error occurs, such as the
|
||||
# hardware device for an interface disappearing. This is
|
||||
# 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
|
||||
|
||||
|
||||
# When Transport is enabled, it is possible to allow the
|
||||
@@ -127,7 +151,7 @@ configuration file is created. The default configuration looks like this:
|
||||
# Transport Instance, and printed to the log at startup.
|
||||
# Optional, and disabled by default.
|
||||
|
||||
respond_to_probes = No
|
||||
# respond_to_probes = No
|
||||
|
||||
|
||||
[logging]
|
||||
@@ -236,13 +260,14 @@ various configuration options, and interface configuration examples:
|
||||
Reticulum Network Stack Daemon
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
-v, --verbose
|
||||
-q, --quiet
|
||||
-s, --service rnsd is running as a service and should log to file
|
||||
--exampleconfig print verbose configuration example to stdout and exit
|
||||
--version show program's version number and exit
|
||||
-s, --service rnsd is running as a service and should log to file
|
||||
-i, --interactive drop into interactive shell after initialisation
|
||||
--exampleconfig print verbose configuration example to stdout and exit
|
||||
--version show program's version number and exit
|
||||
|
||||
You can easily add ``rnsd`` as an always-on service by :ref:`configuring a service<using-systemd>`.
|
||||
|
||||
@@ -313,8 +338,8 @@ Filter output to only show some interfaces:
|
||||
.. code:: text
|
||||
|
||||
usage: rnstatus [-h] [--config CONFIG] [--version] [-a] [-A]
|
||||
[-l] [-s SORT] [-r] [-j] [-R hash] [-i path]
|
||||
[-w seconds] [-v] [filter]
|
||||
[-l] [-t] [-s SORT] [-r] [-j] [-R hash] [-i path]
|
||||
[-w seconds] [-d] [-D] [-m] [-I seconds] [-v] [filter]
|
||||
|
||||
Reticulum Network Stack Status
|
||||
|
||||
@@ -328,15 +353,25 @@ Filter output to only show some interfaces:
|
||||
-a, --all show all interfaces
|
||||
-A, --announce-stats show announce stats
|
||||
-l, --link-stats show link stats
|
||||
-s SORT, --sort SORT sort interfaces by [rate, traffic, rx, tx, announces, arx, atx, held]
|
||||
-t, --totals display traffic totals
|
||||
-s, --sort SORT sort interfaces by [rate, traffic, rx, tx, rxs, txs,
|
||||
announces, arx, atx, held]
|
||||
-r, --reverse reverse sorting
|
||||
-j, --json output in JSON format
|
||||
-R hash transport identity hash of remote instance to get status from
|
||||
-i path path to identity used for remote management
|
||||
-w seconds timeout before giving up on remote queries
|
||||
-d, --discovered list discovered interfaces
|
||||
-D show details and config entries for discovered interfaces
|
||||
-m, --monitor continuously monitor status
|
||||
-I, --monitor-interval seconds
|
||||
refresh interval for monitor mode (default: 1)
|
||||
-v, --verbose
|
||||
|
||||
|
||||
.. note::
|
||||
When using ``-R`` to query a remote transport instance, you must also specify ``-i`` with the path to a management identity file that is authorized for remote management on the target system.
|
||||
|
||||
The rnid Utility
|
||||
====================
|
||||
|
||||
@@ -409,33 +444,33 @@ Decrypt a file using the Reticulum Identity it was encrypted for:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config path path to alternative Reticulum config directory
|
||||
-i identity, --identity identity
|
||||
hexadecimal Reticulum Destination hash or path to Identity file
|
||||
-g path, --generate path
|
||||
generate a new Identity
|
||||
-i, --identity identity
|
||||
hexadecimal Reticulum identity or destination hash, or path to Identity file
|
||||
-g, --generate file generate a new Identity
|
||||
-m, --import identity_data
|
||||
import Reticulum identity in hex, base32 or base64 format
|
||||
-x, --export export identity to hex, base32 or base64 format
|
||||
-v, --verbose increase verbosity
|
||||
-q, --quiet decrease verbosity
|
||||
-a aspects, --announce aspects
|
||||
-a, --announce aspects
|
||||
announce a destination based on this Identity
|
||||
-H aspects, --hash aspects
|
||||
show destination hashes for other aspects for this Identity
|
||||
-e path, --encrypt path
|
||||
encrypt file
|
||||
-d path, --decrypt path
|
||||
decrypt file
|
||||
-s path, --sign path sign file
|
||||
-V path, --validate path
|
||||
validate signature
|
||||
-r path, --read path input file path
|
||||
-w path, --write path
|
||||
output file path
|
||||
-H, --hash aspects show destination hashes for other aspects for this Identity
|
||||
-e, --encrypt file encrypt file
|
||||
-d, --decrypt file decrypt file
|
||||
-s, --sign path sign file
|
||||
-V, --validate path validate signature
|
||||
-r, --read file input file path
|
||||
-w, --write file output file path
|
||||
-f, --force write output even if it overwrites existing files
|
||||
-R, --request request unknown Identities from the network
|
||||
-t seconds identity request timeout before giving up
|
||||
-p, --print-identity print identity info and exit
|
||||
-P, --print-private allow displaying private keys
|
||||
-b, --base64 Use base64-encoded input and output
|
||||
-B, --base32 Use base32-encoded input and output
|
||||
--version show program's version number and exit
|
||||
|
||||
.. _utility-rnpath:
|
||||
|
||||
The rnpath Utility
|
||||
====================
|
||||
@@ -457,21 +492,23 @@ Resolve path to a destination:
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rnpath [-h] [--config CONFIG] [--version] [-t] [-m hops]
|
||||
[-r] [-d] [-D] [-x] [-w seconds] [-R hash] [-i path]
|
||||
[-W seconds] [-j] [-v] [destination]
|
||||
usage: rnpath [-h] [--config CONFIG] [--version] [-t] [-m hops] [-r] [-d] [-D]
|
||||
[-x] [-w seconds] [-R hash] [-i path] [-W seconds] [-b] [-B] [-U]
|
||||
[--duration DURATION] [--reason REASON] [-p] [-j] [-v]
|
||||
[destination] [list_filter]
|
||||
|
||||
Reticulum Path Discovery Utility
|
||||
Reticulum Path Management Utility
|
||||
|
||||
positional arguments:
|
||||
destination hexadecimal hash of the destination
|
||||
list_filter filter for remote blackhole list view
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG path to alternative Reticulum config directory
|
||||
--version show program's version number and exit
|
||||
-t, --table show all known paths
|
||||
-m hops, --max hops maximum hops to filter path table by
|
||||
-m, --max hops maximum hops to filter path table by
|
||||
-r, --rates show announce rate info
|
||||
-d, --drop remove the path to a destination
|
||||
-D, --drop-announces drop all queued announces
|
||||
@@ -480,6 +517,13 @@ Resolve path to a destination:
|
||||
-R hash transport identity hash of remote instance to manage
|
||||
-i path path to identity used for remote management
|
||||
-W seconds timeout before giving up on remote queries
|
||||
-b, --blackholed list blackholed identities
|
||||
-B, --blackhole blackhole identity
|
||||
-U, --unblackhole unblackhole identity
|
||||
--duration DURATION duration of blackhole enforcement in hours
|
||||
--reason REASON reason for blackholing identity
|
||||
-p, --blackholed-list
|
||||
view published blackhole list for remote transport instance
|
||||
-j, --json output in JSON format
|
||||
-v, --verbose
|
||||
|
||||
@@ -592,13 +636,20 @@ Or fetch a file from the remote system:
|
||||
|
||||
$ rncp --fetch ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e
|
||||
|
||||
The default identity file is stored in ``~/.reticulum/identities/rncp``, but you can use
|
||||
another one, which will be created if it does not already exist
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rncp ~/path/to/file.tgz 73cbd378bb0286ed11a707c13447bb1e -i /path/to/identity
|
||||
|
||||
**All Command-Line Options**
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: rncp [-h] [--config path] [-v] [-q] [-S] [-l] [-F] [-f]
|
||||
[-j path] [-b seconds] [-a allowed_hash] [-n] [-p]
|
||||
[-w seconds] [--version] [file] [destination]
|
||||
[-i identity] [-w seconds] [--version] [file] [destination]
|
||||
|
||||
Reticulum File Transfer Utility
|
||||
|
||||
@@ -613,14 +664,19 @@ Or fetch a file from the remote system:
|
||||
-q, --quiet decrease verbosity
|
||||
-S, --silent disable transfer progress output
|
||||
-l, --listen listen for incoming transfer requests
|
||||
-C, --no-compress disable automatic compression
|
||||
-F, --allow-fetch allow authenticated clients to fetch files
|
||||
-f, --fetch fetch file from remote listener instead of sending
|
||||
-j path, --jail path restrict fetch requests to specified path
|
||||
-j, --jail path restrict fetch requests to specified path
|
||||
-s, --save path save received files in specified path
|
||||
-O, --overwrite Allow overwriting received files, instead of adding postfix
|
||||
-b seconds announce interval, 0 to only announce at startup
|
||||
-a allowed_hash allow this identity
|
||||
-a allowed_hash allow this identity (or add in ~/.rncp/allowed_identities)
|
||||
-n, --no-auth accept requests from anyone
|
||||
-p, --print-identity print identity and destination info and exit
|
||||
-i identity path to identity to use
|
||||
-w seconds sender timeout before giving up
|
||||
-P, --phy-rates display physical layer transfer rates
|
||||
--version show program's version number and exit
|
||||
|
||||
|
||||
@@ -728,31 +784,36 @@ to create and provision new :ref:`RNodes<rnode-main>` from any supported hardwar
|
||||
-i, --info Show device info
|
||||
-a, --autoinstall Automatic installation on various supported devices
|
||||
-u, --update Update firmware to the latest version
|
||||
-U, --force-update Update to specified firmware even if version matches
|
||||
or is older than installed version
|
||||
--fw-version version Use a specific firmware version for update or
|
||||
autoinstall
|
||||
-U, --force-update Update to specified firmware even if version matches or is older than installed version
|
||||
--fw-version version Use a specific firmware version for update or autoinstall
|
||||
--fw-url url Use an alternate firmware download URL
|
||||
--nocheck Don't check for firmware updates online
|
||||
-e, --extract Extract firmware from connected RNode for later use
|
||||
-E, --use-extracted Use the extracted firmware for autoinstallation or
|
||||
update
|
||||
-E, --use-extracted Use the extracted firmware for autoinstallation or update
|
||||
-C, --clear-cache Clear locally cached firmware files
|
||||
--baud-flash baud_flash
|
||||
Set specific baud rate when flashing device. Default
|
||||
is 921600
|
||||
Set specific baud rate when flashing device. Default is 921600
|
||||
-N, --normal Switch device to normal mode
|
||||
-T, --tnc Switch device to TNC mode
|
||||
-b, --bluetooth-on Turn device bluetooth on
|
||||
-B, --bluetooth-off Turn device bluetooth off
|
||||
-p, --bluetooth-pair Put device into bluetooth pairing mode
|
||||
-D i, --display i Set display intensity (0-255)
|
||||
-D, --display i Set display intensity (0-255)
|
||||
-t, --timeout s Set display timeout in seconds, 0 to disable
|
||||
-R, --rotation rotation
|
||||
Set display rotation, valid values are 0 through 3
|
||||
--display-addr byte Set display address as hex byte (00 - FF)
|
||||
--recondition-display
|
||||
Start display reconditioning
|
||||
--np i Set NeoPixel intensity (0-255)
|
||||
--freq Hz Frequency in Hz for TNC mode
|
||||
--bw Hz Bandwidth in Hz for TNC mode
|
||||
--txp dBm TX power in dBm for TNC mode
|
||||
--sf factor Spreading factor for TNC mode (7 - 12)
|
||||
--cr rate Coding rate for TNC mode (5 - 8)
|
||||
-x, --ia-enable Enable interference avoidance
|
||||
-X, --ia-disable Disable interference avoidance
|
||||
-c, --config Print device configuration
|
||||
--eeprom-backup Backup EEPROM to file
|
||||
--eeprom-dump Dump EEPROM to console
|
||||
--eeprom-wipe Unlock and wipe EEPROM
|
||||
@@ -763,8 +824,8 @@ to create and provision new :ref:`RNodes<rnode-main>` from any supported hardwar
|
||||
-r, --rom Bootstrap EEPROM without flashing firmware
|
||||
-k, --key Generate a new signing key and exit
|
||||
-S, --sign Display public part of signing key
|
||||
-H FIRMWARE_HASH, --firmware-hash FIRMWARE_HASH
|
||||
Display installed firmware hash
|
||||
-H, --firmware-hash FIRMWARE_HASH
|
||||
Set installed firmware hash
|
||||
--platform platform Platform specification for device bootstrap
|
||||
--product product Product specification for device bootstrap
|
||||
--model model Model code for device bootstrap
|
||||
@@ -774,6 +835,104 @@ to create and provision new :ref:`RNodes<rnode-main>` from any supported hardwar
|
||||
For more information on how to create your own RNodes, please read the :ref:`Creating RNodes<rnode-creating>`
|
||||
section of this manual.
|
||||
|
||||
.. _using-interface_discovery:
|
||||
Discovering Interfaces
|
||||
----------------------
|
||||
|
||||
Reticulum includes built-in functionality for discovering connectable interfaces over Reticulum itself. This is particularly useful in situations where you want to do one or more of the following:
|
||||
|
||||
* Discover connectable entrypoints available on the Internet
|
||||
* Find connectable radio access points in the physical world
|
||||
* Maintain connectivity to RNS instances with unknown or changing IP addresses
|
||||
|
||||
Discovered interfaces can be **auto-connected** by Reticulum, which makes it possible to create setups where an arbitrary interface can act simply as a bootstrap connection, that can be torn down again once more suitable interfaces have been discovered and connected.
|
||||
|
||||
The interface discovery mechanism uses announces sent over Reticulum itself, and supports both publicly readable interfaces and private, encrypted discovery, that can only be decoded by specified *network identities*. It is also possible to specify which network identities should be considered valid sources for discovered interfaces, so that interfaces published by unknown entities are ignored.
|
||||
|
||||
.. note::
|
||||
A *network identity* is a normal Reticulum identity keyset that can be used by
|
||||
one or more transport nodes to identify them as belonging to the same overall
|
||||
network. In the context of interface discovery, this makes it easy to manage
|
||||
connecting to only the particular networks you care about, even if those networks
|
||||
utilize many individual physical transport node.
|
||||
|
||||
This also makes it convenient to auto-connect discovered interfaces only for networks you have some level of trust in.
|
||||
|
||||
For information on how to make your interfaces discoverable, see the :ref:`Discoverable Interfaces<interfaces-discoverable>` chapter of this manual. The current section will focus on how to actually *discover and connect to* interfaces available on the network.
|
||||
|
||||
In its most basic form, enabling interface discovery is as simple as setting ``discover_interfaces`` to ``true`` in your Reticulum config:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
discover_interfaces = yes
|
||||
...
|
||||
|
||||
Once this option is enabled, your RNS instance will start listening for interface discovery announces, and store them for later use or inspection. You can list discovered interfaces with the ``rnstatus`` utility:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnstatus -d
|
||||
|
||||
Name Type Status Last Heard Value Location
|
||||
-------------------------------------------------------------------------
|
||||
Sideband Hub Backbone ✓ Available 1h ago 16 46.2316, 6.0536
|
||||
RNS Amsterdam Backbone ✓ Available 32m ago 16 52.3865, 4.9037
|
||||
|
||||
|
||||
You can view more detailed information about discovered interfaces, including configuration snippets for pasting directly into your ``[interfaces]`` config, by using the ``rnstatus -D`` option:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnstatus -D sideband
|
||||
|
||||
Transport ID : 521c87a83afb8f29e4455e77930b973b
|
||||
Name : Sideband Hub
|
||||
Type : BackboneInterface
|
||||
Status : Available
|
||||
Transport : Enabled
|
||||
Distance : 2 hops
|
||||
Discovered : 9h and 40m ago
|
||||
Last Heard : 1h and 15m ago
|
||||
Location : 46.2316, 6.0536
|
||||
Address : sideband.connect.reticulum.network:7822
|
||||
Stamp Value : 16
|
||||
|
||||
Configuration Entry:
|
||||
[[Sideband Hub]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
remote = sideband.connect.reticulum.network
|
||||
target_port = 7822
|
||||
transport_identity = 521c87a83afb8f29e4455e77930b973b
|
||||
|
||||
In addition to providing local interface discovery information and control, the ``rnstatus`` utility can export discovered interface data in machine-readable JSON format using the ``rnstatus -d --json`` option. This can be useful for exporting the data to external applications such as status pages, access point maps and similar.
|
||||
|
||||
To control what sources are considered valid for discovered sources, additional
|
||||
configuration options can be specified for the interface discovery system.
|
||||
|
||||
* The ``interface_discovery_sources`` option is a list of the network or transport identities from which interfaces will be accepted. If this option is set, all others will be ignored. If this option is not set, discovered interfaces will be accepted from any source, but are still subject to stamp value requirements.
|
||||
|
||||
* The ``required_discovery_value`` options specifies the minimum stamp value required for the interface announce to be considered valid. To make it computationally difficult to spam the network with a large number of defunct or malicious interfaces, each announced interface requires a valid cryptographical stamp, of configurable difficulty value.
|
||||
|
||||
* The ``autoconnect_discovered_interfaces`` value defaults to ``0``, and specifies the maximum number of discovered interfaces that should be auto-connected at any given time. If set to a number greater than ``0``, Reticulum automatically manages discovered interface connections, and will bring discovered interfaces up and down based on availability. You can at any time add discovered interfaces to your configuration manually, to persistently keep them available.
|
||||
|
||||
* The ``network_identity`` option specifies the *network identity* for this RNS instance. This identity is used both to sign (and potentially encrypt) *outgoing* interface discovery announces, and to decrypt incoming discovery information.
|
||||
|
||||
The configuration snippet below contains an example of setting these additional configuration options:
|
||||
|
||||
.. code:: text
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
discover_interfaces = yes
|
||||
interface_discovery_sources = 521c87a83afb8f29e4455e77930b973b
|
||||
required_discovery_value = 16
|
||||
autoconnect_discovered_interfaces = 3
|
||||
network_identity = ~/.reticulum/storage/identities/my_network
|
||||
...
|
||||
|
||||
Remote Management
|
||||
-----------------
|
||||
|
||||
@@ -799,6 +958,130 @@ in the Reticulum configuration file:
|
||||
|
||||
For a complete example configuration, you can run ``rnsd --exampleconfig``.
|
||||
|
||||
.. _using-blackhole_management:
|
||||
|
||||
Blackhole Management
|
||||
--------------------
|
||||
|
||||
Reticulum networks are fundamentally permissionless and open, allowing anyone with a compatible interface to participate. While this openness is essential for a resilient and decentralized network, it also exposes the network to potential abuse, such as peers flooding the network with excessive announce broadcasts or other forms of resource exhaustion.
|
||||
|
||||
The **Blackhole** system provides tools to help manage this problem. It allows operators and individual users to block specific identities at the Transport layer, preventing them from propagating announces through your node, and for other nodes to reach them through your network.
|
||||
|
||||
.. important::
|
||||
|
||||
There is fundamentally **no way** to *globally* block or censor any identity or destination in Reticulum networks. The blackhole functionality will prevent announces from (and traffic to) all destinations associated with the blackholed identity *on your own network segments only*.
|
||||
|
||||
This provides users and operators with control over what they want to allow *on their own network segments*, but there is no way to globally censor or remove an identity, as long as *someone* is willing to provide transport for it.
|
||||
|
||||
This functionality serves a dual purpose:
|
||||
|
||||
* **For Individual Users:** It offers a simple way to maintain a quiet and efficient local network by manually blocking spammy or unwanted peers.
|
||||
* **For Network Operators:** It enables the creation of federated, community-wide security standards. By publishing and sharing blackhole lists, operators can protect large infrastructures and distribute spam filtering rules across the mesh without manual intervention.
|
||||
|
||||
|
||||
Local Blackhole Management
|
||||
==========================
|
||||
|
||||
The most immediate way to manage unwanted identities is through manual configuration using the ``rnpath`` utility. This allows you to instantly block or unblock specific identities on your local Transport Instance.
|
||||
|
||||
**Blackholing an Identity**
|
||||
|
||||
To block an identity, use the ``-B`` (or ``--blackhole``) flag followed by the identity hash. You can optionally specify a duration and a reason, which are useful for logging and future reference.
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnpath -B 3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o
|
||||
|
||||
You can also add a duration (in hours) and a reason:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnpath -B 3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o --duration 24 --reason "Excessive announces"
|
||||
|
||||
**Lifting Blackholes**
|
||||
|
||||
To remove an identity from the blackhole, use the ``-U`` (or ``--unblackhole``) flag:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnpath -U 3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o
|
||||
|
||||
**Viewing the Blackhole List**
|
||||
|
||||
To see all identities currently blackholed on your local instance, use the ``-b`` (or ``--blackholed``) flag:
|
||||
|
||||
.. code:: text
|
||||
|
||||
$ rnpath -b
|
||||
|
||||
<3a4f8b9c1d2e3f4g5h6i7j8k9l0m1n2o> blackholed for 23h, 56m (Excessive announces)
|
||||
<399ea050ce0eed1816c300bcb0840938> blackholed indefinitely (Announce spam)
|
||||
<d56a4fa02c0a77b3575935aedd90bdb2> blackholed indefinitely (Announce spam)
|
||||
<2b9ec651326d9bc274119054c70fb75e> blackholed indefinitely (Announce spam)
|
||||
<1178a8f1fad405bf2ad153bf5036bdfd> blackholed indefinitely (Announce spam)
|
||||
|
||||
|
||||
|
||||
Automated List Sourcing
|
||||
=======================
|
||||
|
||||
Manually blocking identities is effective for immediate threats, but maintaining an up-to-date blocklist for a large network is impractical. Reticulum supports **automated list sourcing**, allowing your node to subscribe to blackhole lists maintained by trusted peers, or a central authority you manage yourself.
|
||||
|
||||
.. warning::
|
||||
**Verify Before Subscribing!** Subscribing to a blackhole source is a powerful action that grants that source the ability to dictate who you can communicate with. Before adding a source to your configuration, verify that the maintainer aligns with your usage policy and values. Blindly subscribing to untrusted lists could inadvertently block legitimate peers or essential services.
|
||||
|
||||
When enabled, your Transport Instance will periodically (approximately once per hour) connect to configured sources, retrieve their latest blackhole lists, and automatically merge them into your local blocklist. This provides "set-and-forget" protection for both individual users and large networks.
|
||||
|
||||
**Configuration**
|
||||
|
||||
To enable automated sourcing, add the ``blackhole_sources`` option to the ``[reticulum]`` section of your configuration file. This option accepts a comma-separated list of Transport Identity hashes that you trust to provide valid blackhole lists.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
# Automatically fetch blackhole lists from these trusted sources
|
||||
blackhole_sources = 521c87a83afb8f29e4455e77930b973b, 68a4aa91ac350c4087564e8a69f84e86
|
||||
...
|
||||
|
||||
**How It Works**
|
||||
|
||||
1. When enabled, the ``BlackholeUpdater`` service runs in the background.
|
||||
2. For every identity hash listed in ``blackhole_sources``, it attempts to establish a temporary link to its associated``rnstransport.info.blackhole`` destination.
|
||||
3. It requests the ``/list`` path, which returns a dictionary of blackholed identities and their associated metadata.
|
||||
4. The received list is merged with your local ``blackholed_identities`` database.
|
||||
5. The lists are persisted to disk, ensuring they survive restarts.
|
||||
|
||||
.. note::
|
||||
You can verify the external lists you are subscribed to, and their contents, without importing them by using ``rnpath -p``. See the :ref:`rnpath utility documentation<utility-rnpath>` for details on querying remote blackhole lists.
|
||||
|
||||
|
||||
Publishing Blackhole Lists
|
||||
==========================
|
||||
|
||||
If you are operating a public gateway, a community hub, or simply wish to share your blackhole list with others, you can configure your instance to act as a blackhole list publisher. This allows other nodes to subscribe to *your* definitions of unwanted traffic.
|
||||
|
||||
**Enabling Publishing**
|
||||
|
||||
To publish your local blackhole list, enable the ``publish_blackhole`` option in the ``[reticulum]`` section:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[reticulum]
|
||||
...
|
||||
publish_blackhole = yes
|
||||
...
|
||||
|
||||
When this is enabled, your Transport Instance will register a request handler at ``rnstransport.info.blackhole``. Any peer that connects to this destination and requests ``/list`` will receive the complete set of identities currently present in your local blackhole database.
|
||||
|
||||
**Federation and Trust**
|
||||
|
||||
The blackhole system relies on the trust relationship between the subscriber and the publisher. By subscribing to a source, you are implicitly trusting that source to only block identities that are genuinely detrimental to the network.
|
||||
|
||||
As the ecosystem matures, this system is designed to integrate with **Network Identities**. This allows communities to verify that a published blackhole list is actually provided by a specific network or organization with a certain level of reputation and trustworthiness, adding a layer of cryptographic trust to the federation process. This prevents malicious actors from publishing fake lists intended to censor legitimate traffic.
|
||||
|
||||
For operators, this creates a scalable model where maintaining a single high-quality blocklist can protect thousands of downstream peers, drastically reducing the administrative.
|
||||
|
||||
Improving System Configuration
|
||||
------------------------------
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user