mirror of
https://github.com/markqvist/Reticulum.git
synced 2026-06-23 12:24:30 -07:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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**
|
||||
|
||||
@@ -29,7 +29,9 @@ jobs:
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: make test
|
||||
- run: |
|
||||
python -m pip install -q cryptography
|
||||
make test
|
||||
|
||||
package:
|
||||
needs: test
|
||||
|
||||
@@ -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()
|
||||
@@ -1,3 +1,78 @@
|
||||
### 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**
|
||||
```
|
||||
0b880827369dd4bda7e4691e0e157e75fab4c22554743c1c7f07a350106114e8 rns-0.9.5-py3-none-any.whl
|
||||
6c00d12176de40d7dc45d9edd7952a161413f4cc4cc276b82bf3811f99440556 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.
|
||||
|
||||
+10
-2
@@ -6,7 +6,7 @@ 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.
|
||||
|
||||
## Asking Questions
|
||||
|
||||
@@ -40,4 +40,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 issues will regularly remain undetected and slip through, even with stringent review. This is not a worthwhile tradeoff for Reticulum, especially considering the limited time maintainers have to correct these issues, and we ask that you refrain from using any such output in your contributions.
|
||||
|
||||
This applies to all official Reticlulm 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. We also ask that you avoid using LLMs for troubleshooting, as results can be misleading, and instead request help in one of our [various communities](https://reticulum.network/start.html).
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,7 @@ documentation:
|
||||
manual:
|
||||
make -C docs latexpdf epub
|
||||
|
||||
release: test remove_symlinks build_wheel build_pure_wheel documentation manual 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,4 +1,4 @@
|
||||
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>
|
||||
@@ -52,7 +52,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-128 or AES-256 in CBC mode with PKCS7 padding
|
||||
- HMAC using SHA256 for authentication
|
||||
- IVs are generated through os.urandom()
|
||||
- Unforgeable packet delivery confirmations
|
||||
@@ -86,9 +86,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
|
||||
@@ -296,23 +297,28 @@ You can help support the continued development of open, free and private communi
|
||||
```
|
||||
84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w
|
||||
```
|
||||
- Ethereum
|
||||
```
|
||||
0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73
|
||||
```
|
||||
- Bitcoin
|
||||
```
|
||||
35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH
|
||||
bc1p4a6axuvl7n9hpapfj8sv5reqj8kz6uxa67d5en70vzrttj0fmcusgxsfk5
|
||||
```
|
||||
- Ethereum
|
||||
```
|
||||
0xae89F3B94fC4AD6563F0864a55F9a697a90261ff
|
||||
```
|
||||
- Ko-Fi: https://ko-fi.com/markqvist
|
||||
|
||||
Are certain features in the development roadmap are important to you or your
|
||||
organisation? Make them a reality quickly by sponsoring their implementation.
|
||||
|
||||
## 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 +326,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-128 or 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`, `AES-128-CBC`
|
||||
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 +348,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 +384,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
|
||||
|
||||
+53
-38
@@ -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,12 @@ 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
|
||||
|
||||
# TODO: Remove after migration
|
||||
import RNS
|
||||
|
||||
class Token():
|
||||
"""
|
||||
@@ -40,45 +53,49 @@ class Token():
|
||||
TOKEN_OVERHEAD = 48 # Bytes
|
||||
|
||||
@staticmethod
|
||||
def generate_key():
|
||||
return os.urandom(32)
|
||||
def generate_key(mode=AES_128_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(
|
||||
# RNS.log(f"Encrypting with {self.mode}") # TODO: Remove
|
||||
ciphertext = self.mode.encrypt(
|
||||
plaintext = PKCS7.pad(data),
|
||||
key = self._encryption_key,
|
||||
iv = iv,
|
||||
)
|
||||
iv = iv)
|
||||
|
||||
signed_parts = iv+ciphertext
|
||||
|
||||
@@ -86,25 +103,23 @@ class Token():
|
||||
|
||||
|
||||
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")
|
||||
# RNS.log(f"Trying decryption with {self.mode}") # TODO: Remove
|
||||
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,
|
||||
)
|
||||
)
|
||||
self.mode.decrypt(
|
||||
ciphertext = ciphertext,
|
||||
key = self._encryption_key,
|
||||
iv = iv))
|
||||
|
||||
# RNS.log(f"Decrypted packet with {self.mode}") # TODO: Remove
|
||||
return plaintext
|
||||
|
||||
except Exception as e:
|
||||
RNS.trace_exception(e) # TODO: Remove after migration
|
||||
raise ValueError("Could not decrypt token")
|
||||
@@ -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"]
|
||||
+23
-7
@@ -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)
|
||||
@@ -407,7 +419,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 +428,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)
|
||||
@@ -450,6 +465,7 @@ class Destination:
|
||||
self.ratchets_path = None
|
||||
RNS.trace_exception(e)
|
||||
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 = []
|
||||
|
||||
+106
-41
@@ -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:
|
||||
@@ -643,8 +679,20 @@ class Identity:
|
||||
|
||||
shared_key = ephemeral_key.exchange(target_public_key)
|
||||
|
||||
# TODO: Reset after migration
|
||||
# derived_key = RNS.Cryptography.hkdf(
|
||||
# length=Identity.DERIVED_KEY_LENGTH,
|
||||
# derive_from=shared_key,
|
||||
# salt=self.get_salt(),
|
||||
# context=self.get_context(),
|
||||
# )
|
||||
|
||||
# Use legacy derived key length (AES-128) during migration by
|
||||
# default. This allows AES-256 capable instances on RNS 0.9.5
|
||||
# to still communicate with older versions. This migration
|
||||
# handling will be removed in RNS 0.9.6.
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
length=Identity.DERIVED_KEY_LENGTH_LEGACY,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
@@ -667,6 +715,37 @@ class Identity:
|
||||
:returns: Plaintext as *bytes*, or *None* if decryption fails.
|
||||
:raises: *KeyError* if the instance does not hold a private key.
|
||||
"""
|
||||
|
||||
# This handles decryption during migration to AES-256 where
|
||||
# older instances may still use AES-128. If decryption fails
|
||||
# initially, AES-128 will be attempted as a fallback mode.
|
||||
# This handler will be removed in RNS 0.9.6.
|
||||
def migration_decrypt(shared_key, ciphertext):
|
||||
try:
|
||||
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)
|
||||
|
||||
# TODO: Remove after migration
|
||||
# If decryption fails, try legacy decryption mode
|
||||
except Exception as e:
|
||||
RNS.log("Decryption failed, attempting legacy mode fallback", RNS.LOG_DEBUG)
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=Identity.DERIVED_KEY_LENGTH_LEGACY,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context())
|
||||
|
||||
token = Token(derived_key)
|
||||
plaintext = token.decrypt(ciphertext)
|
||||
|
||||
return plaintext
|
||||
|
||||
if self.prv != None:
|
||||
if len(ciphertext_token) > Identity.KEYSIZE//8//2:
|
||||
plaintext = None
|
||||
@@ -681,15 +760,8 @@ 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(),
|
||||
)
|
||||
plaintext = migration_decrypt(shared_key, ciphertext)
|
||||
|
||||
token = Token(derived_key)
|
||||
plaintext = token.decrypt(ciphertext)
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = ratchet_id
|
||||
|
||||
@@ -706,15 +778,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 = migration_decrypt(shared_key, ciphertext)
|
||||
|
||||
token = Token(derived_key)
|
||||
plaintext = token.decrypt(ciphertext)
|
||||
if ratchet_id_receiver:
|
||||
ratchet_id_receiver.latest_ratchet_id = 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,
|
||||
@@ -368,7 +376,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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
+179
-65
@@ -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,7 @@ class AutoInterface(Interface):
|
||||
MULTICAST_PERMANENT_ADDRESS_TYPE = "0"
|
||||
MULTICAST_TEMPORARY_ADDRESS_TYPE = "1"
|
||||
|
||||
PEERING_TIMEOUT = 7.5
|
||||
PEERING_TIMEOUT = 10.0
|
||||
|
||||
ALL_IGNORE_IFS = ["lo0"]
|
||||
DARWIN_IGNORE_IFS = ["awdl0", "llw0", "lo0", "en5"]
|
||||
@@ -79,7 +90,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,15 +109,15 @@ 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.peers = {}
|
||||
self.link_local_addresses = []
|
||||
@@ -115,6 +125,8 @@ class AutoInterface(Interface):
|
||||
self.interface_servers = {}
|
||||
self.multicast_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
|
||||
@@ -130,7 +142,7 @@ class AutoInterface(Interface):
|
||||
# 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 *= 2.5
|
||||
|
||||
if allowed_interfaces == None:
|
||||
self.allowed_interfaces = []
|
||||
@@ -288,32 +300,31 @@ 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()
|
||||
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()
|
||||
job_thread = threading.Thread(target=self.peer_jobs)
|
||||
job_thread.daemon = True
|
||||
job_thread.start()
|
||||
|
||||
time.sleep(peering_wait)
|
||||
|
||||
self.online = True
|
||||
time.sleep(peering_wait)
|
||||
|
||||
self.online = True
|
||||
|
||||
def discovery_handler(self, socket, ifname):
|
||||
def announce_loop():
|
||||
@@ -325,8 +336,9 @@ class AutoInterface(Interface):
|
||||
|
||||
while True:
|
||||
data, ipv6_src = socket.recvfrom(1024)
|
||||
peering_hash = data[:RNS.Identity.HASHLENGTH//8]
|
||||
expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
|
||||
if data == expected_hash:
|
||||
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)
|
||||
@@ -347,6 +359,10 @@ 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)
|
||||
|
||||
for ifname in self.adopted_interfaces:
|
||||
@@ -433,6 +449,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
|
||||
@@ -448,46 +468,62 @@ class AutoInterface(Interface):
|
||||
else:
|
||||
if not addr in self.peers:
|
||||
self.peers[addr] = [ifname, 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()
|
||||
self.spawned_interfaces.pop(spawned_interface)
|
||||
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:
|
||||
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 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.rxb += len(data)
|
||||
self.owner.inbound(data, self)
|
||||
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):
|
||||
if self.online:
|
||||
for peer in self.peers:
|
||||
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])
|
||||
|
||||
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)
|
||||
|
||||
pass
|
||||
|
||||
# Until per-device sub-interfacing is implemented,
|
||||
# ingress limiting should be disabled on AutoInterface
|
||||
@@ -500,6 +536,83 @@ class AutoInterface(Interface):
|
||||
def __str__(self):
|
||||
return "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.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.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.rxb += len(data)
|
||||
self.owner.owner.inbound(data, self)
|
||||
|
||||
def process_outgoing(self, data):
|
||||
if self.online:
|
||||
with self.owner.write_lock:
|
||||
try:
|
||||
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)
|
||||
|
||||
def detach(self):
|
||||
self.online = False
|
||||
self.detached = True
|
||||
|
||||
def teardown(self):
|
||||
if not self.detached:
|
||||
RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is being torn down.", 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 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)
|
||||
|
||||
# Until per-device sub-interfacing is implemented,
|
||||
# ingress limiting should be disabled on AutoInterface
|
||||
def should_ingress_limit(self):
|
||||
return False
|
||||
|
||||
class AutoInterfaceHandler(socketserver.BaseRequestHandler):
|
||||
def __init__(self, callback, *args, **keys):
|
||||
self.callback = callback
|
||||
@@ -507,4 +620,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,691 @@
|
||||
# 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 = []
|
||||
|
||||
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: 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: 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:
|
||||
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.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)
|
||||
self.reconnect()
|
||||
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)
|
||||
self.reconnect()
|
||||
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,
|
||||
|
||||
@@ -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,9 @@ class Interface:
|
||||
self.bitrate = 62500
|
||||
self.HW_MTU = None
|
||||
|
||||
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,7 +135,9 @@ class Interface:
|
||||
|
||||
def optimise_mtu(self):
|
||||
if self.AUTOCONFIGURE_MTU:
|
||||
if self.bitrate > 16_000_000:
|
||||
if self.bitrate > 500_000_000:
|
||||
self.HW_MTU = 524288
|
||||
elif self.bitrate > 16_000_000:
|
||||
self.HW_MTU = 262144
|
||||
elif self.bitrate > 8_000_000:
|
||||
self.HW_MTU = 131072
|
||||
@@ -181,12 +195,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 +281,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.vendor.platformutils.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,36 +227,50 @@ 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()
|
||||
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:
|
||||
@@ -229,7 +282,6 @@ class LocalClientInterface(Interface):
|
||||
|
||||
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 +337,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.vendor.platformutils.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 +455,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,
|
||||
@@ -126,7 +134,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:
|
||||
@@ -1190,7 +1198,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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -331,7 +339,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 +453,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 +462,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 +472,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:
|
||||
@@ -516,7 +532,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"]
|
||||
|
||||
|
||||
@@ -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
|
||||
+142
-49
@@ -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,
|
||||
@@ -33,7 +41,6 @@ import math
|
||||
import time
|
||||
import RNS
|
||||
|
||||
|
||||
class LinkCallbacks:
|
||||
def __init__(self):
|
||||
self.link_established = None
|
||||
@@ -93,39 +100,80 @@ class Link:
|
||||
and will be torn down.
|
||||
"""
|
||||
|
||||
PENDING = 0x00
|
||||
HANDSHAKE = 0x01
|
||||
ACTIVE = 0x02
|
||||
STALE = 0x03
|
||||
CLOSED = 0x04
|
||||
PENDING = 0x00
|
||||
HANDSHAKE = 0x01
|
||||
ACTIVE = 0x02
|
||||
STALE = 0x03
|
||||
CLOSED = 0x04
|
||||
|
||||
TIMEOUT = 0x01
|
||||
INITIATOR_CLOSED = 0x02
|
||||
DESTINATION_CLOSED = 0x03
|
||||
TIMEOUT = 0x01
|
||||
INITIATOR_CLOSED = 0x02
|
||||
DESTINATION_CLOSED = 0x03
|
||||
|
||||
ACCEPT_NONE = 0x00
|
||||
ACCEPT_APP = 0x01
|
||||
ACCEPT_ALL = 0x02
|
||||
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_AES128_CBC, MODE_AES256_CBC]
|
||||
MODE_DEFAULT = MODE_AES128_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 +190,11 @@ class Link:
|
||||
RNS.trace_exception(e)
|
||||
link.mtu = RNS.Reticulum.MTU
|
||||
|
||||
link.mode = Link.mode_from_lr_packet(packet)
|
||||
|
||||
# TODO: Remove
|
||||
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 +223,15 @@ 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")
|
||||
# TODO: Set default link mode to AES_256_CBC after migration
|
||||
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
|
||||
@@ -244,12 +299,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 +348,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 +389,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 +407,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):
|
||||
@@ -535,6 +595,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
|
||||
|
||||
@@ -1058,8 +1151,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 +1165,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 +1243,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):
|
||||
"""
|
||||
|
||||
+22
-5
@@ -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,
|
||||
@@ -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,7 +279,11 @@ 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
|
||||
|
||||
+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,
|
||||
|
||||
+41
-29
@@ -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,
|
||||
@@ -163,32 +171,33 @@ 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
|
||||
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:
|
||||
@@ -316,6 +325,7 @@ 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
|
||||
|
||||
@@ -470,6 +480,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,6 +509,7 @@ 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)
|
||||
|
||||
+90
-51
@@ -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,
|
||||
@@ -26,6 +34,7 @@ if get_platform() == "android":
|
||||
from .Interfaces import Interface
|
||||
from .Interfaces import LocalInterface
|
||||
from .Interfaces import AutoInterface
|
||||
from .Interfaces import BackboneInterface
|
||||
from .Interfaces import TCPInterface
|
||||
from .Interfaces import UDPInterface
|
||||
from .Interfaces import I2PInterface
|
||||
@@ -39,7 +48,7 @@ else:
|
||||
from RNS.vendor.configobj import ConfigObj
|
||||
import configparser
|
||||
import multiprocessing.connection
|
||||
import importlib
|
||||
import importlib.util
|
||||
import threading
|
||||
import signal
|
||||
import atexit
|
||||
@@ -90,7 +99,7 @@ class Reticulum:
|
||||
the default value.
|
||||
"""
|
||||
|
||||
LINK_MTU_DISCOVERY = False
|
||||
LINK_MTU_DISCOVERY = True
|
||||
"""
|
||||
Whether automatic link MTU discovery is enabled by default in this
|
||||
release. Link MTU discovery significantly increases throughput over
|
||||
@@ -172,6 +181,7 @@ class Reticulum:
|
||||
# classes, saving necessary information to disk and carrying
|
||||
# out cleanup operations.
|
||||
if not Reticulum.__exit_handler_ran:
|
||||
Reticulum.__exit_handler_ran = True
|
||||
if not Reticulum.__interface_detach_ran:
|
||||
RNS.Transport.detach_interfaces()
|
||||
RNS.Transport.exit_handler()
|
||||
@@ -251,9 +261,14 @@ class Reticulum:
|
||||
|
||||
self.local_interface_port = 37428
|
||||
self.local_control_port = 37429
|
||||
self.local_socket_path = None
|
||||
self.share_instance = True
|
||||
self.rpc_listener = None
|
||||
self.rpc_key = None
|
||||
self.rpc_type = "AF_INET"
|
||||
|
||||
if RNS.vendor.platformutils.use_af_unix():
|
||||
self.local_socket_path = "default"
|
||||
|
||||
self.ifac_salt = Reticulum.IFAC_SALT
|
||||
|
||||
@@ -282,6 +297,9 @@ class Reticulum:
|
||||
if not os.path.isdir(Reticulum.cachepath):
|
||||
os.makedirs(Reticulum.cachepath)
|
||||
|
||||
if not os.path.isdir(os.path.join(Reticulum.cachepath, "announces")):
|
||||
os.makedirs(os.path.join(Reticulum.cachepath, "announces"))
|
||||
|
||||
if not os.path.isdir(Reticulum.resourcepath):
|
||||
os.makedirs(Reticulum.resourcepath)
|
||||
|
||||
@@ -312,12 +330,18 @@ class Reticulum:
|
||||
|
||||
RNS.Transport.start(self)
|
||||
|
||||
self.rpc_addr = ("127.0.0.1", self.local_control_port)
|
||||
if RNS.vendor.platformutils.use_af_unix():
|
||||
self.rpc_addr = f"\0rns/{self.local_socket_path}/rpc"
|
||||
self.rpc_type = "AF_UNIX"
|
||||
else:
|
||||
self.rpc_addr = ("127.0.0.1", self.local_control_port)
|
||||
self.rpc_type = "AF_INET"
|
||||
|
||||
if self.rpc_key == None:
|
||||
self.rpc_key = RNS.Identity.full_hash(RNS.Transport.identity.get_private_key())
|
||||
|
||||
if self.is_shared_instance:
|
||||
self.rpc_listener = multiprocessing.connection.Listener(self.rpc_addr, authkey=self.rpc_key)
|
||||
self.rpc_listener = multiprocessing.connection.Listener(self.rpc_addr, family=self.rpc_type, authkey=self.rpc_key)
|
||||
thread = threading.Thread(target=self.rpc_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
@@ -351,7 +375,8 @@ class Reticulum:
|
||||
try:
|
||||
interface = LocalInterface.LocalServerInterface(
|
||||
RNS.Transport,
|
||||
self.local_interface_port
|
||||
self.local_interface_port,
|
||||
socket_path=self.local_socket_path
|
||||
)
|
||||
interface.OUT = True
|
||||
if hasattr(Reticulum, "_force_shared_instance_bitrate"):
|
||||
@@ -377,7 +402,8 @@ class Reticulum:
|
||||
interface = LocalInterface.LocalClientInterface(
|
||||
RNS.Transport,
|
||||
"Local shared instance",
|
||||
self.local_interface_port)
|
||||
self.local_interface_port,
|
||||
socket_path=self.local_socket_path)
|
||||
interface.target_port = self.local_interface_port
|
||||
interface.OUT = True
|
||||
if hasattr(Reticulum, "_force_shared_instance_bitrate"):
|
||||
@@ -428,6 +454,10 @@ class Reticulum:
|
||||
if option == "share_instance":
|
||||
value = self.config["reticulum"].as_bool(option)
|
||||
self.share_instance = value
|
||||
if RNS.vendor.platformutils.use_af_unix():
|
||||
if option == "instance_name":
|
||||
value = self.config["reticulum"][option]
|
||||
self.local_socket_path = value
|
||||
if option == "shared_instance_port":
|
||||
value = int(self.config["reticulum"][option])
|
||||
self.local_interface_port = value
|
||||
@@ -484,6 +514,8 @@ class Reticulum:
|
||||
if v == False:
|
||||
Reticulum.__use_implicit_proof = False
|
||||
|
||||
if RNS.compiled: RNS.log("Reticulum running in compiled mode", RNS.LOG_DEBUG)
|
||||
else: RNS.log("Reticulum running in interpreted mode", RNS.LOG_DEBUG)
|
||||
self.__start_local_interface()
|
||||
|
||||
if self.is_shared_instance or self.is_standalone_instance:
|
||||
@@ -648,6 +680,7 @@ class Reticulum:
|
||||
interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key))
|
||||
|
||||
RNS.Transport.interfaces.append(interface)
|
||||
interface.final_init()
|
||||
|
||||
interface = None
|
||||
if (("interface_enabled" in c) and c.as_bool("interface_enabled") == True) or (("enabled" in c) and c.as_bool("enabled") == True):
|
||||
@@ -660,31 +693,34 @@ class Reticulum:
|
||||
interface = AutoInterface.AutoInterface(RNS.Transport, interface_config)
|
||||
interface_post_init(interface)
|
||||
|
||||
if c["type"] == "BackboneInterface" or c["type"] == "BackboneClientInterface":
|
||||
if "port" in c: c["listen_port"] = c["port"]
|
||||
if "port" in c: c["target_port"] = c["port"]
|
||||
if "remote" in c: c["target_host"] = c["remote"]
|
||||
if "listen_on" in c: c["listen_ip"] = c["listen_on"]
|
||||
|
||||
if c["type"] == "BackboneInterface":
|
||||
if "target_host" in c: interface = BackboneInterface.BackboneClientInterface(RNS.Transport, interface_config)
|
||||
else: interface = BackboneInterface.BackboneInterface(RNS.Transport, interface_config)
|
||||
interface_post_init(interface)
|
||||
|
||||
if c["type"] == "BackboneClientInterface":
|
||||
interface = BackboneInterface.BackboneClientInterface(RNS.Transport, interface_config)
|
||||
interface_post_init(interface)
|
||||
|
||||
if c["type"] == "UDPInterface":
|
||||
interface = UDPInterface.UDPInterface(RNS.Transport, interface_config)
|
||||
interface_post_init(interface)
|
||||
|
||||
if c["type"] == "TCPServerInterface":
|
||||
if interface_mode == Interface.Interface.MODE_ACCESS_POINT:
|
||||
RNS.log(str(interface)+" does not support Access Point mode, reverting to default mode: Full", RNS.LOG_WARNING)
|
||||
interface_mode = Interface.Interface.MODE_FULL
|
||||
|
||||
interface = TCPInterface.TCPServerInterface(RNS.Transport, interface_config)
|
||||
interface_post_init(interface)
|
||||
|
||||
if c["type"] == "TCPClientInterface":
|
||||
if interface_mode == Interface.Interface.MODE_ACCESS_POINT:
|
||||
RNS.log(str(interface)+" does not support Access Point mode, reverting to default mode: Full", RNS.LOG_WARNING)
|
||||
interface_mode = Interface.Interface.MODE_FULL
|
||||
|
||||
interface = TCPInterface.TCPClientInterface(RNS.Transport, interface_config)
|
||||
interface_post_init(interface)
|
||||
|
||||
if c["type"] == "I2PInterface":
|
||||
if interface_mode == Interface.Interface.MODE_ACCESS_POINT:
|
||||
RNS.log(str(interface)+" does not support Access Point mode, reverting to default mode: Full", RNS.LOG_WARNING)
|
||||
interface_mode = Interface.Interface.MODE_FULL
|
||||
|
||||
interface_config["storagepath"] = Reticulum.storagepath
|
||||
interface_config["ifac_netname"] = ifac_netname
|
||||
interface_config["ifac_netkey"] = ifac_netkey
|
||||
@@ -805,6 +841,7 @@ class Reticulum:
|
||||
interface.ifac_signature = interface.ifac_identity.sign(RNS.Identity.full_hash(interface.ifac_key))
|
||||
|
||||
RNS.Transport.interfaces.append(interface)
|
||||
interface.final_init()
|
||||
|
||||
def _should_persist_data(self):
|
||||
if time.time() > self.last_data_persist+Reticulum.GRACIOUS_PERSIST_INTERVAL:
|
||||
@@ -910,9 +947,11 @@ class Reticulum:
|
||||
except Exception as e:
|
||||
RNS.log("An error ocurred while handling RPC call from local client: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
def get_rpc_client(self): return multiprocessing.connection.Client(self.rpc_addr, family=self.rpc_type, authkey=self.rpc_key)
|
||||
|
||||
def get_interface_stats(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "interface_stats"})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
@@ -1059,23 +1098,23 @@ class Reticulum:
|
||||
|
||||
def get_path_table(self, max_hops=None):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "path_table", "max_hops": max_hops})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
|
||||
else:
|
||||
path_table = []
|
||||
for dst_hash in RNS.Transport.destination_table:
|
||||
path_hops = RNS.Transport.destination_table[dst_hash][2]
|
||||
for dst_hash in RNS.Transport.path_table:
|
||||
path_hops = RNS.Transport.path_table[dst_hash][2]
|
||||
if max_hops == None or path_hops <= max_hops:
|
||||
entry = {
|
||||
"hash": dst_hash,
|
||||
"timestamp": RNS.Transport.destination_table[dst_hash][0],
|
||||
"via": RNS.Transport.destination_table[dst_hash][1],
|
||||
"timestamp": RNS.Transport.path_table[dst_hash][0],
|
||||
"via": RNS.Transport.path_table[dst_hash][1],
|
||||
"hops": path_hops,
|
||||
"expires": RNS.Transport.destination_table[dst_hash][3],
|
||||
"interface": str(RNS.Transport.destination_table[dst_hash][5]),
|
||||
"expires": RNS.Transport.path_table[dst_hash][3],
|
||||
"interface": str(RNS.Transport.path_table[dst_hash][5]),
|
||||
}
|
||||
path_table.append(entry)
|
||||
|
||||
@@ -1083,7 +1122,7 @@ class Reticulum:
|
||||
|
||||
def get_rate_table(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "rate_table"})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
@@ -1104,7 +1143,7 @@ class Reticulum:
|
||||
|
||||
def drop_path(self, destination):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"drop": "path", "destination_hash": destination})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
@@ -1114,15 +1153,15 @@ class Reticulum:
|
||||
|
||||
def drop_all_via(self, transport_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"drop": "all_via", "destination_hash": transport_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
|
||||
else:
|
||||
dropped_count = 0
|
||||
for destination_hash in RNS.Transport.destination_table:
|
||||
if RNS.Transport.destination_table[destination_hash][1] == transport_hash:
|
||||
for destination_hash in RNS.Transport.path_table:
|
||||
if RNS.Transport.path_table[destination_hash][1] == transport_hash:
|
||||
RNS.Transport.expire_path(destination_hash)
|
||||
dropped_count += 1
|
||||
|
||||
@@ -1130,7 +1169,7 @@ class Reticulum:
|
||||
|
||||
def drop_announce_queues(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"drop": "announce_queues"})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
@@ -1140,7 +1179,7 @@ class Reticulum:
|
||||
|
||||
def get_next_hop_if_name(self, destination):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "next_hop_if_name", "destination_hash": destination})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
@@ -1151,7 +1190,7 @@ class Reticulum:
|
||||
def get_first_hop_timeout(self, destination):
|
||||
if self.is_connected_to_shared_instance:
|
||||
try:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "first_hop_timeout", "destination_hash": destination})
|
||||
response = rpc_connection.recv()
|
||||
|
||||
@@ -1170,14 +1209,10 @@ class Reticulum:
|
||||
|
||||
def get_next_hop(self, destination):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "next_hop", "destination_hash": destination})
|
||||
response = rpc_connection.recv()
|
||||
|
||||
# TODO: Remove this debugging function
|
||||
# if not response:
|
||||
# response = RNS.Transport.next_hop(destination)
|
||||
|
||||
return response
|
||||
|
||||
else:
|
||||
@@ -1185,7 +1220,7 @@ class Reticulum:
|
||||
|
||||
def get_link_count(self):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "link_count"})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
@@ -1195,7 +1230,7 @@ class Reticulum:
|
||||
|
||||
def get_packet_rssi(self, packet_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "packet_rssi", "packet_hash": packet_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
@@ -1209,7 +1244,7 @@ class Reticulum:
|
||||
|
||||
def get_packet_snr(self, packet_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "packet_snr", "packet_hash": packet_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
@@ -1223,7 +1258,7 @@ class Reticulum:
|
||||
|
||||
def get_packet_q(self, packet_hash):
|
||||
if self.is_connected_to_shared_instance:
|
||||
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
|
||||
rpc_connection = self.get_rpc_client()
|
||||
rpc_connection.send({"get": "packet_q", "packet_hash": packet_hash})
|
||||
response = rpc_connection.recv()
|
||||
return response
|
||||
@@ -1335,14 +1370,18 @@ 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:
|
||||
|
||||
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
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
+414
-327
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,
|
||||
|
||||
+60
-40
@@ -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,
|
||||
@@ -34,6 +42,7 @@ from RNS._version import __version__
|
||||
APP_NAME = "rncp"
|
||||
allow_all = False
|
||||
allow_fetch = False
|
||||
fetch_auto_compress = True
|
||||
fetch_jail = None
|
||||
save_path = None
|
||||
show_phy_rates = False
|
||||
@@ -45,11 +54,14 @@ 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
|
||||
limit = None, disable_auth = None, fetch_allowed = False, no_compress=False,
|
||||
jail = None, save = None, announce = False):
|
||||
|
||||
global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path, fetch_auto_compress
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
allow_fetch = fetch_allowed
|
||||
fetch_auto_compress = not no_compress
|
||||
identity = None
|
||||
if announce < 0:
|
||||
announce = False
|
||||
@@ -68,10 +80,10 @@ 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)
|
||||
|
||||
@@ -89,7 +101,7 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
|
||||
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 +151,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
|
||||
|
||||
@@ -178,14 +190,14 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi
|
||||
|
||||
if filename_len > 0xFFFF:
|
||||
print("Filename exceeds max size, cannot send")
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
|
||||
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)
|
||||
fetch_resource = RNS.Resource(temp_file, target_link, auto_compress=fetch_auto_compress)
|
||||
return True
|
||||
else:
|
||||
return None
|
||||
@@ -342,10 +354,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,7 +369,7 @@ 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)
|
||||
|
||||
@@ -366,7 +378,7 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No
|
||||
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)
|
||||
RNS.exit(2)
|
||||
else:
|
||||
identity = None
|
||||
|
||||
@@ -398,7 +410,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 +439,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 +453,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,10 +473,11 @@ 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
|
||||
@@ -518,25 +532,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,29 +572,34 @@ 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):
|
||||
@@ -599,13 +618,13 @@ 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")
|
||||
@@ -614,7 +633,7 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
|
||||
if filename_len > 0xFFFF:
|
||||
print("Filename exceeds max size, cannot send")
|
||||
exit(1)
|
||||
RNS.exit(1)
|
||||
else:
|
||||
print("Preparing file...", end=es)
|
||||
|
||||
@@ -632,7 +651,7 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
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)
|
||||
RNS.exit(2)
|
||||
else:
|
||||
identity = None
|
||||
|
||||
@@ -664,7 +683,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 +712,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...")
|
||||
@@ -727,7 +746,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 +792,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))
|
||||
@@ -783,7 +802,7 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non
|
||||
time.sleep(0.25)
|
||||
real_file.close()
|
||||
temp_file.close()
|
||||
exit(0)
|
||||
RNS.exit(0)
|
||||
|
||||
def main():
|
||||
try:
|
||||
@@ -818,6 +837,7 @@ def main():
|
||||
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,
|
||||
@@ -868,7 +888,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']
|
||||
|
||||
+26
-14
@@ -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")
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -199,6 +207,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
|
||||
@@ -261,6 +274,7 @@ products = {
|
||||
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 = {
|
||||
@@ -317,6 +331,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"],
|
||||
}
|
||||
@@ -1720,6 +1736,7 @@ def main():
|
||||
print("[12] LilyGO T-Beam Supreme")
|
||||
print("[13] LilyGO T-Deck")
|
||||
print("[14] Heltec T114")
|
||||
print("[15] Seeed XIAO ESP32S3 Wio-SX1262")
|
||||
print("")
|
||||
print("---------------------------------------------------------------------------")
|
||||
print("\nEnter the number that matches your device type:\n? ", end="")
|
||||
@@ -1728,7 +1745,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 > 15:
|
||||
raise ValueError()
|
||||
elif c_dev == 1:
|
||||
selected_product = ROM.PRODUCT_RNODE
|
||||
@@ -1924,6 +1941,19 @@ def main():
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
elif c_dev == 15:
|
||||
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 courtesey to those")
|
||||
print("who would like to experiment with it. Hit enter to continue.")
|
||||
print("---------------------------------------------------------------------------")
|
||||
input()
|
||||
|
||||
except Exception as e:
|
||||
print("That device type does not exist, exiting now.")
|
||||
@@ -2268,7 +2298,26 @@ 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")
|
||||
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")
|
||||
@@ -3060,6 +3109,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,
|
||||
|
||||
+12
-4
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+21
-10
@@ -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("")
|
||||
|
||||
@@ -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,
|
||||
@@ -250,6 +258,8 @@ def program_setup(configdir, dispall=False, verbosity=0, name_filter=None, json=
|
||||
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("I2PInterfacePeer[Connected peer") or
|
||||
(name.startswith("I2PInterface[") and ("i2p_connectable" in ifstat and ifstat["i2p_connectable"] == False))
|
||||
):
|
||||
|
||||
+12
-4
@@ -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,
|
||||
|
||||
+23
-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,
|
||||
@@ -49,6 +57,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 +127,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 +137,12 @@ 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: print(logstring)
|
||||
|
||||
elif (logdest == LOG_FILE and logfile != None):
|
||||
try:
|
||||
@@ -362,13 +377,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__ = "0.9.5"
|
||||
|
||||
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)
|
||||
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
|
||||
config: 4e1b5f77b3c7f5d0453477a09dcc765c
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ Linux, macOS and Windows.
|
||||
:align: center
|
||||
:target: _images/sideband_devices.webp
|
||||
|
||||
.. only:: latexpdf
|
||||
.. only:: latex
|
||||
|
||||
.. image:: screenshots/sideband_devices.png
|
||||
:align: center
|
||||
@@ -149,7 +149,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
|
||||
|
||||
@@ -88,7 +88,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 +99,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 +111,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 +123,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 +135,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** `RAK Wireless <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 +171,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 +183,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 +195,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 +207,43 @@ 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** 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 +255,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
|
||||
|
||||
@@ -34,12 +34,26 @@ 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.
|
||||
|
||||
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::
|
||||
|
||||
@@ -48,40 +62,34 @@ system, which should be enabled by default in almost all OSes.
|
||||
# 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
|
||||
|
||||
|
||||
@@ -95,7 +103,7 @@ the discovery scope by setting it to one of ``link``, ``admin``, ``site``,
|
||||
|
||||
[[Default Interface]]
|
||||
type = AutoInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Configure global discovery
|
||||
|
||||
@@ -108,73 +116,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::
|
||||
|
||||
# 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::
|
||||
|
||||
# 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::
|
||||
|
||||
# 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::
|
||||
|
||||
# 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::
|
||||
|
||||
[[Yggdrasil Remote]]
|
||||
type = BackboneInterface
|
||||
enabled = yes
|
||||
target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5
|
||||
target_port = 4343
|
||||
|
||||
.. _interfaces-tcps:
|
||||
|
||||
@@ -188,28 +237,27 @@ configured, other Reticulum peers can connect to it with a TCP Client interface.
|
||||
.. code::
|
||||
|
||||
# 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:
|
||||
@@ -222,11 +270,10 @@ 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:
|
||||
@@ -234,10 +281,10 @@ can simply specify the Yggdrasil ``tun`` device and a listening port, like so:
|
||||
.. code::
|
||||
|
||||
[[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 +293,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 +307,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.
|
||||
|
||||
@@ -272,10 +319,9 @@ and restore connectivity after a failure, once the other end of a TCP interface
|
||||
|
||||
# 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
|
||||
|
||||
@@ -286,7 +332,7 @@ specify the target Yggdrasil IPv6 address and port, like so:
|
||||
|
||||
[[Yggdrasil TCP Client Interface]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = yes
|
||||
enabled = yes
|
||||
target_host = 201:5d78:af73:5caf:a4de:a79f:3278:71e5
|
||||
target_port = 4343
|
||||
|
||||
@@ -301,7 +347,7 @@ software-based soundmodems. To do this, use the ``kiss_framing`` option:
|
||||
|
||||
[[TCP KISS Interface]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
kiss_framing = True
|
||||
target_host = 127.0.0.1
|
||||
target_port = 8001
|
||||
@@ -321,7 +367,7 @@ intermittent TCP links.
|
||||
|
||||
[[TCP Client over I2P]]
|
||||
type = TCPClientInterface
|
||||
interface_enabled = yes
|
||||
enabled = yes
|
||||
target_host = 127.0.0.1
|
||||
target_port = 5001
|
||||
i2p_tunneled = yes
|
||||
@@ -351,7 +397,7 @@ with all other peers on a local area network.
|
||||
|
||||
[[UDP Interface]]
|
||||
type = UDPInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
listen_ip = 0.0.0.0
|
||||
listen_port = 4242
|
||||
@@ -389,6 +435,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::
|
||||
|
||||
[[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::
|
||||
|
||||
[[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
|
||||
@@ -411,7 +525,7 @@ 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
|
||||
@@ -503,7 +617,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 +633,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 +675,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
|
||||
@@ -614,7 +728,7 @@ directly over a wire-pair, or for using devices such as data radios and lasers.
|
||||
|
||||
[[Serial Interface]]
|
||||
type = SerialInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB0
|
||||
@@ -639,7 +753,7 @@ custom hardware or other systems.
|
||||
|
||||
[[Pipe Interface]]
|
||||
type = PipeInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# External command to execute
|
||||
command = netcat -l 5757
|
||||
@@ -670,7 +784,7 @@ for station identification purposes.
|
||||
|
||||
[[Packet Radio KISS Interface]]
|
||||
type = KISSInterface
|
||||
interface_enabled = True
|
||||
enabled = yes
|
||||
|
||||
# Serial port for the device
|
||||
port = /dev/ttyUSB1
|
||||
@@ -744,7 +858,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
|
||||
|
||||
@@ -147,7 +147,7 @@ A member of the organisation at site D, named Dori, is willing to help by sharin
|
||||
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
|
||||
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.
|
||||
|
||||
|
||||
@@ -858,9 +858,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 +880,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-128 or 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 +892,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``, ``AES-128-CBC`` 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``,
|
||||
|
||||
@@ -68,7 +68,7 @@ What does Reticulum Offer?
|
||||
|
||||
* Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519
|
||||
|
||||
* AES-128 in CBC mode with PKCS7 padding
|
||||
* AES-128 or AES-256 in CBC mode with PKCS7 padding
|
||||
|
||||
* HMAC using SHA256 for authentication
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var DOCUMENTATION_OPTIONS = {
|
||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||
VERSION: '0.9.2 beta',
|
||||
VERSION: '0.9.5 beta',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
+230
-230
@@ -5,83 +5,83 @@
|
||||
.highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.highlight .hll { background-color: #ffffcc }
|
||||
.highlight { background: #f8f8f8; }
|
||||
.highlight .c { color: #8f5902; font-style: italic } /* Comment */
|
||||
.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */
|
||||
.highlight .g { color: #000000 } /* Generic */
|
||||
.highlight .k { color: #204a87; font-weight: bold } /* Keyword */
|
||||
.highlight .l { color: #000000 } /* Literal */
|
||||
.highlight .n { color: #000000 } /* Name */
|
||||
.highlight .o { color: #ce5c00; font-weight: bold } /* Operator */
|
||||
.highlight .x { color: #000000 } /* Other */
|
||||
.highlight .p { color: #000000; font-weight: bold } /* Punctuation */
|
||||
.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */
|
||||
.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #8f5902; font-style: italic } /* Comment.Preproc */
|
||||
.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */
|
||||
.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { color: #a40000 } /* Generic.Deleted */
|
||||
.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */
|
||||
.highlight .ges { color: #000000; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
.highlight .gr { color: #ef2929 } /* Generic.Error */
|
||||
.highlight .c { color: #8F5902; font-style: italic } /* Comment */
|
||||
.highlight .err { color: #A40000; border: 1px solid #EF2929 } /* Error */
|
||||
.highlight .g { color: #000 } /* Generic */
|
||||
.highlight .k { color: #204A87; font-weight: bold } /* Keyword */
|
||||
.highlight .l { color: #000 } /* Literal */
|
||||
.highlight .n { color: #000 } /* Name */
|
||||
.highlight .o { color: #CE5C00; font-weight: bold } /* Operator */
|
||||
.highlight .x { color: #000 } /* Other */
|
||||
.highlight .p { color: #000; font-weight: bold } /* Punctuation */
|
||||
.highlight .ch { color: #8F5902; font-style: italic } /* Comment.Hashbang */
|
||||
.highlight .cm { color: #8F5902; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #8F5902; font-style: italic } /* Comment.Preproc */
|
||||
.highlight .cpf { color: #8F5902; font-style: italic } /* Comment.PreprocFile */
|
||||
.highlight .c1 { color: #8F5902; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #8F5902; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { color: #A40000 } /* Generic.Deleted */
|
||||
.highlight .ge { color: #000; font-style: italic } /* Generic.Emph */
|
||||
.highlight .ges { color: #000; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
.highlight .gr { color: #EF2929 } /* Generic.Error */
|
||||
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.highlight .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.highlight .go { color: #000000; font-style: italic } /* Generic.Output */
|
||||
.highlight .gp { color: #8f5902 } /* Generic.Prompt */
|
||||
.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */
|
||||
.highlight .go { color: #000; font-style: italic } /* Generic.Output */
|
||||
.highlight .gp { color: #8F5902 } /* Generic.Prompt */
|
||||
.highlight .gs { color: #000; font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */
|
||||
.highlight .kc { color: #204a87; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #204a87; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #204a87; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #204a87; font-weight: bold } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #204a87; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #204a87; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .ld { color: #000000 } /* Literal.Date */
|
||||
.highlight .m { color: #0000cf; font-weight: bold } /* Literal.Number */
|
||||
.highlight .s { color: #4e9a06 } /* Literal.String */
|
||||
.highlight .na { color: #c4a000 } /* Name.Attribute */
|
||||
.highlight .nb { color: #204a87 } /* Name.Builtin */
|
||||
.highlight .nc { color: #000000 } /* Name.Class */
|
||||
.highlight .no { color: #000000 } /* Name.Constant */
|
||||
.highlight .nd { color: #5c35cc; font-weight: bold } /* Name.Decorator */
|
||||
.highlight .ni { color: #ce5c00 } /* Name.Entity */
|
||||
.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #000000 } /* Name.Function */
|
||||
.highlight .nl { color: #f57900 } /* Name.Label */
|
||||
.highlight .nn { color: #000000 } /* Name.Namespace */
|
||||
.highlight .nx { color: #000000 } /* Name.Other */
|
||||
.highlight .py { color: #000000 } /* Name.Property */
|
||||
.highlight .nt { color: #204a87; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #000000 } /* Name.Variable */
|
||||
.highlight .ow { color: #204a87; font-weight: bold } /* Operator.Word */
|
||||
.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */
|
||||
.highlight .w { color: #f8f8f8 } /* Text.Whitespace */
|
||||
.highlight .mb { color: #0000cf; font-weight: bold } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: #0000cf; font-weight: bold } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #0000cf; font-weight: bold } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #0000cf; font-weight: bold } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #0000cf; font-weight: bold } /* Literal.Number.Oct */
|
||||
.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */
|
||||
.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #4e9a06 } /* Literal.String.Char */
|
||||
.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */
|
||||
.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */
|
||||
.highlight .se { color: #4e9a06 } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #4e9a06 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */
|
||||
.highlight .fm { color: #000000 } /* Name.Function.Magic */
|
||||
.highlight .vc { color: #000000 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #000000 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #000000 } /* Name.Variable.Instance */
|
||||
.highlight .vm { color: #000000 } /* Name.Variable.Magic */
|
||||
.highlight .il { color: #0000cf; font-weight: bold } /* Literal.Number.Integer.Long */
|
||||
.highlight .gt { color: #A40000; font-weight: bold } /* Generic.Traceback */
|
||||
.highlight .kc { color: #204A87; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #204A87; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #204A87; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #204A87; font-weight: bold } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #204A87; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #204A87; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .ld { color: #000 } /* Literal.Date */
|
||||
.highlight .m { color: #0000CF; font-weight: bold } /* Literal.Number */
|
||||
.highlight .s { color: #4E9A06 } /* Literal.String */
|
||||
.highlight .na { color: #C4A000 } /* Name.Attribute */
|
||||
.highlight .nb { color: #204A87 } /* Name.Builtin */
|
||||
.highlight .nc { color: #000 } /* Name.Class */
|
||||
.highlight .no { color: #000 } /* Name.Constant */
|
||||
.highlight .nd { color: #5C35CC; font-weight: bold } /* Name.Decorator */
|
||||
.highlight .ni { color: #CE5C00 } /* Name.Entity */
|
||||
.highlight .ne { color: #C00; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #000 } /* Name.Function */
|
||||
.highlight .nl { color: #F57900 } /* Name.Label */
|
||||
.highlight .nn { color: #000 } /* Name.Namespace */
|
||||
.highlight .nx { color: #000 } /* Name.Other */
|
||||
.highlight .py { color: #000 } /* Name.Property */
|
||||
.highlight .nt { color: #204A87; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #000 } /* Name.Variable */
|
||||
.highlight .ow { color: #204A87; font-weight: bold } /* Operator.Word */
|
||||
.highlight .pm { color: #000; font-weight: bold } /* Punctuation.Marker */
|
||||
.highlight .w { color: #F8F8F8 } /* Text.Whitespace */
|
||||
.highlight .mb { color: #0000CF; font-weight: bold } /* Literal.Number.Bin */
|
||||
.highlight .mf { color: #0000CF; font-weight: bold } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #0000CF; font-weight: bold } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #0000CF; font-weight: bold } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #0000CF; font-weight: bold } /* Literal.Number.Oct */
|
||||
.highlight .sa { color: #4E9A06 } /* Literal.String.Affix */
|
||||
.highlight .sb { color: #4E9A06 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #4E9A06 } /* Literal.String.Char */
|
||||
.highlight .dl { color: #4E9A06 } /* Literal.String.Delimiter */
|
||||
.highlight .sd { color: #8F5902; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #4E9A06 } /* Literal.String.Double */
|
||||
.highlight .se { color: #4E9A06 } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #4E9A06 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #4E9A06 } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #4E9A06 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #4E9A06 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #4E9A06 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #4E9A06 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #3465A4 } /* Name.Builtin.Pseudo */
|
||||
.highlight .fm { color: #000 } /* Name.Function.Magic */
|
||||
.highlight .vc { color: #000 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #000 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #000 } /* Name.Variable.Instance */
|
||||
.highlight .vm { color: #000 } /* Name.Variable.Magic */
|
||||
.highlight .il { color: #0000CF; font-weight: bold } /* Literal.Number.Integer.Long */
|
||||
@media not print {
|
||||
body[data-theme="dark"] .highlight pre { line-height: 125%; }
|
||||
body[data-theme="dark"] .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
@@ -89,85 +89,85 @@ body[data-theme="dark"] .highlight span.linenos { color: #aaaaaa; background-col
|
||||
body[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
body[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
body[data-theme="dark"] .highlight .hll { background-color: #404040 }
|
||||
body[data-theme="dark"] .highlight { background: #202020; color: #d0d0d0 }
|
||||
body[data-theme="dark"] .highlight .c { color: #ababab; font-style: italic } /* Comment */
|
||||
body[data-theme="dark"] .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
||||
body[data-theme="dark"] .highlight .esc { color: #d0d0d0 } /* Escape */
|
||||
body[data-theme="dark"] .highlight .g { color: #d0d0d0 } /* Generic */
|
||||
body[data-theme="dark"] .highlight .k { color: #6ebf26; font-weight: bold } /* Keyword */
|
||||
body[data-theme="dark"] .highlight .l { color: #d0d0d0 } /* Literal */
|
||||
body[data-theme="dark"] .highlight .n { color: #d0d0d0 } /* Name */
|
||||
body[data-theme="dark"] .highlight .o { color: #d0d0d0 } /* Operator */
|
||||
body[data-theme="dark"] .highlight .x { color: #d0d0d0 } /* Other */
|
||||
body[data-theme="dark"] .highlight .p { color: #d0d0d0 } /* Punctuation */
|
||||
body[data-theme="dark"] .highlight .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
|
||||
body[data-theme="dark"] .highlight .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
|
||||
body[data-theme="dark"] .highlight .cp { color: #ff3a3a; font-weight: bold } /* Comment.Preproc */
|
||||
body[data-theme="dark"] .highlight .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
|
||||
body[data-theme="dark"] .highlight .c1 { color: #ababab; font-style: italic } /* Comment.Single */
|
||||
body[data-theme="dark"] .highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
||||
body[data-theme="dark"] .highlight .gd { color: #ff3a3a } /* Generic.Deleted */
|
||||
body[data-theme="dark"] .highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
|
||||
body[data-theme="dark"] .highlight .ges { color: #d0d0d0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
body[data-theme="dark"] .highlight .gr { color: #ff3a3a } /* Generic.Error */
|
||||
body[data-theme="dark"] .highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
|
||||
body[data-theme="dark"] .highlight { background: #202020; color: #D0D0D0 }
|
||||
body[data-theme="dark"] .highlight .c { color: #ABABAB; font-style: italic } /* Comment */
|
||||
body[data-theme="dark"] .highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */
|
||||
body[data-theme="dark"] .highlight .esc { color: #D0D0D0 } /* Escape */
|
||||
body[data-theme="dark"] .highlight .g { color: #D0D0D0 } /* Generic */
|
||||
body[data-theme="dark"] .highlight .k { color: #6EBF26; font-weight: bold } /* Keyword */
|
||||
body[data-theme="dark"] .highlight .l { color: #D0D0D0 } /* Literal */
|
||||
body[data-theme="dark"] .highlight .n { color: #D0D0D0 } /* Name */
|
||||
body[data-theme="dark"] .highlight .o { color: #D0D0D0 } /* Operator */
|
||||
body[data-theme="dark"] .highlight .x { color: #D0D0D0 } /* Other */
|
||||
body[data-theme="dark"] .highlight .p { color: #D0D0D0 } /* Punctuation */
|
||||
body[data-theme="dark"] .highlight .ch { color: #ABABAB; font-style: italic } /* Comment.Hashbang */
|
||||
body[data-theme="dark"] .highlight .cm { color: #ABABAB; font-style: italic } /* Comment.Multiline */
|
||||
body[data-theme="dark"] .highlight .cp { color: #FF3A3A; font-weight: bold } /* Comment.Preproc */
|
||||
body[data-theme="dark"] .highlight .cpf { color: #ABABAB; font-style: italic } /* Comment.PreprocFile */
|
||||
body[data-theme="dark"] .highlight .c1 { color: #ABABAB; font-style: italic } /* Comment.Single */
|
||||
body[data-theme="dark"] .highlight .cs { color: #E50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
||||
body[data-theme="dark"] .highlight .gd { color: #FF3A3A } /* Generic.Deleted */
|
||||
body[data-theme="dark"] .highlight .ge { color: #D0D0D0; font-style: italic } /* Generic.Emph */
|
||||
body[data-theme="dark"] .highlight .ges { color: #D0D0D0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
body[data-theme="dark"] .highlight .gr { color: #FF3A3A } /* Generic.Error */
|
||||
body[data-theme="dark"] .highlight .gh { color: #FFF; font-weight: bold } /* Generic.Heading */
|
||||
body[data-theme="dark"] .highlight .gi { color: #589819 } /* Generic.Inserted */
|
||||
body[data-theme="dark"] .highlight .go { color: #cccccc } /* Generic.Output */
|
||||
body[data-theme="dark"] .highlight .gp { color: #aaaaaa } /* Generic.Prompt */
|
||||
body[data-theme="dark"] .highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
|
||||
body[data-theme="dark"] .highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
|
||||
body[data-theme="dark"] .highlight .gt { color: #ff3a3a } /* Generic.Traceback */
|
||||
body[data-theme="dark"] .highlight .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
|
||||
body[data-theme="dark"] .highlight .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
|
||||
body[data-theme="dark"] .highlight .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
|
||||
body[data-theme="dark"] .highlight .kp { color: #6ebf26 } /* Keyword.Pseudo */
|
||||
body[data-theme="dark"] .highlight .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
|
||||
body[data-theme="dark"] .highlight .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
|
||||
body[data-theme="dark"] .highlight .ld { color: #d0d0d0 } /* Literal.Date */
|
||||
body[data-theme="dark"] .highlight .m { color: #51b2fd } /* Literal.Number */
|
||||
body[data-theme="dark"] .highlight .s { color: #ed9d13 } /* Literal.String */
|
||||
body[data-theme="dark"] .highlight .na { color: #bbbbbb } /* Name.Attribute */
|
||||
body[data-theme="dark"] .highlight .nb { color: #2fbccd } /* Name.Builtin */
|
||||
body[data-theme="dark"] .highlight .nc { color: #71adff; text-decoration: underline } /* Name.Class */
|
||||
body[data-theme="dark"] .highlight .no { color: #40ffff } /* Name.Constant */
|
||||
body[data-theme="dark"] .highlight .nd { color: #ffa500 } /* Name.Decorator */
|
||||
body[data-theme="dark"] .highlight .ni { color: #d0d0d0 } /* Name.Entity */
|
||||
body[data-theme="dark"] .highlight .ne { color: #bbbbbb } /* Name.Exception */
|
||||
body[data-theme="dark"] .highlight .nf { color: #71adff } /* Name.Function */
|
||||
body[data-theme="dark"] .highlight .nl { color: #d0d0d0 } /* Name.Label */
|
||||
body[data-theme="dark"] .highlight .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
|
||||
body[data-theme="dark"] .highlight .nx { color: #d0d0d0 } /* Name.Other */
|
||||
body[data-theme="dark"] .highlight .py { color: #d0d0d0 } /* Name.Property */
|
||||
body[data-theme="dark"] .highlight .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
|
||||
body[data-theme="dark"] .highlight .nv { color: #40ffff } /* Name.Variable */
|
||||
body[data-theme="dark"] .highlight .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
|
||||
body[data-theme="dark"] .highlight .pm { color: #d0d0d0 } /* Punctuation.Marker */
|
||||
body[data-theme="dark"] .highlight .w { color: #666666 } /* Text.Whitespace */
|
||||
body[data-theme="dark"] .highlight .mb { color: #51b2fd } /* Literal.Number.Bin */
|
||||
body[data-theme="dark"] .highlight .mf { color: #51b2fd } /* Literal.Number.Float */
|
||||
body[data-theme="dark"] .highlight .mh { color: #51b2fd } /* Literal.Number.Hex */
|
||||
body[data-theme="dark"] .highlight .mi { color: #51b2fd } /* Literal.Number.Integer */
|
||||
body[data-theme="dark"] .highlight .mo { color: #51b2fd } /* Literal.Number.Oct */
|
||||
body[data-theme="dark"] .highlight .sa { color: #ed9d13 } /* Literal.String.Affix */
|
||||
body[data-theme="dark"] .highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */
|
||||
body[data-theme="dark"] .highlight .sc { color: #ed9d13 } /* Literal.String.Char */
|
||||
body[data-theme="dark"] .highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */
|
||||
body[data-theme="dark"] .highlight .sd { color: #ed9d13 } /* Literal.String.Doc */
|
||||
body[data-theme="dark"] .highlight .s2 { color: #ed9d13 } /* Literal.String.Double */
|
||||
body[data-theme="dark"] .highlight .se { color: #ed9d13 } /* Literal.String.Escape */
|
||||
body[data-theme="dark"] .highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */
|
||||
body[data-theme="dark"] .highlight .si { color: #ed9d13 } /* Literal.String.Interpol */
|
||||
body[data-theme="dark"] .highlight .sx { color: #ffa500 } /* Literal.String.Other */
|
||||
body[data-theme="dark"] .highlight .sr { color: #ed9d13 } /* Literal.String.Regex */
|
||||
body[data-theme="dark"] .highlight .s1 { color: #ed9d13 } /* Literal.String.Single */
|
||||
body[data-theme="dark"] .highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */
|
||||
body[data-theme="dark"] .highlight .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
|
||||
body[data-theme="dark"] .highlight .fm { color: #71adff } /* Name.Function.Magic */
|
||||
body[data-theme="dark"] .highlight .vc { color: #40ffff } /* Name.Variable.Class */
|
||||
body[data-theme="dark"] .highlight .vg { color: #40ffff } /* Name.Variable.Global */
|
||||
body[data-theme="dark"] .highlight .vi { color: #40ffff } /* Name.Variable.Instance */
|
||||
body[data-theme="dark"] .highlight .vm { color: #40ffff } /* Name.Variable.Magic */
|
||||
body[data-theme="dark"] .highlight .il { color: #51b2fd } /* Literal.Number.Integer.Long */
|
||||
body[data-theme="dark"] .highlight .go { color: #CCC } /* Generic.Output */
|
||||
body[data-theme="dark"] .highlight .gp { color: #AAA } /* Generic.Prompt */
|
||||
body[data-theme="dark"] .highlight .gs { color: #D0D0D0; font-weight: bold } /* Generic.Strong */
|
||||
body[data-theme="dark"] .highlight .gu { color: #FFF; text-decoration: underline } /* Generic.Subheading */
|
||||
body[data-theme="dark"] .highlight .gt { color: #FF3A3A } /* Generic.Traceback */
|
||||
body[data-theme="dark"] .highlight .kc { color: #6EBF26; font-weight: bold } /* Keyword.Constant */
|
||||
body[data-theme="dark"] .highlight .kd { color: #6EBF26; font-weight: bold } /* Keyword.Declaration */
|
||||
body[data-theme="dark"] .highlight .kn { color: #6EBF26; font-weight: bold } /* Keyword.Namespace */
|
||||
body[data-theme="dark"] .highlight .kp { color: #6EBF26 } /* Keyword.Pseudo */
|
||||
body[data-theme="dark"] .highlight .kr { color: #6EBF26; font-weight: bold } /* Keyword.Reserved */
|
||||
body[data-theme="dark"] .highlight .kt { color: #6EBF26; font-weight: bold } /* Keyword.Type */
|
||||
body[data-theme="dark"] .highlight .ld { color: #D0D0D0 } /* Literal.Date */
|
||||
body[data-theme="dark"] .highlight .m { color: #51B2FD } /* Literal.Number */
|
||||
body[data-theme="dark"] .highlight .s { color: #ED9D13 } /* Literal.String */
|
||||
body[data-theme="dark"] .highlight .na { color: #BBB } /* Name.Attribute */
|
||||
body[data-theme="dark"] .highlight .nb { color: #2FBCCD } /* Name.Builtin */
|
||||
body[data-theme="dark"] .highlight .nc { color: #71ADFF; text-decoration: underline } /* Name.Class */
|
||||
body[data-theme="dark"] .highlight .no { color: #40FFFF } /* Name.Constant */
|
||||
body[data-theme="dark"] .highlight .nd { color: #FFA500 } /* Name.Decorator */
|
||||
body[data-theme="dark"] .highlight .ni { color: #D0D0D0 } /* Name.Entity */
|
||||
body[data-theme="dark"] .highlight .ne { color: #BBB } /* Name.Exception */
|
||||
body[data-theme="dark"] .highlight .nf { color: #71ADFF } /* Name.Function */
|
||||
body[data-theme="dark"] .highlight .nl { color: #D0D0D0 } /* Name.Label */
|
||||
body[data-theme="dark"] .highlight .nn { color: #71ADFF; text-decoration: underline } /* Name.Namespace */
|
||||
body[data-theme="dark"] .highlight .nx { color: #D0D0D0 } /* Name.Other */
|
||||
body[data-theme="dark"] .highlight .py { color: #D0D0D0 } /* Name.Property */
|
||||
body[data-theme="dark"] .highlight .nt { color: #6EBF26; font-weight: bold } /* Name.Tag */
|
||||
body[data-theme="dark"] .highlight .nv { color: #40FFFF } /* Name.Variable */
|
||||
body[data-theme="dark"] .highlight .ow { color: #6EBF26; font-weight: bold } /* Operator.Word */
|
||||
body[data-theme="dark"] .highlight .pm { color: #D0D0D0 } /* Punctuation.Marker */
|
||||
body[data-theme="dark"] .highlight .w { color: #666 } /* Text.Whitespace */
|
||||
body[data-theme="dark"] .highlight .mb { color: #51B2FD } /* Literal.Number.Bin */
|
||||
body[data-theme="dark"] .highlight .mf { color: #51B2FD } /* Literal.Number.Float */
|
||||
body[data-theme="dark"] .highlight .mh { color: #51B2FD } /* Literal.Number.Hex */
|
||||
body[data-theme="dark"] .highlight .mi { color: #51B2FD } /* Literal.Number.Integer */
|
||||
body[data-theme="dark"] .highlight .mo { color: #51B2FD } /* Literal.Number.Oct */
|
||||
body[data-theme="dark"] .highlight .sa { color: #ED9D13 } /* Literal.String.Affix */
|
||||
body[data-theme="dark"] .highlight .sb { color: #ED9D13 } /* Literal.String.Backtick */
|
||||
body[data-theme="dark"] .highlight .sc { color: #ED9D13 } /* Literal.String.Char */
|
||||
body[data-theme="dark"] .highlight .dl { color: #ED9D13 } /* Literal.String.Delimiter */
|
||||
body[data-theme="dark"] .highlight .sd { color: #ED9D13 } /* Literal.String.Doc */
|
||||
body[data-theme="dark"] .highlight .s2 { color: #ED9D13 } /* Literal.String.Double */
|
||||
body[data-theme="dark"] .highlight .se { color: #ED9D13 } /* Literal.String.Escape */
|
||||
body[data-theme="dark"] .highlight .sh { color: #ED9D13 } /* Literal.String.Heredoc */
|
||||
body[data-theme="dark"] .highlight .si { color: #ED9D13 } /* Literal.String.Interpol */
|
||||
body[data-theme="dark"] .highlight .sx { color: #FFA500 } /* Literal.String.Other */
|
||||
body[data-theme="dark"] .highlight .sr { color: #ED9D13 } /* Literal.String.Regex */
|
||||
body[data-theme="dark"] .highlight .s1 { color: #ED9D13 } /* Literal.String.Single */
|
||||
body[data-theme="dark"] .highlight .ss { color: #ED9D13 } /* Literal.String.Symbol */
|
||||
body[data-theme="dark"] .highlight .bp { color: #2FBCCD } /* Name.Builtin.Pseudo */
|
||||
body[data-theme="dark"] .highlight .fm { color: #71ADFF } /* Name.Function.Magic */
|
||||
body[data-theme="dark"] .highlight .vc { color: #40FFFF } /* Name.Variable.Class */
|
||||
body[data-theme="dark"] .highlight .vg { color: #40FFFF } /* Name.Variable.Global */
|
||||
body[data-theme="dark"] .highlight .vi { color: #40FFFF } /* Name.Variable.Instance */
|
||||
body[data-theme="dark"] .highlight .vm { color: #40FFFF } /* Name.Variable.Magic */
|
||||
body[data-theme="dark"] .highlight .il { color: #51B2FD } /* Literal.Number.Integer.Long */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body:not([data-theme="light"]) .highlight pre { line-height: 125%; }
|
||||
body:not([data-theme="light"]) .highlight td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
@@ -175,84 +175,84 @@ body:not([data-theme="light"]) .highlight span.linenos { color: #aaaaaa; backgro
|
||||
body:not([data-theme="light"]) .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
body:not([data-theme="light"]) .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
body:not([data-theme="light"]) .highlight .hll { background-color: #404040 }
|
||||
body:not([data-theme="light"]) .highlight { background: #202020; color: #d0d0d0 }
|
||||
body:not([data-theme="light"]) .highlight .c { color: #ababab; font-style: italic } /* Comment */
|
||||
body:not([data-theme="light"]) .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
|
||||
body:not([data-theme="light"]) .highlight .esc { color: #d0d0d0 } /* Escape */
|
||||
body:not([data-theme="light"]) .highlight .g { color: #d0d0d0 } /* Generic */
|
||||
body:not([data-theme="light"]) .highlight .k { color: #6ebf26; font-weight: bold } /* Keyword */
|
||||
body:not([data-theme="light"]) .highlight .l { color: #d0d0d0 } /* Literal */
|
||||
body:not([data-theme="light"]) .highlight .n { color: #d0d0d0 } /* Name */
|
||||
body:not([data-theme="light"]) .highlight .o { color: #d0d0d0 } /* Operator */
|
||||
body:not([data-theme="light"]) .highlight .x { color: #d0d0d0 } /* Other */
|
||||
body:not([data-theme="light"]) .highlight .p { color: #d0d0d0 } /* Punctuation */
|
||||
body:not([data-theme="light"]) .highlight .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
|
||||
body:not([data-theme="light"]) .highlight .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
|
||||
body:not([data-theme="light"]) .highlight .cp { color: #ff3a3a; font-weight: bold } /* Comment.Preproc */
|
||||
body:not([data-theme="light"]) .highlight .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
|
||||
body:not([data-theme="light"]) .highlight .c1 { color: #ababab; font-style: italic } /* Comment.Single */
|
||||
body:not([data-theme="light"]) .highlight .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
||||
body:not([data-theme="light"]) .highlight .gd { color: #ff3a3a } /* Generic.Deleted */
|
||||
body:not([data-theme="light"]) .highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
|
||||
body:not([data-theme="light"]) .highlight .ges { color: #d0d0d0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
body:not([data-theme="light"]) .highlight .gr { color: #ff3a3a } /* Generic.Error */
|
||||
body:not([data-theme="light"]) .highlight .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
|
||||
body:not([data-theme="light"]) .highlight { background: #202020; color: #D0D0D0 }
|
||||
body:not([data-theme="light"]) .highlight .c { color: #ABABAB; font-style: italic } /* Comment */
|
||||
body:not([data-theme="light"]) .highlight .err { color: #A61717; background-color: #E3D2D2 } /* Error */
|
||||
body:not([data-theme="light"]) .highlight .esc { color: #D0D0D0 } /* Escape */
|
||||
body:not([data-theme="light"]) .highlight .g { color: #D0D0D0 } /* Generic */
|
||||
body:not([data-theme="light"]) .highlight .k { color: #6EBF26; font-weight: bold } /* Keyword */
|
||||
body:not([data-theme="light"]) .highlight .l { color: #D0D0D0 } /* Literal */
|
||||
body:not([data-theme="light"]) .highlight .n { color: #D0D0D0 } /* Name */
|
||||
body:not([data-theme="light"]) .highlight .o { color: #D0D0D0 } /* Operator */
|
||||
body:not([data-theme="light"]) .highlight .x { color: #D0D0D0 } /* Other */
|
||||
body:not([data-theme="light"]) .highlight .p { color: #D0D0D0 } /* Punctuation */
|
||||
body:not([data-theme="light"]) .highlight .ch { color: #ABABAB; font-style: italic } /* Comment.Hashbang */
|
||||
body:not([data-theme="light"]) .highlight .cm { color: #ABABAB; font-style: italic } /* Comment.Multiline */
|
||||
body:not([data-theme="light"]) .highlight .cp { color: #FF3A3A; font-weight: bold } /* Comment.Preproc */
|
||||
body:not([data-theme="light"]) .highlight .cpf { color: #ABABAB; font-style: italic } /* Comment.PreprocFile */
|
||||
body:not([data-theme="light"]) .highlight .c1 { color: #ABABAB; font-style: italic } /* Comment.Single */
|
||||
body:not([data-theme="light"]) .highlight .cs { color: #E50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
|
||||
body:not([data-theme="light"]) .highlight .gd { color: #FF3A3A } /* Generic.Deleted */
|
||||
body:not([data-theme="light"]) .highlight .ge { color: #D0D0D0; font-style: italic } /* Generic.Emph */
|
||||
body:not([data-theme="light"]) .highlight .ges { color: #D0D0D0; font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
body:not([data-theme="light"]) .highlight .gr { color: #FF3A3A } /* Generic.Error */
|
||||
body:not([data-theme="light"]) .highlight .gh { color: #FFF; font-weight: bold } /* Generic.Heading */
|
||||
body:not([data-theme="light"]) .highlight .gi { color: #589819 } /* Generic.Inserted */
|
||||
body:not([data-theme="light"]) .highlight .go { color: #cccccc } /* Generic.Output */
|
||||
body:not([data-theme="light"]) .highlight .gp { color: #aaaaaa } /* Generic.Prompt */
|
||||
body:not([data-theme="light"]) .highlight .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
|
||||
body:not([data-theme="light"]) .highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
|
||||
body:not([data-theme="light"]) .highlight .gt { color: #ff3a3a } /* Generic.Traceback */
|
||||
body:not([data-theme="light"]) .highlight .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
|
||||
body:not([data-theme="light"]) .highlight .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
|
||||
body:not([data-theme="light"]) .highlight .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
|
||||
body:not([data-theme="light"]) .highlight .kp { color: #6ebf26 } /* Keyword.Pseudo */
|
||||
body:not([data-theme="light"]) .highlight .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
|
||||
body:not([data-theme="light"]) .highlight .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
|
||||
body:not([data-theme="light"]) .highlight .ld { color: #d0d0d0 } /* Literal.Date */
|
||||
body:not([data-theme="light"]) .highlight .m { color: #51b2fd } /* Literal.Number */
|
||||
body:not([data-theme="light"]) .highlight .s { color: #ed9d13 } /* Literal.String */
|
||||
body:not([data-theme="light"]) .highlight .na { color: #bbbbbb } /* Name.Attribute */
|
||||
body:not([data-theme="light"]) .highlight .nb { color: #2fbccd } /* Name.Builtin */
|
||||
body:not([data-theme="light"]) .highlight .nc { color: #71adff; text-decoration: underline } /* Name.Class */
|
||||
body:not([data-theme="light"]) .highlight .no { color: #40ffff } /* Name.Constant */
|
||||
body:not([data-theme="light"]) .highlight .nd { color: #ffa500 } /* Name.Decorator */
|
||||
body:not([data-theme="light"]) .highlight .ni { color: #d0d0d0 } /* Name.Entity */
|
||||
body:not([data-theme="light"]) .highlight .ne { color: #bbbbbb } /* Name.Exception */
|
||||
body:not([data-theme="light"]) .highlight .nf { color: #71adff } /* Name.Function */
|
||||
body:not([data-theme="light"]) .highlight .nl { color: #d0d0d0 } /* Name.Label */
|
||||
body:not([data-theme="light"]) .highlight .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
|
||||
body:not([data-theme="light"]) .highlight .nx { color: #d0d0d0 } /* Name.Other */
|
||||
body:not([data-theme="light"]) .highlight .py { color: #d0d0d0 } /* Name.Property */
|
||||
body:not([data-theme="light"]) .highlight .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
|
||||
body:not([data-theme="light"]) .highlight .nv { color: #40ffff } /* Name.Variable */
|
||||
body:not([data-theme="light"]) .highlight .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
|
||||
body:not([data-theme="light"]) .highlight .pm { color: #d0d0d0 } /* Punctuation.Marker */
|
||||
body:not([data-theme="light"]) .highlight .w { color: #666666 } /* Text.Whitespace */
|
||||
body:not([data-theme="light"]) .highlight .mb { color: #51b2fd } /* Literal.Number.Bin */
|
||||
body:not([data-theme="light"]) .highlight .mf { color: #51b2fd } /* Literal.Number.Float */
|
||||
body:not([data-theme="light"]) .highlight .mh { color: #51b2fd } /* Literal.Number.Hex */
|
||||
body:not([data-theme="light"]) .highlight .mi { color: #51b2fd } /* Literal.Number.Integer */
|
||||
body:not([data-theme="light"]) .highlight .mo { color: #51b2fd } /* Literal.Number.Oct */
|
||||
body:not([data-theme="light"]) .highlight .sa { color: #ed9d13 } /* Literal.String.Affix */
|
||||
body:not([data-theme="light"]) .highlight .sb { color: #ed9d13 } /* Literal.String.Backtick */
|
||||
body:not([data-theme="light"]) .highlight .sc { color: #ed9d13 } /* Literal.String.Char */
|
||||
body:not([data-theme="light"]) .highlight .dl { color: #ed9d13 } /* Literal.String.Delimiter */
|
||||
body:not([data-theme="light"]) .highlight .sd { color: #ed9d13 } /* Literal.String.Doc */
|
||||
body:not([data-theme="light"]) .highlight .s2 { color: #ed9d13 } /* Literal.String.Double */
|
||||
body:not([data-theme="light"]) .highlight .se { color: #ed9d13 } /* Literal.String.Escape */
|
||||
body:not([data-theme="light"]) .highlight .sh { color: #ed9d13 } /* Literal.String.Heredoc */
|
||||
body:not([data-theme="light"]) .highlight .si { color: #ed9d13 } /* Literal.String.Interpol */
|
||||
body:not([data-theme="light"]) .highlight .sx { color: #ffa500 } /* Literal.String.Other */
|
||||
body:not([data-theme="light"]) .highlight .sr { color: #ed9d13 } /* Literal.String.Regex */
|
||||
body:not([data-theme="light"]) .highlight .s1 { color: #ed9d13 } /* Literal.String.Single */
|
||||
body:not([data-theme="light"]) .highlight .ss { color: #ed9d13 } /* Literal.String.Symbol */
|
||||
body:not([data-theme="light"]) .highlight .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
|
||||
body:not([data-theme="light"]) .highlight .fm { color: #71adff } /* Name.Function.Magic */
|
||||
body:not([data-theme="light"]) .highlight .vc { color: #40ffff } /* Name.Variable.Class */
|
||||
body:not([data-theme="light"]) .highlight .vg { color: #40ffff } /* Name.Variable.Global */
|
||||
body:not([data-theme="light"]) .highlight .vi { color: #40ffff } /* Name.Variable.Instance */
|
||||
body:not([data-theme="light"]) .highlight .vm { color: #40ffff } /* Name.Variable.Magic */
|
||||
body:not([data-theme="light"]) .highlight .il { color: #51b2fd } /* Literal.Number.Integer.Long */
|
||||
body:not([data-theme="light"]) .highlight .go { color: #CCC } /* Generic.Output */
|
||||
body:not([data-theme="light"]) .highlight .gp { color: #AAA } /* Generic.Prompt */
|
||||
body:not([data-theme="light"]) .highlight .gs { color: #D0D0D0; font-weight: bold } /* Generic.Strong */
|
||||
body:not([data-theme="light"]) .highlight .gu { color: #FFF; text-decoration: underline } /* Generic.Subheading */
|
||||
body:not([data-theme="light"]) .highlight .gt { color: #FF3A3A } /* Generic.Traceback */
|
||||
body:not([data-theme="light"]) .highlight .kc { color: #6EBF26; font-weight: bold } /* Keyword.Constant */
|
||||
body:not([data-theme="light"]) .highlight .kd { color: #6EBF26; font-weight: bold } /* Keyword.Declaration */
|
||||
body:not([data-theme="light"]) .highlight .kn { color: #6EBF26; font-weight: bold } /* Keyword.Namespace */
|
||||
body:not([data-theme="light"]) .highlight .kp { color: #6EBF26 } /* Keyword.Pseudo */
|
||||
body:not([data-theme="light"]) .highlight .kr { color: #6EBF26; font-weight: bold } /* Keyword.Reserved */
|
||||
body:not([data-theme="light"]) .highlight .kt { color: #6EBF26; font-weight: bold } /* Keyword.Type */
|
||||
body:not([data-theme="light"]) .highlight .ld { color: #D0D0D0 } /* Literal.Date */
|
||||
body:not([data-theme="light"]) .highlight .m { color: #51B2FD } /* Literal.Number */
|
||||
body:not([data-theme="light"]) .highlight .s { color: #ED9D13 } /* Literal.String */
|
||||
body:not([data-theme="light"]) .highlight .na { color: #BBB } /* Name.Attribute */
|
||||
body:not([data-theme="light"]) .highlight .nb { color: #2FBCCD } /* Name.Builtin */
|
||||
body:not([data-theme="light"]) .highlight .nc { color: #71ADFF; text-decoration: underline } /* Name.Class */
|
||||
body:not([data-theme="light"]) .highlight .no { color: #40FFFF } /* Name.Constant */
|
||||
body:not([data-theme="light"]) .highlight .nd { color: #FFA500 } /* Name.Decorator */
|
||||
body:not([data-theme="light"]) .highlight .ni { color: #D0D0D0 } /* Name.Entity */
|
||||
body:not([data-theme="light"]) .highlight .ne { color: #BBB } /* Name.Exception */
|
||||
body:not([data-theme="light"]) .highlight .nf { color: #71ADFF } /* Name.Function */
|
||||
body:not([data-theme="light"]) .highlight .nl { color: #D0D0D0 } /* Name.Label */
|
||||
body:not([data-theme="light"]) .highlight .nn { color: #71ADFF; text-decoration: underline } /* Name.Namespace */
|
||||
body:not([data-theme="light"]) .highlight .nx { color: #D0D0D0 } /* Name.Other */
|
||||
body:not([data-theme="light"]) .highlight .py { color: #D0D0D0 } /* Name.Property */
|
||||
body:not([data-theme="light"]) .highlight .nt { color: #6EBF26; font-weight: bold } /* Name.Tag */
|
||||
body:not([data-theme="light"]) .highlight .nv { color: #40FFFF } /* Name.Variable */
|
||||
body:not([data-theme="light"]) .highlight .ow { color: #6EBF26; font-weight: bold } /* Operator.Word */
|
||||
body:not([data-theme="light"]) .highlight .pm { color: #D0D0D0 } /* Punctuation.Marker */
|
||||
body:not([data-theme="light"]) .highlight .w { color: #666 } /* Text.Whitespace */
|
||||
body:not([data-theme="light"]) .highlight .mb { color: #51B2FD } /* Literal.Number.Bin */
|
||||
body:not([data-theme="light"]) .highlight .mf { color: #51B2FD } /* Literal.Number.Float */
|
||||
body:not([data-theme="light"]) .highlight .mh { color: #51B2FD } /* Literal.Number.Hex */
|
||||
body:not([data-theme="light"]) .highlight .mi { color: #51B2FD } /* Literal.Number.Integer */
|
||||
body:not([data-theme="light"]) .highlight .mo { color: #51B2FD } /* Literal.Number.Oct */
|
||||
body:not([data-theme="light"]) .highlight .sa { color: #ED9D13 } /* Literal.String.Affix */
|
||||
body:not([data-theme="light"]) .highlight .sb { color: #ED9D13 } /* Literal.String.Backtick */
|
||||
body:not([data-theme="light"]) .highlight .sc { color: #ED9D13 } /* Literal.String.Char */
|
||||
body:not([data-theme="light"]) .highlight .dl { color: #ED9D13 } /* Literal.String.Delimiter */
|
||||
body:not([data-theme="light"]) .highlight .sd { color: #ED9D13 } /* Literal.String.Doc */
|
||||
body:not([data-theme="light"]) .highlight .s2 { color: #ED9D13 } /* Literal.String.Double */
|
||||
body:not([data-theme="light"]) .highlight .se { color: #ED9D13 } /* Literal.String.Escape */
|
||||
body:not([data-theme="light"]) .highlight .sh { color: #ED9D13 } /* Literal.String.Heredoc */
|
||||
body:not([data-theme="light"]) .highlight .si { color: #ED9D13 } /* Literal.String.Interpol */
|
||||
body:not([data-theme="light"]) .highlight .sx { color: #FFA500 } /* Literal.String.Other */
|
||||
body:not([data-theme="light"]) .highlight .sr { color: #ED9D13 } /* Literal.String.Regex */
|
||||
body:not([data-theme="light"]) .highlight .s1 { color: #ED9D13 } /* Literal.String.Single */
|
||||
body:not([data-theme="light"]) .highlight .ss { color: #ED9D13 } /* Literal.String.Symbol */
|
||||
body:not([data-theme="light"]) .highlight .bp { color: #2FBCCD } /* Name.Builtin.Pseudo */
|
||||
body:not([data-theme="light"]) .highlight .fm { color: #71ADFF } /* Name.Function.Magic */
|
||||
body:not([data-theme="light"]) .highlight .vc { color: #40FFFF } /* Name.Variable.Class */
|
||||
body:not([data-theme="light"]) .highlight .vg { color: #40FFFF } /* Name.Variable.Global */
|
||||
body:not([data-theme="light"]) .highlight .vi { color: #40FFFF } /* Name.Variable.Instance */
|
||||
body:not([data-theme="light"]) .highlight .vm { color: #40FFFF } /* Name.Variable.Magic */
|
||||
body:not([data-theme="light"]) .highlight .il { color: #51B2FD } /* Literal.Number.Integer.Long */
|
||||
}
|
||||
}
|
||||
+169
-171
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/>
|
||||
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 0.9.2 beta documentation</title>
|
||||
<title>An Explanation of Reticulum for Human Beings - Reticulum Network Stack 0.9.5 beta documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -141,7 +141,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.2 beta documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.5 beta documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -167,7 +167,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.2 beta documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.5 beta documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="#" /><link rel="search" title="Search" href="search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/><title>Index - Reticulum Network Stack 0.9.2 beta documentation</title>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/><title>Index - Reticulum Network Stack 0.9.5 beta documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -139,7 +139,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.2 beta documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.5 beta documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -165,7 +165,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.2 beta documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.5 beta documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -381,10 +381,18 @@
|
||||
<li><a href="reference.html#RNS.Resource.get_data_size">get_data_size() (RNS.Resource method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Link.get_establishment_rate">get_establishment_rate() (RNS.Link method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Link.get_expected_rate">get_expected_rate() (RNS.Link method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Resource.get_hash">get_hash() (RNS.Resource method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Reticulum.get_instance">get_instance() (RNS.Reticulum static method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Link.get_mdu">get_mdu() (RNS.Link method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Link.get_mode">get_mode() (RNS.Link method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Link.get_mtu">get_mtu() (RNS.Link method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Resource.get_parts">get_parts() (RNS.Resource method)</a>
|
||||
</li>
|
||||
@@ -402,14 +410,14 @@
|
||||
</ul></li>
|
||||
<li><a href="reference.html#RNS.Identity.get_public_key">get_public_key() (RNS.Identity method)</a>
|
||||
</li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Link.get_q">get_q() (RNS.Link method)</a>
|
||||
|
||||
<ul>
|
||||
<li><a href="reference.html#RNS.Packet.get_q">(RNS.Packet method)</a>
|
||||
</li>
|
||||
</ul></li>
|
||||
</ul></td>
|
||||
<td style="width: 33%; vertical-align: top;"><ul>
|
||||
<li><a href="reference.html#RNS.Identity.get_random_hash">get_random_hash() (RNS.Identity static method)</a>
|
||||
</li>
|
||||
<li><a href="reference.html#RNS.Link.get_remote_identity">get_remote_identity() (RNS.Link method)</a>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Using Reticulum on Your System" href="using.html" /><link rel="prev" title="What is Reticulum?" href="whatis.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/>
|
||||
<title>Getting Started Fast - Reticulum Network Stack 0.9.2 beta documentation</title>
|
||||
<title>Getting Started Fast - Reticulum Network Stack 0.9.5 beta documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -141,7 +141,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.2 beta documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.5 beta documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -167,7 +167,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.2 beta documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.5 beta documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
|
||||
+52
-27
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Configuring Interfaces" href="interfaces.html" /><link rel="prev" title="Understanding Reticulum" href="understanding.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/>
|
||||
<title>Communications Hardware - Reticulum Network Stack 0.9.2 beta documentation</title>
|
||||
<title>Communications Hardware - Reticulum Network Stack 0.9.5 beta documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -141,7 +141,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.2 beta documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.5 beta documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -167,7 +167,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.2 beta documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.5 beta documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -290,14 +290,15 @@ to the configuration.</p>
|
||||
<section id="supported-boards-and-devices">
|
||||
<span id="rnode-supported"></span><h3>Supported Boards and Devices<a class="headerlink" href="#supported-boards-and-devices" title="Permalink to this heading">#</a></h3>
|
||||
<p>To create one or more RNodes, you will need to obtain supported development
|
||||
boards. The following boards are supported by the auto-installer.</p>
|
||||
boards or completed devices. The following boards and devices are supported
|
||||
by the auto-installer.</p>
|
||||
<hr class="docutils" />
|
||||
<a class="reference internal image-reference" href="_images/board_tbeam_supreme.png"><img alt="_images/board_tbeam_supreme.png" class="align-center" src="_images/board_tbeam_supreme.png" style="width: 75%;" />
|
||||
</a>
|
||||
<section id="lilygo-t-beam-supreme">
|
||||
<h4>LilyGO T-Beam Supreme<a class="headerlink" href="#lilygo-t-beam-supreme" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262, SX1268</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262 or SX1268</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://lilygo.cn">LilyGO</a></p></li>
|
||||
</ul>
|
||||
@@ -308,7 +309,7 @@ boards. The following boards are supported by the auto-installer.</p>
|
||||
<section id="lilygo-t-beam">
|
||||
<h4>LilyGO T-Beam<a class="headerlink" href="#lilygo-t-beam" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262, SX1268, SX1276 and SX1278</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262, SX1268, SX1276 or SX1278</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://lilygo.cn">LilyGO</a></p></li>
|
||||
</ul>
|
||||
@@ -319,7 +320,7 @@ boards. The following boards are supported by the auto-installer.</p>
|
||||
<section id="lilygo-t3s3">
|
||||
<h4>LilyGO T3S3<a class="headerlink" href="#lilygo-t3s3" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262, SX1268, SX1276 and SX1278</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262, SX1268, SX1276 or SX1278</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://lilygo.cn">LilyGO</a></p></li>
|
||||
</ul>
|
||||
@@ -330,18 +331,29 @@ boards. The following boards are supported by the auto-installer.</p>
|
||||
<section id="rak4631-based-boards">
|
||||
<h4>RAK4631-based Boards<a class="headerlink" href="#rak4631-based-boards" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262, SX1268</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262 or SX1268</p></li>
|
||||
<li><p><strong>Device Platform</strong> nRF52</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://www.rakwireless.com">RAK Wireless</a></p></li>
|
||||
</ul>
|
||||
<hr class="docutils" />
|
||||
<a class="reference internal image-reference" href="_images/board_opencomxl.png"><img alt="_images/board_opencomxl.png" class="align-center" src="_images/board_opencomxl.png" style="width: 45%;" />
|
||||
</a>
|
||||
</section>
|
||||
<section id="opencom-xl">
|
||||
<h4>OpenCom XL<a class="headerlink" href="#opencom-xl" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver ICs</strong> Semtech SX1262 and SX1280 (dual transceiver)</p></li>
|
||||
<li><p><strong>Device Platform</strong> nRF52</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://liberatedsystems.co.uk/">RAK Wireless</a></p></li>
|
||||
</ul>
|
||||
<hr class="docutils" />
|
||||
<a class="reference internal image-reference" href="_images/board_rnodev2.png"><img alt="_images/board_rnodev2.png" class="align-center" src="_images/board_rnodev2.png" style="width: 68%;" />
|
||||
</a>
|
||||
</section>
|
||||
<section id="unsigned-rnode-v2-x">
|
||||
<h4>Unsigned RNode v2.x<a class="headerlink" href="#unsigned-rnode-v2-x" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 and SX1278</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 or SX1278</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://unsigned.io">unsigned.io</a></p></li>
|
||||
</ul>
|
||||
@@ -352,7 +364,7 @@ boards. The following boards are supported by the auto-installer.</p>
|
||||
<section id="lilygo-lora32-v2-1">
|
||||
<h4>LilyGO LoRa32 v2.1<a class="headerlink" href="#lilygo-lora32-v2-1" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 and SX1278</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 or SX1278</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://lilygo.cn">LilyGO</a></p></li>
|
||||
</ul>
|
||||
@@ -363,7 +375,7 @@ boards. The following boards are supported by the auto-installer.</p>
|
||||
<section id="lilygo-lora32-v2-0">
|
||||
<h4>LilyGO LoRa32 v2.0<a class="headerlink" href="#lilygo-lora32-v2-0" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 and SX1278</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 or SX1278</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://lilygo.cn">LilyGO</a></p></li>
|
||||
</ul>
|
||||
@@ -374,7 +386,7 @@ boards. The following boards are supported by the auto-installer.</p>
|
||||
<section id="lilygo-lora32-v1-0">
|
||||
<h4>LilyGO LoRa32 v1.0<a class="headerlink" href="#lilygo-lora32-v1-0" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 and SX1278</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 or SX1278</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://lilygo.cn">LilyGO</a></p></li>
|
||||
</ul>
|
||||
@@ -385,18 +397,40 @@ boards. The following boards are supported by the auto-installer.</p>
|
||||
<section id="lilygo-t-deck">
|
||||
<h4>LilyGO T-Deck<a class="headerlink" href="#lilygo-t-deck" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262, SX1268</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262 or SX1268</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://lilygo.cn">LilyGO</a></p></li>
|
||||
</ul>
|
||||
<hr class="docutils" />
|
||||
<a class="reference internal image-reference" href="_images/board_techo.png"><img alt="_images/board_techo.png" class="align-center" src="_images/board_techo.png" style="width: 45%;" />
|
||||
</a>
|
||||
</section>
|
||||
<section id="lilygo-t-echo">
|
||||
<h4>LilyGO T-Echo<a class="headerlink" href="#lilygo-t-echo" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262 or SX1268</p></li>
|
||||
<li><p><strong>Device Platform</strong> nRF52</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://lilygo.cn">LilyGO</a></p></li>
|
||||
</ul>
|
||||
<hr class="docutils" />
|
||||
<a class="reference internal image-reference" href="_images/board_t114.png"><img alt="_images/board_t114.png" class="align-center" src="_images/board_t114.png" style="width: 58%;" />
|
||||
</a>
|
||||
</section>
|
||||
<section id="heltec-t114">
|
||||
<h4>Heltec T114<a class="headerlink" href="#heltec-t114" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262 or SX1268</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://heltec.org">Heltec Automation</a></p></li>
|
||||
</ul>
|
||||
<hr class="docutils" />
|
||||
<a class="reference internal image-reference" href="_images/board_heltec32v30.png"><img alt="_images/board_heltec32v30.png" class="align-center" src="_images/board_heltec32v30.png" style="width: 58%;" />
|
||||
</a>
|
||||
</section>
|
||||
<section id="heltec-lora32-v3-0">
|
||||
<h4>Heltec LoRa32 v3.0<a class="headerlink" href="#heltec-lora32-v3-0" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262 and SX1268</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1262 or SX1268</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://heltec.org">Heltec Automation</a></p></li>
|
||||
</ul>
|
||||
@@ -407,22 +441,11 @@ boards. The following boards are supported by the auto-installer.</p>
|
||||
<section id="heltec-lora32-v2-0">
|
||||
<h4>Heltec LoRa32 v2.0<a class="headerlink" href="#heltec-lora32-v2-0" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 and SX1278</p></li>
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 or SX1278</p></li>
|
||||
<li><p><strong>Device Platform</strong> ESP32</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://heltec.org">Heltec Automation</a></p></li>
|
||||
</ul>
|
||||
<hr class="docutils" />
|
||||
<a class="reference internal image-reference" href="_images/board_rnode.png"><img alt="_images/board_rnode.png" class="align-center" src="_images/board_rnode.png" style="width: 50%;" />
|
||||
</a>
|
||||
</section>
|
||||
<section id="unsigned-rnode-v1-x">
|
||||
<h4>Unsigned RNode v1.x<a class="headerlink" href="#unsigned-rnode-v1-x" title="Permalink to this heading">#</a></h4>
|
||||
<ul class="simple">
|
||||
<li><p><strong>Transceiver IC</strong> Semtech SX1276 and SX1278</p></li>
|
||||
<li><p><strong>Device Platform</strong> AVR ATmega1284p</p></li>
|
||||
<li><p><strong>Manufacturer</strong> <a class="reference external" href="https://unsigned.io">unsigned.io</a></p></li>
|
||||
</ul>
|
||||
<hr class="docutils" />
|
||||
</section>
|
||||
</section>
|
||||
<section id="installation">
|
||||
@@ -569,14 +592,16 @@ can be used with Reticulum. This includes virtual software modems such as
|
||||
<li><a class="reference internal" href="#lilygo-t-beam">LilyGO T-Beam</a></li>
|
||||
<li><a class="reference internal" href="#lilygo-t3s3">LilyGO T3S3</a></li>
|
||||
<li><a class="reference internal" href="#rak4631-based-boards">RAK4631-based Boards</a></li>
|
||||
<li><a class="reference internal" href="#opencom-xl">OpenCom XL</a></li>
|
||||
<li><a class="reference internal" href="#unsigned-rnode-v2-x">Unsigned RNode v2.x</a></li>
|
||||
<li><a class="reference internal" href="#lilygo-lora32-v2-1">LilyGO LoRa32 v2.1</a></li>
|
||||
<li><a class="reference internal" href="#lilygo-lora32-v2-0">LilyGO LoRa32 v2.0</a></li>
|
||||
<li><a class="reference internal" href="#lilygo-lora32-v1-0">LilyGO LoRa32 v1.0</a></li>
|
||||
<li><a class="reference internal" href="#lilygo-t-deck">LilyGO T-Deck</a></li>
|
||||
<li><a class="reference internal" href="#lilygo-t-echo">LilyGO T-Echo</a></li>
|
||||
<li><a class="reference internal" href="#heltec-t114">Heltec T114</a></li>
|
||||
<li><a class="reference internal" href="#heltec-lora32-v3-0">Heltec LoRa32 v3.0</a></li>
|
||||
<li><a class="reference internal" href="#heltec-lora32-v2-0">Heltec LoRa32 v2.0</a></li>
|
||||
<li><a class="reference internal" href="#unsigned-rnode-v1-x">Unsigned RNode v1.x</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#installation">Installation</a></li>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="What is Reticulum?" href="whatis.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/>
|
||||
<title>Reticulum Network Stack 0.9.2 beta documentation</title>
|
||||
<title>Reticulum Network Stack 0.9.5 beta documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -141,7 +141,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 0.9.2 beta documentation</div></a>
|
||||
<a href="#"><div class="brand">Reticulum Network Stack 0.9.5 beta documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -167,7 +167,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.2 beta documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.5 beta documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -342,10 +342,15 @@ to participate in the development of Reticulum itself.</p>
|
||||
<li class="toctree-l1"><a class="reference internal" href="interfaces.html">Configuring Interfaces</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#custom-interfaces">Custom Interfaces</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#auto-interface">Auto Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#i2p-interface">I2P Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#backbone-interface">Backbone Interface</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#listeners">Listeners</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="interfaces.html#connecting-remotes">Connecting Remotes</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#tcp-server-interface">TCP Server Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#tcp-client-interface">TCP Client Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#udp-interface">UDP Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#i2p-interface">I2P Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#rnode-lora-interface">RNode LoRa Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#rnode-multi-interface">RNode Multi Interface</a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="interfaces.html#serial-interface">Serial Interface</a></li>
|
||||
|
||||
+219
-114
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Building Networks" href="networks.html" /><link rel="prev" title="Communications Hardware" href="hardware.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/>
|
||||
<title>Configuring Interfaces - Reticulum Network Stack 0.9.2 beta documentation</title>
|
||||
<title>Configuring Interfaces - Reticulum Network Stack 0.9.5 beta documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -141,7 +141,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.2 beta documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.5 beta documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -167,7 +167,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.2 beta documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.5 beta documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -242,51 +242,58 @@ example for basic interface code to build upon.</p>
|
||||
</section>
|
||||
<section id="auto-interface">
|
||||
<span id="interfaces-auto"></span><h2>Auto Interface<a class="headerlink" href="#auto-interface" title="Permalink to this heading">#</a></h2>
|
||||
<p>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.</p>
|
||||
<p>The <code class="docutils literal notranslate"><span class="pre">AutoInterface</span></code> 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 <strong>does not</strong> need any functional IP
|
||||
infrastructure like routers or DHCP servers, on your physical network.</p>
|
||||
<p>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.</p>
|
||||
<p>For <code class="docutils literal notranslate"><span class="pre">AutoInterface</span></code> 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.</p>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>Almost all current Ethernet and WiFi hardware will work without any kind
|
||||
of configuration or setup with <code class="docutils literal notranslate"><span class="pre">AutoInterface</span></code>, but a small subset of
|
||||
devices turn on options that limit device-to-device communication by default,
|
||||
resulting in <code class="docutils literal notranslate"><span class="pre">AutoInterface</span></code> 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.</p>
|
||||
</div>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example demonstrates a bare-minimum setup</span>
|
||||
<span class="c1"># of an Auto Interface. It will allow communica-</span>
|
||||
<span class="c1"># tion with all other reachable devices on all</span>
|
||||
<span class="c1"># usable physical ethernet-based devices that</span>
|
||||
<span class="c1"># are available on the system.</span>
|
||||
|
||||
<span class="p">[[</span><span class="n">Default</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">AutoInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># This example demonstrates an more specifically</span>
|
||||
<span class="c1"># configured Auto Interface, that only uses spe-</span>
|
||||
<span class="c1"># cific physical interfaces, and has a number of</span>
|
||||
<span class="c1"># other configuration options set.</span>
|
||||
|
||||
<span class="p">[[</span><span class="n">Default</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">AutoInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># You can create multiple isolated Reticulum</span>
|
||||
<span class="c1"># networks on the same physical LAN by</span>
|
||||
<span class="c1"># specifying different Group IDs.</span>
|
||||
|
||||
<span class="n">group_id</span> <span class="o">=</span> <span class="n">reticulum</span>
|
||||
|
||||
<span class="c1"># You can also choose the multicast address type:</span>
|
||||
<span class="c1"># temporary (default, Temporary Multicast Address)</span>
|
||||
<span class="c1"># or permanent (Permanent Multicast Address)</span>
|
||||
|
||||
<span class="n">multicast_address_type</span> <span class="o">=</span> <span class="n">permanent</span>
|
||||
|
||||
<span class="c1"># You can also select specifically which</span>
|
||||
<span class="c1"># kernel networking devices to use.</span>
|
||||
|
||||
<span class="n">devices</span> <span class="o">=</span> <span class="n">wlan0</span><span class="p">,</span><span class="n">eth1</span>
|
||||
|
||||
<span class="c1"># Or let AutoInterface use all suitable</span>
|
||||
<span class="c1"># devices except for a list of ignored ones.</span>
|
||||
|
||||
<span class="n">ignored_devices</span> <span class="o">=</span> <span class="n">tun0</span><span class="p">,</span><span class="n">eth0</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
@@ -297,7 +304,7 @@ the discovery scope by setting it to one of <code class="docutils literal notran
|
||||
<code class="docutils literal notranslate"><span class="pre">organisation</span></code> or <code class="docutils literal notranslate"><span class="pre">global</span></code>.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Default</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">AutoInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># Configure global discovery</span>
|
||||
|
||||
@@ -311,61 +318,101 @@ the discovery scope by setting it to one of <code class="docutils literal notran
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="i2p-interface">
|
||||
<span id="interfaces-i2p"></span><h2>I2P Interface<a class="headerlink" href="#i2p-interface" title="Permalink to this heading">#</a></h2>
|
||||
<p>The I2P interface lets you connect Reticulum instances over the
|
||||
<a class="reference external" href="https://i2pd.website">Invisible Internet Protocol</a>. 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.</p>
|
||||
<p>Using the I2P interface, you will get a globally reachable, portable
|
||||
and persistent I2P address that your Reticulum instance can be reached
|
||||
at.</p>
|
||||
<p>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 <a class="reference external" href="https://github.com/PurpleI2P/i2pd/releases/latest">latest release</a>
|
||||
of the <code class="docutils literal notranslate"><span class="pre">i2pd</span></code> package. For more details about I2P, see the
|
||||
<a class="reference external" href="https://geti2p.net/en/about/intro">geti2p.net website</a>.</p>
|
||||
<p>When an I2P router is running on your system, you can simply add
|
||||
an I2P interface to Reticulum:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">I2P</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">I2PInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">connectable</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>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 <code class="docutils literal notranslate"><span class="pre">rnstatus</span></code> utility.</p>
|
||||
<p>To connect to other Reticulum instances over I2P, just add a comma-separated
|
||||
list of I2P base32 addresses to the <code class="docutils literal notranslate"><span class="pre">peers</span></code> option of the interface:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">I2P</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">I2PInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">connectable</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">peers</span> <span class="o">=</span> <span class="mi">5</span><span class="n">urvjicpzi7q3ybztsef4i5ow2aq4soktfj7zedz53s47r54jnqq</span><span class="o">.</span><span class="n">b32</span><span class="o">.</span><span class="n">i2p</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>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.</p>
|
||||
<section id="backbone-interface">
|
||||
<span id="interfaces-backbone"></span><h2>Backbone Interface<a class="headerlink" href="#backbone-interface" title="Permalink to this heading">#</a></h2>
|
||||
<p>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. <strong>This interface type is currently only
|
||||
supported on Linux and Android</strong>.</p>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>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.</p>
|
||||
<p>The Backbone Interface is fully compatible with the <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code> and <code class="docutils literal notranslate"><span class="pre">TCPClientInterface</span></code>
|
||||
types, and they can be used interchangably, and cross-connect with each other. On systems that support
|
||||
<code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code>, it is generally recommended to use it, unless you need specific options or
|
||||
features that the TCP server and client interfaces provide.</p>
|
||||
</div>
|
||||
<p>It is important to note that the two methods are <em>interchangably compatible</em>.
|
||||
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.</p>
|
||||
<p>While the goal is to support <em>all</em> 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.</p>
|
||||
<p>For all types of connections over a <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code>, Reticulum will gracefully
|
||||
handle intermittency, link loss, and connections that come and go.</p>
|
||||
<section id="listeners">
|
||||
<h3>Listeners<a class="headerlink" href="#listeners" title="Permalink to this heading">#</a></h3>
|
||||
<p>The following examples illustrates various ways to set up <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code> listeners.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example demonstrates a backbone interface</span>
|
||||
<span class="c1"># that listens for incoming connections on the</span>
|
||||
<span class="c1"># specified IP address and port number.</span>
|
||||
<span class="p">[[</span><span class="n">Backbone</span> <span class="n">Listener</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">BackboneInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">listen_on</span> <span class="o">=</span> <span class="mf">0.0.0.0</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="mi">4242</span>
|
||||
|
||||
<span class="c1"># Alternatively you can bind to a specific IP</span>
|
||||
<span class="p">[[</span><span class="n">Backbone</span> <span class="n">Listener</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">BackboneInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">listen_on</span> <span class="o">=</span> <span class="mf">10.0.0.88</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="mi">4242</span>
|
||||
|
||||
<span class="c1"># Or a specific network device</span>
|
||||
<span class="p">[[</span><span class="n">Backbone</span> <span class="n">Listener</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">BackboneInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">device</span> <span class="o">=</span> <span class="n">eth0</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="mi">4242</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>If you are using the interface on a device which has both IPv4 and IPv6 addresses available,
|
||||
you can use the <code class="docutils literal notranslate"><span class="pre">prefer_ipv6</span></code> option to bind to the IPv6 address:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example demonstrates a backbone interface</span>
|
||||
<span class="c1"># listening on the IPv6 address of a specified</span>
|
||||
<span class="c1"># kernel networking device.</span>
|
||||
<span class="p">[[</span><span class="n">Backbone</span> <span class="n">Listener</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">BackboneInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">prefer_ipv6</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">device</span> <span class="o">=</span> <span class="n">eth0</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="mi">4242</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>To use the <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code> over <a class="reference external" href="https://yggdrasil-network.github.io/">Yggdrasil</a>, you
|
||||
can simply specify the Yggdrasil <code class="docutils literal notranslate"><span class="pre">tun</span></code> device and a listening port, like so:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example demonstrates a backbone interface</span>
|
||||
<span class="c1"># listening for connections over Yggdrasil.</span>
|
||||
<span class="p">[[</span><span class="n">Yggdrasil</span> <span class="n">Backbone</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">BackboneInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">device</span> <span class="o">=</span> <span class="n">tun0</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="mi">4343</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="connecting-remotes">
|
||||
<h3>Connecting Remotes<a class="headerlink" href="#connecting-remotes" title="Permalink to this heading">#</a></h3>
|
||||
<p>The following examples illustrates various ways to connect to remote <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code> listeners.
|
||||
As noted above, <code class="docutils literal notranslate"><span class="pre">BackboneInterface</span></code> interfaces can also connect to remote <code class="docutils literal notranslate"><span class="pre">TCPServerInterface</span></code>,
|
||||
and as such these interface types can be used interchangably.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Here's an example of a backbone interface that</span>
|
||||
<span class="c1"># connects to a remote listener.</span>
|
||||
<span class="p">[[</span><span class="n">Backbone</span> <span class="n">Remote</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">BackboneInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">remote</span> <span class="o">=</span> <span class="n">amsterdam</span><span class="o">.</span><span class="n">connect</span><span class="o">.</span><span class="n">reticulum</span><span class="o">.</span><span class="n">network</span>
|
||||
<span class="n">target_port</span> <span class="o">=</span> <span class="mi">4251</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>To connect to remotes over <a class="reference external" href="https://yggdrasil-network.github.io/">Yggdrasil</a>, simply
|
||||
specify the target Yggdrasil IPv6 address and port, like so:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Yggdrasil</span> <span class="n">Remote</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">BackboneInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">target_host</span> <span class="o">=</span> <span class="mi">201</span><span class="p">:</span><span class="mi">5</span><span class="n">d78</span><span class="p">:</span><span class="n">af73</span><span class="p">:</span><span class="mi">5</span><span class="n">caf</span><span class="p">:</span><span class="n">a4de</span><span class="p">:</span><span class="n">a79f</span><span class="p">:</span><span class="mi">3278</span><span class="p">:</span><span class="mf">71e5</span>
|
||||
<span class="n">target_port</span> <span class="o">=</span> <span class="mi">4343</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section id="tcp-server-interface">
|
||||
<span id="interfaces-tcps"></span><h2>TCP Server Interface<a class="headerlink" href="#tcp-server-interface" title="Permalink to this heading">#</a></h2>
|
||||
@@ -373,28 +420,27 @@ use-cases.</p>
|
||||
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.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># This example demonstrates a TCP server interface.</span>
|
||||
<span class="c1"># It will listen for incoming connections on the</span>
|
||||
<span class="c1"># specified IP address and port number.</span>
|
||||
|
||||
<span class="c1"># It will listen for incoming connections on all IP</span>
|
||||
<span class="c1"># interfaces on port 4242.</span>
|
||||
<span class="p">[[</span><span class="n">TCP</span> <span class="n">Server</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPServerInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
|
||||
<span class="c1"># This configuration will listen on all IP</span>
|
||||
<span class="c1"># interfaces on port 4242</span>
|
||||
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">listen_ip</span> <span class="o">=</span> <span class="mf">0.0.0.0</span>
|
||||
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">4242</span>
|
||||
|
||||
<span class="c1"># Alternatively you can bind to a specific IP</span>
|
||||
<span class="c1"># Alternatively you can bind to a specific IP</span>
|
||||
<span class="p">[[</span><span class="n">TCP</span> <span class="n">Server</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPServerInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">listen_ip</span> <span class="o">=</span> <span class="mf">10.0.0.88</span>
|
||||
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">4242</span>
|
||||
|
||||
<span class="c1"># listen_ip = 10.0.0.88</span>
|
||||
<span class="c1"># listen_port = 4242</span>
|
||||
|
||||
<span class="c1"># Or a specific network device</span>
|
||||
|
||||
<span class="c1"># device = eth0</span>
|
||||
<span class="c1"># port = 4242</span>
|
||||
<span class="c1"># Or a specific network device</span>
|
||||
<span class="p">[[</span><span class="n">TCP</span> <span class="n">Server</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPServerInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">device</span> <span class="o">=</span> <span class="n">eth0</span>
|
||||
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">4242</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>If you are using the interface on a device which has both IPv4 and IPv6 addresses available,
|
||||
@@ -405,20 +451,19 @@ you can use the <code class="docutils literal notranslate"><span class="pre">pre
|
||||
|
||||
<span class="p">[[</span><span class="n">TCP</span> <span class="n">Server</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPServerInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">prefer_ipv6</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">device</span> <span class="o">=</span> <span class="n">eth0</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="mi">4242</span>
|
||||
<span class="n">prefer_ipv6</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>To use the TCP Server Interface over <a class="reference external" href="https://yggdrasil-network.github.io/">Yggdrasil</a>, you
|
||||
can simply specify the Yggdrasil <code class="docutils literal notranslate"><span class="pre">tun</span></code> device and a listening port, like so:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Yggdrasil</span> <span class="n">TCP</span> <span class="n">Server</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPServerInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">device</span> <span class="o">=</span> <span class="n">tun0</span>
|
||||
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">4343</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPServerInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">device</span> <span class="o">=</span> <span class="n">tun0</span>
|
||||
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">4343</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<div class="admonition note">
|
||||
@@ -427,11 +472,11 @@ can simply specify the Yggdrasil <code class="docutils literal notranslate"><spa
|
||||
you must use the i2p_tunneled option:</p>
|
||||
</div>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">TCP</span> <span class="n">Server</span> <span class="n">on</span> <span class="n">I2P</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPServerInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">listen_ip</span> <span class="o">=</span> <span class="mf">127.0.0.1</span>
|
||||
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">5001</span>
|
||||
<span class="n">i2p_tunneled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPServerInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">listen_ip</span> <span class="o">=</span> <span class="mf">127.0.0.1</span>
|
||||
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">5001</span>
|
||||
<span class="n">i2p_tunneled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>In almost all cases, it is easier to use the dedicated <code class="docutils literal notranslate"><span class="pre">I2PInterface</span></code>, but for complete
|
||||
@@ -439,7 +484,7 @@ control, and using I2P routers running on external systems, this option also exi
|
||||
</section>
|
||||
<section id="tcp-client-interface">
|
||||
<span id="interfaces-tcpc"></span><h2>TCP Client Interface<a class="headerlink" href="#tcp-client-interface" title="Permalink to this heading">#</a></h2>
|
||||
<p>To connect to a TCP server interface, you would naturally use the TCP client
|
||||
<p>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.</p>
|
||||
<p>The TCP interface types can also tolerate intermittency in the IP link layer.
|
||||
@@ -447,10 +492,9 @@ 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.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="c1"># Here's an example of a TCP Client interface. The</span>
|
||||
<span class="c1"># target_host can be a hostname or an IPv4 or IPv6 address.</span>
|
||||
|
||||
<span class="p">[[</span><span class="n">TCP</span> <span class="n">Client</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPClientInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">target_host</span> <span class="o">=</span> <span class="mf">127.0.0.1</span>
|
||||
<span class="n">target_port</span> <span class="o">=</span> <span class="mi">4242</span>
|
||||
</pre></div>
|
||||
@@ -459,7 +503,7 @@ and restore connectivity after a failure, once the other end of a TCP interface
|
||||
specify the target Yggdrasil IPv6 address and port, like so:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Yggdrasil</span> <span class="n">TCP</span> <span class="n">Client</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPClientInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">target_host</span> <span class="o">=</span> <span class="mi">201</span><span class="p">:</span><span class="mi">5</span><span class="n">d78</span><span class="p">:</span><span class="n">af73</span><span class="p">:</span><span class="mi">5</span><span class="n">caf</span><span class="p">:</span><span class="n">a4de</span><span class="p">:</span><span class="n">a79f</span><span class="p">:</span><span class="mi">3278</span><span class="p">:</span><span class="mf">71e5</span>
|
||||
<span class="n">target_port</span> <span class="o">=</span> <span class="mi">4343</span>
|
||||
</pre></div>
|
||||
@@ -472,7 +516,7 @@ software-based soundmodems. To do this, use the <code class="docutils literal no
|
||||
|
||||
<span class="p">[[</span><span class="n">TCP</span> <span class="n">KISS</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPClientInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">kiss_framing</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">target_host</span> <span class="o">=</span> <span class="mf">127.0.0.1</span>
|
||||
<span class="n">target_port</span> <span class="o">=</span> <span class="mi">8001</span>
|
||||
@@ -491,7 +535,7 @@ you must use the i2p_tunneled option:</p>
|
||||
</div>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">TCP</span> <span class="n">Client</span> <span class="n">over</span> <span class="n">I2P</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">TCPClientInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">target_host</span> <span class="o">=</span> <span class="mf">127.0.0.1</span>
|
||||
<span class="n">target_port</span> <span class="o">=</span> <span class="mi">5001</span>
|
||||
<span class="n">i2p_tunneled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
@@ -517,7 +561,7 @@ easier to use.</p>
|
||||
|
||||
<span class="p">[[</span><span class="n">UDP</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">UDPInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="n">listen_ip</span> <span class="o">=</span> <span class="mf">0.0.0.0</span>
|
||||
<span class="n">listen_port</span> <span class="o">=</span> <span class="mi">4242</span>
|
||||
@@ -556,6 +600,62 @@ easier to use.</p>
|
||||
</pre></div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="i2p-interface">
|
||||
<span id="interfaces-i2p"></span><h2>I2P Interface<a class="headerlink" href="#i2p-interface" title="Permalink to this heading">#</a></h2>
|
||||
<p>The I2P interface lets you connect Reticulum instances over the
|
||||
<a class="reference external" href="https://i2pd.website">Invisible Internet Protocol</a>. 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.</p>
|
||||
<p>Using the I2P interface, you will get a globally reachable, portable
|
||||
and persistent I2P address that your Reticulum instance can be reached
|
||||
at.</p>
|
||||
<p>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 <a class="reference external" href="https://github.com/PurpleI2P/i2pd/releases/latest">latest release</a>
|
||||
of the <code class="docutils literal notranslate"><span class="pre">i2pd</span></code> package. For more details about I2P, see the
|
||||
<a class="reference external" href="https://geti2p.net/en/about/intro">geti2p.net website</a>.</p>
|
||||
<p>When an I2P router is running on your system, you can simply add
|
||||
an I2P interface to Reticulum:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">I2P</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">I2PInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">connectable</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>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 <code class="docutils literal notranslate"><span class="pre">rnstatus</span></code> utility.</p>
|
||||
<p>To connect to other Reticulum instances over I2P, just add a comma-separated
|
||||
list of I2P base32 addresses to the <code class="docutils literal notranslate"><span class="pre">peers</span></code> option of the interface:</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">I2P</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">I2PInterface</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">connectable</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
<span class="n">peers</span> <span class="o">=</span> <span class="mi">5</span><span class="n">urvjicpzi7q3ybztsef4i5ow2aq4soktfj7zedz53s47r54jnqq</span><span class="o">.</span><span class="n">b32</span><span class="o">.</span><span class="n">i2p</span>
|
||||
</pre></div>
|
||||
</div>
|
||||
<p>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.</p>
|
||||
<div class="admonition note">
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
<p>It is important to note that the two methods are <em>interchangably compatible</em>.
|
||||
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.</p>
|
||||
</section>
|
||||
<section id="rnode-lora-interface">
|
||||
<span id="interfaces-rnode"></span><h2>RNode LoRa Interface<a class="headerlink" href="#rnode-lora-interface" title="Permalink to this heading">#</a></h2>
|
||||
<p>To use Reticulum over LoRa, the <a class="reference external" href="https://unsigned.io/rnode/">RNode</a> interface
|
||||
@@ -573,7 +673,7 @@ relevant regulation for your location, and to make decisions accordingly.</p>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">RNodeInterface</span>
|
||||
|
||||
<span class="c1"># Enable interface if you want use it!</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># Serial port for the device</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">ttyUSB0</span>
|
||||
@@ -661,7 +761,7 @@ relevant regulation for your location, and to make decisions accordingly.</p>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">RNodeMultiInterface</span>
|
||||
|
||||
<span class="c1"># Enable interface if you want to use it!</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># Serial port for the device</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">ttyACM0</span>
|
||||
@@ -677,7 +777,7 @@ relevant regulation for your location, and to make decisions accordingly.</p>
|
||||
<span class="c1"># A subinterface</span>
|
||||
<span class="p">[[[</span><span class="n">High</span> <span class="n">Datarate</span><span class="p">]]]</span>
|
||||
<span class="c1"># Subinterfaces can be enabled and disabled in of themselves</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># Set frequency to 2.4GHz</span>
|
||||
<span class="n">frequency</span> <span class="o">=</span> <span class="mi">2400000000</span>
|
||||
@@ -719,7 +819,7 @@ relevant regulation for your location, and to make decisions accordingly.</p>
|
||||
|
||||
<span class="p">[[[</span><span class="n">Low</span> <span class="n">Datarate</span><span class="p">]]]</span>
|
||||
<span class="c1"># Subinterfaces can be enabled and disabled in of themselves</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># Set frequency to 865.6 MHz</span>
|
||||
<span class="n">frequency</span> <span class="o">=</span> <span class="mi">865600000</span>
|
||||
@@ -768,7 +868,7 @@ 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.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Serial</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">SerialInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># Serial port for the device</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">ttyUSB0</span>
|
||||
@@ -789,7 +889,7 @@ directly over a wire-pair, or for using devices such as data radios and lasers.<
|
||||
custom hardware or other systems.</p>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Pipe</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">PipeInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># External command to execute</span>
|
||||
<span class="n">command</span> <span class="o">=</span> <span class="n">netcat</span> <span class="o">-</span><span class="n">l</span> <span class="mi">5757</span>
|
||||
@@ -816,7 +916,7 @@ relevant regulation for your location, and to make decisions accordingly.</p>
|
||||
</div>
|
||||
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="p">[[</span><span class="n">Packet</span> <span class="n">Radio</span> <span class="n">KISS</span> <span class="n">Interface</span><span class="p">]]</span>
|
||||
<span class="nb">type</span> <span class="o">=</span> <span class="n">KISSInterface</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># Serial port for the device</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">ttyUSB1</span>
|
||||
@@ -885,7 +985,7 @@ relevant regulation for your location, and to make decisions accordingly.</p>
|
||||
<span class="n">ssid</span> <span class="o">=</span> <span class="mi">0</span>
|
||||
|
||||
<span class="c1"># Enable interface if you want use it!</span>
|
||||
<span class="n">interface_enabled</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">enabled</span> <span class="o">=</span> <span class="n">yes</span>
|
||||
|
||||
<span class="c1"># Serial port for the device</span>
|
||||
<span class="n">port</span> <span class="o">=</span> <span class="o">/</span><span class="n">dev</span><span class="o">/</span><span class="n">ttyUSB2</span>
|
||||
@@ -1304,10 +1404,15 @@ to <code class="docutils literal notranslate"><span class="pre">30</span></code>
|
||||
<li><a class="reference internal" href="#">Configuring Interfaces</a><ul>
|
||||
<li><a class="reference internal" href="#custom-interfaces">Custom Interfaces</a></li>
|
||||
<li><a class="reference internal" href="#auto-interface">Auto Interface</a></li>
|
||||
<li><a class="reference internal" href="#i2p-interface">I2P Interface</a></li>
|
||||
<li><a class="reference internal" href="#backbone-interface">Backbone Interface</a><ul>
|
||||
<li><a class="reference internal" href="#listeners">Listeners</a></li>
|
||||
<li><a class="reference internal" href="#connecting-remotes">Connecting Remotes</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#tcp-server-interface">TCP Server Interface</a></li>
|
||||
<li><a class="reference internal" href="#tcp-client-interface">TCP Client Interface</a></li>
|
||||
<li><a class="reference internal" href="#udp-interface">UDP Interface</a></li>
|
||||
<li><a class="reference internal" href="#i2p-interface">I2P Interface</a></li>
|
||||
<li><a class="reference internal" href="#rnode-lora-interface">RNode LoRa Interface</a></li>
|
||||
<li><a class="reference internal" href="#rnode-multi-interface">RNode Multi Interface</a></li>
|
||||
<li><a class="reference internal" href="#serial-interface">Serial Interface</a></li>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="Code Examples" href="examples.html" /><link rel="prev" title="Configuring Interfaces" href="interfaces.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/>
|
||||
<title>Building Networks - Reticulum Network Stack 0.9.2 beta documentation</title>
|
||||
<title>Building Networks - Reticulum Network Stack 0.9.5 beta documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -141,7 +141,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.2 beta documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.5 beta documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -167,7 +167,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.2 beta documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.5 beta documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -371,7 +371,7 @@ configured for Reticulum on the Raspberry Pi at site A.</p>
|
||||
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
|
||||
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.</p>
|
||||
</section>
|
||||
|
||||
Binary file not shown.
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="prev" title="Support Reticulum" href="support.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29.dev1"/>
|
||||
<title>API Reference - Reticulum Network Stack 0.9.2 beta documentation</title>
|
||||
<title>API Reference - Reticulum Network Stack 0.9.5 beta documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=189ec851f9bb375a2509b67be1f64f0cf212b702" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -141,7 +141,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.2 beta documentation</div></a>
|
||||
<a href="index.html"><div class="brand">Reticulum Network Stack 0.9.5 beta documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -167,7 +167,7 @@
|
||||
<img class="sidebar-logo" src="_static/rns_logo_512.png" alt="Logo"/>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.2 beta documentation</span>
|
||||
<span class="sidebar-brand-text">Reticulum Network Stack 0.9.5 beta documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder="Search" name="q" aria-label="Search">
|
||||
@@ -263,7 +263,7 @@ the default value.</p>
|
||||
|
||||
<dl class="py attribute">
|
||||
<dt class="sig sig-object py" id="RNS.Reticulum.LINK_MTU_DISCOVERY">
|
||||
<span class="sig-name descname"><span class="pre">LINK_MTU_DISCOVERY</span></span><em class="property"><span class="w"> </span><span class="p"><span class="pre">=</span></span><span class="w"> </span><span class="pre">False</span></em><a class="headerlink" href="#RNS.Reticulum.LINK_MTU_DISCOVERY" title="Permalink to this definition">#</a></dt>
|
||||
<span class="sig-name descname"><span class="pre">LINK_MTU_DISCOVERY</span></span><em class="property"><span class="w"> </span><span class="p"><span class="pre">=</span></span><span class="w"> </span><span class="pre">True</span></em><a class="headerlink" href="#RNS.Reticulum.LINK_MTU_DISCOVERY" title="Permalink to this definition">#</a></dt>
|
||||
<dd><p>Whether automatic link MTU discovery is enabled by default in this
|
||||
release. Link MTU discovery significantly increases throughput over
|
||||
fast links, but requires all intermediary hops to also support it.
|
||||
@@ -408,11 +408,19 @@ for addressable hashes and other purposes. Non-configurable.</p>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Identity.recall">
|
||||
<em class="property"><span class="pre">static</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">recall</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">destination_hash</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.recall" title="Permalink to this definition">#</a></dt>
|
||||
<dd><p>Recall identity for a destination hash.</p>
|
||||
<em class="property"><span class="pre">static</span><span class="w"> </span></em><span class="sig-name descname"><span class="pre">recall</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">target_hash</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">from_identity_hash</span></span><span class="o"><span class="pre">=</span></span><span class="default_value"><span class="pre">False</span></span></em><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Identity.recall" title="Permalink to this definition">#</a></dt>
|
||||
<dd><p>Recall identity for a destination or identity hash. By default, this function
|
||||
will return the identity associated with a given <em>destination</em> hash. As an
|
||||
example, if you know the <code class="docutils literal notranslate"><span class="pre">lxmf.delivery</span></code> destination hash of an endpoint,
|
||||
this function will return the associated underlying identity. You can also
|
||||
search for an identity from a known <em>identity hash</em>, by setting the
|
||||
<code class="docutils literal notranslate"><span class="pre">from_identity_hash</span></code> argument.</p>
|
||||
<dl class="field-list simple">
|
||||
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><p><strong>destination_hash</strong> – Destination hash as <em>bytes</em>.</p>
|
||||
<dd class="field-odd"><ul class="simple">
|
||||
<li><p><strong>target_hash</strong> – Destination or identity hash as <em>bytes</em>.</p></li>
|
||||
<li><p><strong>from_identity_hash</strong> – Whether to search based on identity hash instead of destination hash as <em>bool</em>.</p></li>
|
||||
</ul>
|
||||
</dd>
|
||||
<dt class="field-even">Returns<span class="colon">:</span></dt>
|
||||
<dd class="field-even"><p>An <a class="reference internal" href="#api-identity"><span class="std std-ref">RNS.Identity</span></a> instance that can be used to create an outgoing <a class="reference internal" href="#api-destination"><span class="std std-ref">RNS.Destination</span></a>, or <em>None</em> if the destination is unknown.</p>
|
||||
@@ -1294,6 +1302,46 @@ statistics, you will be able to retrieve stats such as <em>RSSI</em>, <em>SNR</e
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Link.get_mtu">
|
||||
<span class="sig-name descname"><span class="pre">get_mtu</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Link.get_mtu" title="Permalink to this definition">#</a></dt>
|
||||
<dd><dl class="field-list simple">
|
||||
<dt class="field-odd">Returns<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><p>The MTU of an established link.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Link.get_mdu">
|
||||
<span class="sig-name descname"><span class="pre">get_mdu</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Link.get_mdu" title="Permalink to this definition">#</a></dt>
|
||||
<dd><dl class="field-list simple">
|
||||
<dt class="field-odd">Returns<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><p>The packet MDU of an established link.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Link.get_expected_rate">
|
||||
<span class="sig-name descname"><span class="pre">get_expected_rate</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Link.get_expected_rate" title="Permalink to this definition">#</a></dt>
|
||||
<dd><dl class="field-list simple">
|
||||
<dt class="field-odd">Returns<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><p>The packet expected in-flight data rate of an established link.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Link.get_mode">
|
||||
<span class="sig-name descname"><span class="pre">get_mode</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Link.get_mode" title="Permalink to this definition">#</a></dt>
|
||||
<dd><dl class="field-list simple">
|
||||
<dt class="field-odd">Returns<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><p>The mode of an established link.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd></dl>
|
||||
|
||||
<dl class="py method">
|
||||
<dt class="sig sig-object py" id="RNS.Link.get_age">
|
||||
<span class="sig-name descname"><span class="pre">get_age</span></span><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#RNS.Link.get_age" title="Permalink to this definition">#</a></dt>
|
||||
@@ -2206,6 +2254,10 @@ will announce it.</p>
|
||||
<li><a class="reference internal" href="#RNS.Link.get_snr"><code class="docutils literal notranslate"><span class="pre">get_snr()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Link.get_q"><code class="docutils literal notranslate"><span class="pre">get_q()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Link.get_establishment_rate"><code class="docutils literal notranslate"><span class="pre">get_establishment_rate()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Link.get_mtu"><code class="docutils literal notranslate"><span class="pre">get_mtu()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Link.get_mdu"><code class="docutils literal notranslate"><span class="pre">get_mdu()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Link.get_expected_rate"><code class="docutils literal notranslate"><span class="pre">get_expected_rate()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Link.get_mode"><code class="docutils literal notranslate"><span class="pre">get_mode()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Link.get_age"><code class="docutils literal notranslate"><span class="pre">get_age()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Link.no_inbound_for"><code class="docutils literal notranslate"><span class="pre">no_inbound_for()</span></code></a></li>
|
||||
<li><a class="reference internal" href="#RNS.Link.no_outbound_for"><code class="docutils literal notranslate"><span class="pre">no_outbound_for()</span></code></a></li>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user